自己的Netty总结
TIP
Netty是什么?
Netty是基于reactor模型设计的异步事件驱动的通讯框架。
大体流程图如下:
IO模型
Linux下5种IO模型
事先说明: recvfrom() 是系统调用,通过socket接收数据的函数。
- 阻塞IO: 应用程序调用recvfrom,内核检查有无数据,没有应用程序就阻塞,直到有数据才去读取数据处理接下来的事情
- 非阻塞IO: 应用程序调用recvfrom,内核检查有无数据,没有数据的话应用程序不阻塞,继续处理接下来的事情,会循环调用recvfrom,直到有数据为止
- IO复用: 通过轮询代理器select注册发送消息,内核返回可读条件,等待数据准备好了,调用recvfrom将数据从内核复制到应用程序
- 信号驱动IO: 应用程序构建SIGIO信号处理程序发送sigaction信号,内核返回有无数据,有数据了应用程序调用recvfrom将数据从内核复制到应用程序
- 异步IO: 和信号驱动IO基本一致,主要区别是信号驱动IO是内核通知应用程序启动recvfrom,而异步IO是内核通知应用程序IO何时完成(也就是应用程序不调用recvfrom)
Reactor(Netty的IO模型)
传统的IO模型
一个客户端连接对应一条线程,线程中包含IO的连接、读写和业务等处理。
缺点: 需要的线程数量较多,并且线程阻塞的时候占用的系统资源和开销特别大,浪费系统资源不值得推荐。
reactor模型
reactor1 -> reactor2 -> reactor3
- reactor1: 多个客户端连接对应一个reactor,其内部包含了一个eventDispatch用于分发事件,并且把socket连接后的acceptor事件单独出来,其他的 处理作为Handler交由一个线程池来处理
- reactor2: 为了业务和IO分离,把其他的IO处理比如read,send也分离出来,业务单独使用一个线程池来处理,这样就能提高网络的读写性能,同时也可以 进行业务的多线程处理,大大提高了系统的吞吐
- reactor3: 当连接增加有大量客户端需要网络读写时,也就是高并发的时候就需要一个线程池来管理网络IO, 并且我们发现网络连接acceptor是后续事件和业务处理的前提,此时考虑把
一个reactor拆分成两个
,一个叫做mainReactor
用来接收客户端连接处理acceptor,另一个叫做subReactor
使用两个线程池,一个处理IO事件,另一个处理业务,至此这个就是netty的雏形
Netty的 reactor模型
在Netty中,bossGroup相当于mainReactor,workerGroup相当于SubReactor与Worker线程池的合体。如:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class);
bossGroup bossGroup线程池负责监听端口,获取一个线程作为MainReactor,用于处理端口的Accept事件。 workerGroup workerGroup线程池负责处理Channel(通道)的I/O事件,并处理相应的业务。 在启动时,可以初始化多个线程。
EventLoopGroup bossGroup = new NioEventLoopGroup(2);
EventLoopGroup workerGroup = new NioEventLoopGroup(3);
Netty工作原理(结合上面的工作架构图)
服务端包含了1个boss NioEventLoopGroup和1个work NioEventLoopGroup。 NioEventLoopGroup相当于1个事件循环组, 组内包含多个事件循环(NioEventLoop),每个NioEventLoop包含1个Selector和1个事件循环线程。
- 1、boss NioEventLoop循环任务 轮询Accept事件。 处理Accept IO事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个work NioEventLoop的Selector上。 处理任务队列中的任务。
- 2、work NioEventLoop循环任务 轮询Read、Write事件。 处理IO事件,在NioSocketChannel可读、可写事件发生时进行处理。 处理任务队列中的任务。
- 3、任务队列中的任务 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
//...
}
});
非当前 Reactor 线程调用 Channel 的各种方法 例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用, 然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费。 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
//...
}
}, 60, TimeUnit.SECONDS);
(1) 拆包和粘包
是什么?
客户端用一个循环向服务器发送消息1-9数列。然后服务端打印这些消息。 等次数多了以后,服务端打印时会发现一些问题,比如我们发送的字符串为“123456789”,大部分打印的是123456789; 有一部分变成了123456789123,这就是粘包;有一部分变为了1234,这就是拆包。
为什么会有这种现象?
虽然在我们代码层面,传输的数据单位是ByteBuf。但是到了更底层,用到了TCP协议,终究会按照字节流发送数据。而底层并不知道应用层数据的具体含义, 它会根据TCP缓冲区的实际情况进行数据包的划分。所以最终到达服务端的数据产生上面的现象。
如何解决?
Netty为我们提供了4种解决方法:
- FixedLengthFrameDecoder:固定长度拆包器,每个数据包长度都是固定的。
- LineBasedFrameDecoder:行拆包器,每个数据包之间以换行符作为分隔。
- DelimiterBasedFrameDecoder:类似行拆包器,不过我们可以自定义分隔符。
- LengthFieldBasedFrameDecoder:基于长度域拆包器,最常用的,只要你的自定义协议中包含数据长度这个部分,就可以使用。 它需要三个参数,第一个 是数据包最大长度、第二个是参数长度域偏移量、第三个是长度域长度。
(2) 零拷贝
Java NIO零拷贝: 堆内内存数据转化成对外内存数据,有下面两种方式
- mmap 内存映射
- sendfile
零拷贝的实用
- Netty的零拷贝:完全基于java的零拷贝方式
- RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;
- Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。但是值得注意的一点是, Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。