Netty
Netty 是一个非阻塞(异步)、事件驱动的网络框架,用多线程处理 IO 事件。
一. Netty 结构
Netty 服务端与客户端都是由 Bootstrap 引导程序开始的,对于服务端,引导类是 ServerBootstrap,对于客户端,引导类是 Bootstrap。
从 ServerBootstrap 开始,Netty Server 的结构如下:
- ServerBootstrap
- EventLoopGroup
- EventLoop:事件循环线程,通过 Selector 管理多个连接 Channel
- Channel:连接
- ChannelPipeline:管道,由多个 ChannelHandler 串联构成,处理连接的逻辑;
- EventExecutorGroup:
- EventExecutor:事件执行器,继承于 ExecutorService 接口,用于处理阻塞线程;
- ChannelHandler:逻辑处理器,处理一部分连接事件;
- ChannelHandlerContext:Handler 上下文,Pipeline 用 Context 调用每一个方法;
- ChannelHandlerAdapter:业务逻辑
- Channel:连接
- EventLoop:事件循环线程,通过 Selector 管理多个连接 Channel
- EventLoopGroup
一个典型的 Netty Server 如下:
代码语言:javascript复制public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
}
});
serverBootstrap.bind(8000);
}
}
1.1 Bootstrap
ServerBootstrap 有两个 EventLoopGroup:
- bossGroup:包含一个单例的 Server Channel,它持有一个绑定了本地端口的 Socket,用来监听和接收客户端的连接 (Channel),并交给 workerGroup 进行处理;
- workerGroup:包含所有接入的客户端连接 (Channel);
1.2 EventLoopGroup 与 EventLoop
EventLoopGroup 是一个事件循环集合,每个 EventLoopGroup 都有一个或多个 EventLoop。EventLoop 是一个事件循环线程,它通过 Java NIO 的 selector 管理多个 Channel。
1.3 Channel
Channel 接受 socket 传来的数据,进入到每个 Channel 都有的 ChannelPipeline 中,使用责任链模式,数据经过经过一系列处理器 ChannelHandler 逻辑处理,将数据传出。 ChannelPipeline 是 ChannelHandler 的一个链表,如果一个入站 (inbound) 事件触发,会从第一个 ChannelHandler 开始遍历所有的 ChannelHandler;如果出站 (outbound) 事件触发,则会从最后一个 ChannelHandler 开始反向遍历执行所有逻辑。 每个 ChannelHandler 添加到 ChannelPipeline 之后,都会创建一个 ChannelHandlerContext,并与 ChannelHandler 关联绑定,每个 ChannelHandlerContext 都有 prev, next 指针,与其他的 ChannelHandlerContext 关联,这样就形成了 ChannelPipeline 的链式结构。
1.4 EventExecutor
Netty 处理事件 IO 有很多线程,处理时尽量不要阻塞线程,因为阻塞会降低程序的性能。在添加 ChannelHandler 到 ChannelPipeline 时,可以指定一个 EventExecutorGroup,可以从中获取 EventExecutor。
- EventExecutor:可以执行的线程;
- EventExecutorGroup:线程组,用于管理和分配多个 EventExecutor;
在 ChannelPipeline 添加 ChannelHandler 时,可以为 ChannelHandler 指定 EventExecutor,用线程的方式执行 ChannelHandler 的方法。
1.5 ByteBuf
在 Java NIO 中也有 ByteBuffer 的缓冲类,但它读写索引是放在一起的,而且更改读写状态需要使用 filp() 方法转换状态,整体使用比较麻烦。 Netty 对 Java NIO 进行了优化,形成优化后的数据容器 ByteBuf。它针对 ByteBuffer 类的缺点进行了优化,分为了读写两部分,可以在任意位置读取数据,开发者只需要调整数据索引位置,以及再次开始读操作即可。所以 ByteBuf 本质就是一个由不同的索引分别控制读访问和写访问的字节数组。ByteBuf 的数据结构如下所示:
容器里面的的数据分为三个部分:
- 已经丢弃的字节:这部分数据是无效的;
- 可读字节:这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;
- 可读字节之前的指针即为读指针 (readerIndex);
- 可写字节:所有写到 ByteBuf 的数据都会写到这一段;
- 可写指针之前的指针为写指针 (writerIndex);
- capacity = 已丢弃字节 可读字节 可写字节,表示 ByteBuf 底层内存的总容量;
- 最后一部分虚线表示的是该 ByteBuf 最多还能扩容多少容量;
ByteBuf 有三种模式:
- 堆内存模式:分配对象都在 Java 堆上;
- 优点:由于数据存储在 Jvm 堆中,所以可以快速创建和快速释放,并且提供了数组直接快速访问的方法;
- 缺点:每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区
- 直接内存模式:分配对象都在堆外内存上;
- 优点:使用 Socket 传递数据时性能很好,避免了数据从 JVM 堆内存拷贝到直接缓冲区的过程,提高了性能;
- 缺点:相对于堆缓冲区而言,Direct Buffer 分配内存空间和释放更为昂贵;
- 注:对于涉及大量 I/O 的数据读写,建议使用直接内存;而对于用于后端的业务消息编解码模块建议使用堆内存模式;
- 复合模式:本质上类似于提供一个或多个 ByteBuf 的组合视图,可以根据需要添加和删除不同类型的 ByteBuf;
二. Reactor 模式
Netty 是一个典型的 反应器设计模式 (Reactor)。Reactor 模式是一种基于事件响应的模式,将多个客户进行统一的分离和调度,同步、有序的处理请求。
注:在 Netty 中采用了主从线程模型的 Reactor,即 Bootstrap 的两个 NioEventLoopGroup:bossGroup, workerGroup。
- 优点:
- 响应快,不被阻塞;
- 可扩展性强;
- 对编程友好;
- 缺点:
- 不易于调试;
- 需要底层操作系统支持 select 等多路复用技术;
Reactor 有三种模式:
- Reactor 单线程模式:
- 同时作为 TCP 客户端与服务端;作为客户端,向服务端发送请求;作为服务端,接受请求并处理;
- Reactor 多线程模式:
- 一个 Acceptor 线程用于接受连接,一组 NIO 线程用于处理逻辑;
- 通常这种模式是使用线程池的方式管理 NIO 线程的;
- Reactor 主从模式:
- 一组 Acceptor 线程用于接受连接,一组 NIO 线程用于处理逻辑;
- 当前 Netty 主要使用的方式,即 bossGroup, workerGroup 的方式;
三. TCP 粘包、半包问题
参考地址:《Netty 入门与实战:仿写微信 IM 即时通讯系统》
尽管我们在应用层面使用了 Netty,但是对于操作系统来说,只认 TCP 协议。尽管我们的应用层是按照 ByteBuf 为 单位来发送数据,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf。 而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的。因此,我们需要在客户端根据自定义协议来组装我们应用层的数据包,然后在服务端根据我们的应用层的协议来组装数据包,这个过程通常在服务端称为拆包,而在客户端称为粘包。
Netty 自带的拆包器:
- FixedLengthFrameDecoder:固定长度的拆包器;
- 应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。
- LengthFieldBasedFrameDecoder:基于长度域拆包器;
- 最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。例如 Dubbo 就有自己定义的协议,在 DubboProtocol 的对象头中包含请求的长度与包的长度,根据这些信息可以计算出来当前请求会出现粘包还是半包现象;
注:此外还有不怎么常用的行拆包器和分隔符拆包器;
- 行拆包器 LineBasedFrameDecoder:
- 从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。
- 分隔符拆包器 DelimiterBasedFrameDecoder
- DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。