Netty一文搞懂——核心原理篇<随手笔记>
1.server端工作原理
1.1.原理
server端启动时绑定本地某个端口,将自己NIOServerSocketChannel
注册到某个bossNIOEventLoop
的selector
上。
server端包含一个BossNIOEventGroup
和一个WorkerNioEventaLoopGroup
,NioEventaLoopGroup
相当于一个事件循环组,这个事件循环组里包含多个事件循环NioEventLoop
,每个NioEventLoop
包含一个selector
和一个事件循环线程
。
每个BoosNioEventLoop循环执行的任务包含三步:
- 轮询read、write事件
- 处理I/O任务,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
- 处理任务队列中的任务,runAllTasks
1.2.启动流程
public class NettyServer {public static void main(String[] args) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(boosGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {protected void initchannel(NioSocketChannel ch) {}});serverBootstrap.bind(8000);}
}
- 首先创建了两个NioEventLoopGroup,这两个对象可以看做是传统IO编程模型的两大线程组,bossGroup表示监听端口,accept新连接的线程组,workerGroup表示处理每一条连接的数据读写的线程组。
- 接下来创建了一个引导类ServerBootstrap,这个类将引导我们进服务端的启动工作,直接new出来。
- 通过
.group(boosGroup, workerGroup)
给引导类配置两大线程组,此引导类的线程模型也就定型了。 - 然后指定服务端的I/O模型为NIO,我们通过
.channel(NioServerSocketChannel.class)
来制定IO模型。 - 最后我们调用
childHandler()
方法给这个引导类创建一个channelInitialilzer,这里主要就是定义后续每条连接的数据读写、业务处理逻辑。channelInitiater这个类中的泛型参数NioSocketChannel是Netty对NIO类型的连接的抽象,而NioServerSocketChannel也是对NIO类型的连接的抽象,NioServerSocketChannel和NioSocketChannel的概念可以喝BIO编程模型中的serverSocket以及Socket两个概念对应上。
创建一个引导类,然后给它指定线程模型、IO模型、连续读写处理逻辑,绑定端口之后服务就启动起来了。
2.client端工作原理
2.1.原理
client端启动时connect到server建立NioSocketChannel,并注册到某个NioEventLoop的Selector上。
client端只包含一个NioEventLoopGroup,每个EventLoop循环执行的任务包含三步:
- 轮询connect、read、write事件
- 处理I/O任务,即connect、read、write事件,在NioSocketChannel上建立连接。可读、可写事件发生时进行处理
- 处理非I/O任务,runAllTasks
2.2.启动流程
@Slf4j
public class NettyClient {public static void main(String[] args) {NioEventLoopGroup workerGroup = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {}});bootstrap.connect("ip", 80).addListener(future -> {if (future.isSuccess()) {log.info("connect success.");} else {log.info("connect fail.");}});}
}
- 首先与服务端的启动一样,需要给它指定线程模型,驱动着连接的数据读写
- 然后指定IO模型为
NioSocketChannel
,表示IO模型为NIO - 接着给引导类指定一个handler,这里主要就是定义连接的业务处理逻辑
- 配置完线程模型、IO模型、业务处理逻辑之后,调用connect方法进行连接,可以看到connect方法有两个参数,第一个参数可以填写
IP或者域名
,第二个参数填写的是端口号
,由于方法返回的是Future
,所以我们通过addListener
方法监听结果。
3.ByteBuf
是一个节点容器,里面包含三部分内容:
- 已经丢弃的数据,这部分数据是无效的
- 可读字节,这部分数据是
ByteBuf
的主体- 可写字节
这三段数据被两个指针给划分出来:读指针、写指针
本质:
- 引用一段内存,此内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对ByteBuf的读写,可以理解为外观模式的一种使用
- 基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法,要注意read或write与get/set的区别
- 多个ByteBuf可以引用计数来控制内存的释放,遵循谁retain()谁release()的原则
ByteBuf和ByteBuffer的区别:
- 可扩展到用户定义的buffer类型
- 通过内置的复合buffer类型实现透明的零拷贝(zero-copy)
- 容量可以根据需要扩展
- 切换读写 模式不需要调用
ByteBuffer.flip()
方法 - 读写采用不同的索引
- 支持方法链接调用
- 支持引用计数
4.池技术–线程池等
4.1.ByteBuf和设计模式
ByteBufAllocator
:抽象工厂模式
在Netty的世界里ByteBuf实例通常应该由ByteBufAllocator来创建
CompositeByteBuf
:组合模式
CompositeByteBuf
可以让我们把多个ByteBuf当成一个Buf来处理,ByteBufAllocator
提供了compositeBuffer()
工厂方法来创建compositeByteBuf
。它的实现使用了组合模式。ReadOnlyByteBuf
:装饰器模式
ReadOnlyByteBuf
使用装饰器模式把一个ByteBuf变为只读,ReadOnlyByteBuf通过调用unpooled.unmodifiableBuffer(ByteBuf)方法获得。
ByteBufInputStream
:适配器模式
ByteBufInputStream
使用适配器模式使我们可以把ByteBuf
当做Java的InputStream
来使用,同理,ByteBufOutputStream
允许我们把ByteBuf
当做PutputStream
来使用。ByteBuf
:工厂方法模式
一般不直接通过构造函数来创建ByteBuf实例,而是通过
Allocator
来创建。从装饰器模式可以看出另一种获得ByteBuf的方式是调用ByteBuf的工厂方法,如:- ByteBuf # duplicate()
- ByteBuf # slice()
4.2.channelHandler
channelHandler在只会对感兴趣的时间进行拦截处理,servlet的Filter过滤器负责对IO事件或者IO操作进行拦截和处理,他可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。pipeline和channelHandler他们通过责任链接设计模式来组织代码逻辑,并且支持逻辑的动态添加和删除。
channelHandler有两大子接口:
- channelInBoundhandler,是处理读数据的逻辑
- channelOutBoundHandler,是处理写数据的逻辑
这两个接口分别对应的默认实现,ChannelInBoundHandlerAdapter和ChannelOutBoundHandlerAdapter他们分别实现了两大接口的所有功能,默认情况下会把读写事件传播到下一个handler。
事件的传播:
abstractChannel
直接调用了pipeline的write()
方法,因为write是个output事件,所以DefaultChannelPipeline
直接找到tail
部分的context
调用其write()
方法。
5.NioEventLoop
NioEventLoop除了处理Io事件还有主要:
- 非IO操作的系统Task
- 定时任务
非IO操作和IO操作各占默认值50%,底层使用Selector(多路复用器)
Selector BUG出现的原因: 若selector
的轮询结果为空,也没有wakeup
或新消息处理,则发生空轮询,cpu使用率100%
Netty的解决方法:
- 对
selector
的select
操作周期进行统计,每完成一次空selector
操作进行一次计数 - 若咋某个周期内连接发生N次空轮询,则触发
epoll
死循环bug - 重建selector,判断是否是其他线程发起的重建请求,若不是则将原
socketChannel
从旧的selector
上去除注册,重新注册到新的selector
上,并将原来的selector
关闭
6.Netty内存池和对象池
6.1.基本概念
内存池: 指为了实现内存池的功能,设计一个内存结构chunk,其内部管理者一个大块的连接内存区域,将这个内存区域切分均等的大小,每一个大小称之为一个page。将从内存池中申请内存的动作映射为从chunk中申请一定数量page。为了方便计算和申请page,chunk内部采用完全二叉树的方式对page进行管理。
对象池: 指recycler整个对象池的核心实现由ThreadLocal和stack及wrakOrderQueue构成,接着来看stack和wrakOrderQueue的具体实现,最后概括整体实现。
6.2.设计核心
- stack相当于是一个缓存,同一个线程内的使用和回收都将使用一个stack
- 每个线程都会有一个自己对应的stack,如果回收的线程不是stack的线程,将元素放入到Queue中
- 所有的Queue组合成一个链表,stack可以从这些链表中回收元素(实现了多线程之间共享回收的实例)
7.心跳和空闲检测
连接假死:
在某一端(服务端或者客户端)看来,底层的TCP连接已经断开了,但是应用程序并没有捕获到,因此会认为这条连接仍然是存在的,从TCP层面来说,只有收到四次握手数据包或者一个RST数据包,连接的状态才表示已断开。
导致的问题:
- 对于服务端每条连接都耗费CPU和内存资源,大量假死的连接会耗光服务器的资源
- 对于客户端,假死会造成发送数据超时,影响用户体验
链接假死的原因:
- 应用线程出现线程堵塞,无法进行数据的读写
- 客户端或服务端出现网络相关的故障
- 公网丢包
7.1.服务端空闲检测
如果能一直收到客户端发来的数据,则此条连接是活的,因此服务端对于连接假死的应对策略是空闲检测;
简化一下,服务端只需检测一段时间内是否收到客户端发来的数据即可,Netty自带的IdleStateHandler就实现了此功能。
IdleStateHandler构造器有四个参数:
- 表示读空闲时间:指的是在这段时间内没有数据读到,则连接假死
- 写空闲时间:指的是在这段时间内没有数据写到,则连接假死
- 读写空闲时间:如没有产生读或写,则连接假死
- 时间单位
7.2.客户端定时心跳
服务端在一段时间内没有收到客户端的数据有两种情况:连接假死;非假死的确没有数据;所以我们要排出第二种情况的假死,定期向服务端发送心跳。
实现了每隔五秒向服务端发送一个心跳数据包,这个时间段通常要比服务端的空间检测时间的一半要短一些,我们这直接定义为空闲检测时间的三分之一,主要是为了排除公网偶发的秒级抖动。
为了排除是否是因为服务端在非假死状态下确实没有发送数据,服务端也要定期发送心跳检测到客户端。
8.拆包粘包理论与解决
TCP是个“流协议”,所谓流就是灭幼界限的一串数据。TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实际情况进行包的拆分,所以在业务上认为一个完整的包可能被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包的问题。
解决方法:
- 封装自己的包协议:包=包内容(4byte)+包内容
- 对于粘包问题先读出包头即包体长度n,然后再读取长度为n的包内容,这样数据包之间的边界就清楚了
- 对于断包问题先读出包头即包体长度n,由于此次读取的缓冲区长度小于n,这时候就需要先缓存这部分的内容,等待下次read事件来时拼接起来形成完整的数据包。
8.1.Netty自带的拆包器
- 固定长度的拆包器FixedlengthFrameDecoder
如果应用层协议非常简单,每个数据包的长度都是固定的,比如100,那么只需要把这个拆包器加到pipeline中,netty会把一个长度为100的数据包(ByteBuf)传递到下一个channelhandler.
- 分隔符拆包器DelimiterBasedFrameDecode
从字面意思来看,发送断发送数据包的时候每个数据包之间以换行符作为分隔,接收端通过DelimiterBasedFrameDecode将粘过的ByteBuf拆分成一个完整的应用层数据包。
- 行拆包器LinebasedFrameDecode
LinebasedFrameDecode是行拆包器的通用版本,只不过我们可以自定义分隔符
- 基于长度域拆包器lengthFieldbasedFrameDecoder
这种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包