
工作流程(对应图中的箭头关系)
Netty是一个基于Java NIO的异步事件驱动网络应用程序框架。
-
客户端发起连接 :
Client向服务端发起连接请求。 -
BossGroup 接收连接:
-
BossGroup 中的某个
NioEventLoop通过Selector监听到 Accept 事件。 -
调用
processSelectedKeys执行ServerSocketChannel的accept(),得到一个新的SocketChannel(代表客户端连接)。
-
-
注册到 WorkerGroup:
- 将该
SocketChannel注册到WorkerGroup中的某个NioEventLoop的Selector上,并监听 Read/Write 事件。
- 将该
-
WorkerGroup 处理读写:
-
Worker 中的
NioEventLoop监听到 Read 事件后,读取数据并传递给Pipeline中的ChannelHandler链处理。 -
处理结果(如响应)通过
Pipeline写回客户端。
-
主从Reactor多线程模式
可以把主从Reactor看作是两个线程池 (NioEventLoopGroup事件循环组),其中的每一个线程可以看作是一个NioEventLoop,即不断循环的执行处理任务的线程。每一个NioEventLoop都包含了两个部分:Selector和任务队列。NIO中Selector是用于监听accept事件,通过processSelectorKeys()方法创建socketChannel。
主Reactor
它的NioEventLoop维护了一个注册了NioServerSocketChannel的Selector。只负责监听并接收客户端的连接。它会轮询accept事件。
当有客户端发出连接请求时,接收连接,并分配一个NioSocketChannel,并将其交给从Reactor。
如果任务队列中有任务,就去执行任务。
从Reactor
接收到了主Reactor分配过来的连接后,会将连接分配给一个空闲的NioEventLoop线程。Netty会将此连接分配并注册到Selector维护的连接队列中并监听事件。
轮询read ,write事件
监听到了事件,在对应的NioSocketChannel中处理此IO事件
如果任务队列中有任务,就去执行任务。
ChannelPipeline
管道,具体的业务处理的地方。在每一个NioSocketChannel中都有一个对应的ChannelPipeline,可以看作是一个双向处理器链表,用于处理入站和出站事件。由一系列ChannelHandler组成,每一个节点都是它的实例,可以自定义。当有事件发生的时候,Pipeline会将事件(数据)从链表头开始依次传递给每一个ChannelHandler进行处理。
入站和出站事件
真正具体处理的地方了。如数据的读操作,比如客户端给服务端发消息,服务端可以直接通过
自身入站事件的channelRead方法读取消息。出站就是数据写出站,比如服务端准备发送一个消息,如果有一个出站处理器,那么数据在发送之前会被此处理器拦截处理。
自定义Handler需要继承ChannelInboundHandlerAdapter,它提供了便捷的方法处理事件,只需要关注逻辑即可。

如:读,读完成,异常捕获。。。。
任务队列
任务队列通常执行一些IO外的异步/耗时任务。它不会阻塞当前的NioEventLoop线程。当执行完IO操作后,会去检查此队列是否有任务,有就通过任务执行器去执行。提交任务可以通过ctx.exector()或者ctx.channel.eventloop.execute()。自定义任务,定时任务都可以。
服务器端的启动配置
Netty是基于NIO的,封装很多的底层实现,可能就配置的时候所谓麻烦点,配置完之后只需要关注业务逻辑就可以了,客户端也是如此:


异步模型ChannelFuture
这个异步的结果体现在,执行某方法后,会立即返回这个异步Future对象,不会阻塞当前的线程。而这个Future对象则表示未来会产生的一个结果。跟webFlux中的异步对象Mono<type>差不多的道理。

这里则表示绑定服务器的未来结果是否成功。sync阻塞当前线程,直到绑定出结果。以确保服务端的正常运行。绑定的结果可以通过注册一个监听器回调来获取

粘包和拆包
产生粘包和拆包问题的主要原因是,操作系统在发送 TCP 数据的时候,底层会有一个缓冲区,例如 1024 个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP 则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP 就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。

上图中演示了粘包和拆包的三种情况:
- A 和 B 两个包都刚好满足 TCP 缓冲区的大小,或者说其等待时间已经达到 TCP 等待时长,从而还是使用两个独立的包进行发送;
- A 和 B 两次请求间隔时间内较短,并且数据包较小,因而合并为同一个包发送给服务端;
- B 包比较大,因而将其拆分为两个包 B_1 和 B_2 进行发送,而这里由于拆分后的 B_2 比较小,其又与 A 包合并在一起发送。
常见粘包和拆包解决方案
- 客户端在发送数据包的时候,每个包都固定长度,比如 1024 个字节大小,如果客户端发送的数据长度不足 1024 个字节,则通过补充空格的方式补全到指定长度;
- 客户端在每个包的末尾使用固定的分隔符,例如 \r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的 \r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;
- 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。
对应netty的解决方案
FixedLengthFrameDecoder
LineBasedFrameDecoder 与 DelimiterBasedFrameDecoder
LengthFieldBasedFrameDecoder 与 LengthFieldPrepender
这里LengthFieldBasedFrameDecoder与LengthFieldPrepender需要配合起来使用,其实本质上来讲,这两者一个是解码,一个是编码的关系。它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。LengthFieldBasedFrameDecoder会按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据;而LengthFieldPrepender则会在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。LengthFieldBasedFrameDecoder的解码过程如下图所示:

LengthFieldPrepender的编码过程如下图所示:

MessageToByteEncoder和ByteToMessageDecoder
如果用户确实需要不通过继承的方式实现自己的粘包和拆包处理器,这里可以通过实现MessageToByteEncoder和ByteToMessageDecoder来实现。这里MessageToByteEncoder的作用是将响应数据编码为一个 ByteBuf 对象,而ByteToMessageDecoder则是将接收到的 ByteBuf 数据转换为某个对象数据。