Netty是什么
- 高性能、异步事件驱动的NIO框架,提供了对TCP、UDP和文件传输
- 基于主从reactor模型通过主从线程池实现事件调度
- 优化底层epoll方法避免空轮循情况
Netty服务端创建过程
- 创建启动对象ServerBootstrap
- 绑定主从线程池,EventLoopGroup
- 设置并且绑定服务端通道(Channel),Netty中也就是NioServerSocketChannel。
- 在链路创建的时候初始化ChannelPipeline。
- ChannelPipeline主要负责管理执行相关的网络事件(根据ChannelHandler的执行策略调度ChannelHandler执行)
- 它本质上是一个负责处理网络事件的职责链(用参数配置),负责管理和执行ChannelHandler
- ChannelPipeline调度ChannelHandler。ChannelHandler(下面的WebSocketServerHandler)是处理业务的关键接口,是Netty提供给用户定制和扩展需要实现的接口。比如下面这里就实现了处理网络请求、发送响应信息等。ChannelHandler提供了很多定制化的处理类供使用
- 然后Selector会轮询,遇到准备就绪的Channel之后就由Reactor线程NioLoop执行ChannelPipeline的对应方法调度Handler去实现具体业务。
Netty的组件
- ServerBootstrap,Netty 应用程序通过设置 bootstrap(引导)类的开始,该类提供了一个 用于应用程序网络层 配置的容器。
- Channel、ChannelHandler,底层网络传输 API 必须提供给应用 I/O操作的接口,如读,写,连接,绑定等等。对于我们来 说,这是结构类似“socket”。处理应用程序逻辑。
- ChannelPipline,ChannelPipeline 提供了一个容器给 ChannelHandler 链并提供了一个API 用于管理沿着链入 站和出站事件的流动。
- ChannelFuture,Netty 所有的 I/O 操作都是异步。因为一个操作可能无法立即返回,我们需要有一种方法在以 后确定它的结果。出于这个目的,Netty 提供了接口 ChannelFuture,它的 addListener 方法注 册了一个 ChannelFutureListener ,当操作完成时,可以被通知(不管成功与否)。
ByteBuf字节数据容器
- 网络数据的基本单位永远是 byte(字节) ,Java NIO 提供 ByteBuffer 作为字节的容器,但它的作用太有限,也没有进行优化。
- ByteBuf 是一个很好的经过优化的数据容器,我们可以将字节数据有效的添加到 ByteBuf 中或 从 ByteBuf 中获取数据。
- 为了便于操作,ByteBuf 提供了两个索引:一个用于读,一个用于 写。我们可以按顺序的读取数据,也可以通过调整读取数据的索引或者直接将读取位置索引 作为参数传递给get方法来重复读取数据
ByteBuf使用流程
- 写入数据到 ByteBuf 后,writerIndex(写入索引)增加写入的字节数。
- 读取字节后, readerIndex(读取索引)也增加读取出的字节数。
- 你可以读取字节,直到写入索引和读取索 引处在相同的位置。此时ByteBuf不可读,所以下一次读操作将会抛出 IndexOutOfBoundsException,就像读取数组时越位一样。
- 调用 ByteBuf 的以 "read" 或 "write" 开头的任何方法都将自动增加相应的索引。另一方 面,"set" 、 "get"操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。
- 可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入超过这 个值的数据的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。
- ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访 问。一个容量为16的空的 ByteBuf 的布局和状态,writerIndex 和 readerIndex 都 在索引位置 0
Netty的线程模型
- Netty线程模型主要基于主从 Reactor 多线程模型做了一定的改进,其中主从Reactor多线程模型有多个 Reactor。
- 内部实现了两个线程池,boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的连接事件,当接收到连接事件的请求时,把对应的socket封装到一个NioSocketChannel 中,并交给 work 线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。
Netty零拷贝
- 即CPU在执行某项任务时不需要先将数据从内存中的一个位置移动到另一个位置就可以完成操作,从而节省了CPU时钟周期和内存带宽。
- 在操作系统层面,将数据从来源设备A发送到目标设备B时
- 需要先将数据~~从A的内核空间读缓冲区复制到用户空间缓冲区(即应用程序提供的一块buffer)
- 再从用户空间缓冲区~~复制到B的内核空间写缓冲区。
- 将数据从A的内核读缓冲直接移动到B的内核写缓冲里,即整个数据的流动都在内核空间完成,不需要再向用户空间里走一遍了
- 例如将磁盘中的一个文件发送到网络中时,
- 正常情况下我们需要在程序中开辟一块buffer, 先将数据从磁盘中读到这个buffer中。
- 这个过程里就发生了数据从磁盘到内核空间读缓冲再到用户空间程序buffer的数据流动。
- 随后我们再从自己的buffer中将数据写入到socket,实际上发生了从用户空间程序buffer到内核空间socket写缓冲的数据流动
Netty实现零拷贝的方法
- 优秀文章:https://zhuanlan.zhihu.com/p/88599349
- 避免数据流经用户空间
- Netty在这一层对零拷贝实现就是FileRegion类的transferTo()方法,我们可以不提供buffer完成整个文件的发送,不再需要开辟buffer循环读写。避免内核到用户空间的拷贝
- 避免数据从JVM Heap到C Heap的拷贝
- JVM层面,每当程序需要执行一个I/O操作时,都需要将数据先从JVM管理的堆内存复制到使用C malloc()或类似函数分配的Heap内存中才能够触发系统调用完成操作
- 这部分内存站在Java程序的视角来看就是堆外内存,但是以操作系统的视角来看其实都属于进程的堆区,OS并不知道JVM的存在
- 这样一来JVM在I/O时永远比使用native语言编写的程序多一次数据复制,这是所有基于VM的编程语言都绕不开的问题
- Netty中对零拷贝思想的第二处实现,就是在适当的位置直接使用堆外内存从而避免了数据从JVM Heap到C Heap的拷贝。
- 减少数据在用户空间的多次拷贝
- 这里Netty的第三个层次的实现,就是提供了CompositeByteBuf类,它提供了对多个ByteBuffer的一个"视图",可以将它们逻辑上当成一个完整的ByteBuffer来操作,这样就免去了重新分配空间再复制数据的开销。
Netty心跳机制
- 为什么要心跳
- 因为网络的不可靠性, 有可能在 TCP 保持长连接的过程中, 由于某些突发情况, 例如网线被拔出, 突然掉电等, 会造成服务器和客户端的连接中断.
- 在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话, 那么它们是不能在短时间内发现对方已经掉线的. 为了解决这个问题, 我们就需要引入 心跳 机制.
- 如何实现心跳
- 使用 TCP 协议层面的 keepalive 机制.
- 在应用层上实现自定义的心跳机制.
- 缺点:
- 它不是 TCP 的标准协议, 并且是默认关闭的.
- TCP keepalive 机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 两个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够.
- TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了.
- 使用 Netty 实现心跳机制的关键就是利用 IdleStateHandler 来产生对应的 idle 事件.
- 一般是客户端负责发送心跳的 PING 消息, 因此客户端注意关注 ALL_IDLE 事件, 在这个事件触发后, 客户端需要向服务器发送 PING 消息, 告诉服务器"我还存活着".
- 服务器是接收客户端的 PING 消息的, 因此服务器关注的是 READER_IDLE 事件, 并且服务器的 READER_IDLE 间隔需要比客户端的 ALL_IDLE 事件间隔大(例如客户端ALL_IDLE 是5s 没有读写时触发, 因此服务器的 READER_IDLE 可以设置为10s)
- 当服务器收到客户端的 PING 消息时, 会发送一个 PONG 消息作为回复. 一个 PING-PONG 消息对就是一个心跳交互.
Netty如何解决粘包和拆包
- 消息定长,例如每个报文的大小固定长度200字节,不够空位补空格
- 在包尾增加回车换行符进行分割,例如FTP协议
- 将消息分为消息头和消息体,消息头中包含表示消息总长度的字段
- 更复杂的应用层协议。
// 消息格式 [长度][消息体], 解决粘包问题pipeline.addLast(newLengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4)); // 计算当前待发送消息的长度,写入到前4个字节中pipeline.addLast(newLengthFieldPrepender(4));
Netty 收发消息如何保证消息的有序性?
- 本质上,Netty的发送和响应是异步的,那么这个异步的响应是怎么能关联到对应的请求上呢?
- 我们可以通过一个唯一的值来关联请求和响应。比如在请求对象中添加一个唯一ID,而响应的时候也将这个唯一ID加到响应对象中。channel在接收到响应时,就可以根据这个id找到对应的请求了。
- 这种处理本质上就是通过request和response中的唯一的xid来匹配的
开的连接过多怎么办?
- 设置合理的线程数
- 心跳优化
- 接收和发送缓冲区调优
- 合理使用内存池
- IO线程和业务线程分离