一、Netty定义
定义
Netty
是一款异步的事件驱动的网络应用框架、支持快速的开发可维护的高性能的面向协议的服务器和客户端。
优点
- 一个高性能、异步事件驱动的NIO框架,它提供了对
TCP、UDP
和文件传输的支持 - 使用更高效的
socket
底层,对epoll
空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO
的陷阱,简化了NIO
的处理方式。 - 采用多种
decoder/encoder
支持,对TCP粘包/分包
进行自动化处理 - 可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
- 可配置
IO线程数
、TCP参数
,TCP接收和发送缓冲区
使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
- 通过引用计数器及时申请释放不再引用的对象,降低了
GC频率
- 使用单线程串行化的方式,高效的
Reactor
线程模型大量使用了volitale
、使用了CAS和原子类
、线程安全类的使用、读写锁的使用
核心组件
1.Channel
Channel代表着的是Java NIO的一个基本构造,中文译为“通道”,它代表到一个实体(一个硬件设备、一个Socket或者一个文件)的开放连接,可以将其理解为两个socket之间的数据传输通道。相较于BIO来说,它支持双向传输,也就是可以读写数据同时进行。
2. Callback
一个回调其实是一个方法,用来在某些场景中完成后利用该方法来通知,Netty
是一个异步且事件驱动型网络框架,当数据到达时,读写数据都是阻塞的,所以可以调用一个回调方法来通知其它处理器以合适的时机与方法来处理数据,这样整个Netty
框架就是异步非阻塞的了,比如一个新的连接建立时,ChannelHandle
的channelActive()
回调方法就会被调用。
3. Future
Future
利用Call接口
来实现一种在操作完成时通知应用程序的方法。可以看做是一种占位符,它在操作完成时会返回一个预定的结果,根据这个结果就可以判断结果是否是符合开发者所预期的。比如JDK预置的Future接口
,Netty
中的ChannelFuture
在Future
的基础上增加了额外的方法,使得ChannelFuture
可以注册一个或者多个ChannelFuturelistener
实例:
ChannelFuture channelFuture=bootstrap.connect().sync();
channelFuture.addListener(new ConnectListener(this));
public class ConnectListener implements ChannelFutureListener {
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (!channelFuture.isSuccess()){
......
}else {
......
}
}
}
4. Event、ChannelHandle
在Netty
中,每次I/O操作
都被视作为一次事件(Event)
,其中分为出站事件与入站事件,入站事件是接收数据,而出站事件是发送数据,两者是相对的。
ChannelHandle
即事件处理器,每个事件会调用响应的事件处理器来处理,Netty
提供了很多的内置事件处理器来提高开发效率。
二、Netty组件与设计
EventLoopGroup、EventLoop、Channel
-
EventLoopGroup
为Netty
的事件循环组,里面包含了多个EventLoop
,服务端一般具有两个事件循环组,一个用于监听新的连接,处理三次握手(Boss
线程组),另外一个来处理已连接的客户端,处理读写数据(Work
线程组),当然也可以使用一个,那么这两者都在一个事件循环组里进行。 -
每个
EventLoop
代表着一个线程,用来处理其绑定的Channel
的所有事件,如果是采用NIO传输,那么每个EventLoop
可以被绑定多个Channel
,如果是BIO传输,那么每个EventLoop只能绑定一个Channel。 -
EventLoopGroup
负责为每个新创建的Channel
分配一个EventLoop
,使用顺序循环的方式进行分配,保证Channel
在EventLoop
中有一个均衡的分布。借于2、3点的设计,Netty
可以只需要在启动类更改传输的方式就可以完成I/O模型的转变。 -
每个
EventLoop
都有自己的任务队列,独立于任何其他的EventLoop
,Netty
调度线程池继承并扩充了ScheduledExcutorService
,在事件任务进入对应的任务队列的时候判断当前线程是否是当前EventLoop
绑定的那个线程,是就直接执行,不是则进入任务队列。
ChannelHandle、ChannelPipline、ChannelHandleContext
-
ChannelPipline
是ChannelHandle
实例链的抽象,可以看做是对应Channel
所有ChannelHandle处理器的容器,采用了责任链模式。 -
每个
Channel
都有一条ChannelPipline
,且不允许更改,每条ChannelPipline
也只对应一个Channel
,每个ChannelPipline
里面包括多个ChannelHandle
。 -
ChannelHandle
用来处理流经Channel
的数据,通过ChannelHandleContext
将每个ChannelHandle
处理的结果连接起来。 -
对于在
Channel
上直接写入,数据会从ChannelPipline
头部传到尾部,而在ChannelHandleContext
上写入数据,则只会从下一个ChannelHandle
开始传播。
ServerBootstrap、Bootstrap
ServerBootstrap
对应服务端的启动类,而Bootstrap
对应着客户端的启动类- 大部分的配置都在启动类上进行的,包括指明传输方式、配置
ChannelHandle
、连接方式等
- childHandler方法、handle方法
childHandler
方法为已被接受的子Channel
处理,代表着一个绑定到远程节点的套接字,也就是为每个客户端连接设置ChannelHandle
,用来处理每个客户端除了连接的操作handle
方法添加的ChannelHandle
由ServerChannel
处理,代表着为服务端创建新的子Channel并处理,其实就是处理新的客户端连接
ByteBuf
ByteBuf
是替代NIO
中数据容器的ByteBuffer类
而产生的,它维护了两个不同的索引,读索引与写索引,当从ByteBuf
中读数据时,会递增读索引,当向ByteBuf
中写入数据时,会递增写索引,当读索引与写索引相同位置时,代表着数据已全部读完。有三种使用模式:
- 堆缓冲区:将数据存贮在
JVM
的堆空间中,可以在没有池化的情况下提供迅速的分配与释放,适合有遗留数据需要处理的情况。 - 直接缓冲区:允许
JVM
通过本地调用来分配内存,主要是为了避免每次调用本地I/O
时将缓冲区的内容复制到一个中间缓冲区。缺点是分配与释放都开销大。 - 复合缓冲区:允许多个
ByteBuf
聚合成一个视图。通过一个ByteBuf
子类CompositeBuf
类来完成聚合,将多个ByteBuf
表示为单个合并的ByteBuf
。比如一个Bytebuf
代表着Http的头部,另外一个代表着主体,那么可以通过CompositeBuf
来组合。
相对于ByteBuffer
多了零拷贝、引用计数、复合缓冲区、无flip操作等优点。
三、Netty传输
对于Netty,可以这么说,它是在NIO、BIO上的再次封装,所以它内置支持BIO与NIO传输,为了性能的着想,在Netty中会选择NIO来作为它的传输实现,但是也不是绝对的,因为NIO适用于连接很多但是活跃的连接不多的情况下。
NIO与BIO
相比较于BIO,NIO多了如下的优点:
- 基于Reactor线程模型的实现,是事件驱动型。
- 是I/O多路复用技术的实现,可以利用I/O复用器实现一个线程监听多个Socket事件。
- 零拷贝,允许在堆外直接内存中分配数据,比起在堆内空间分配数据,还要复制到直接内存才能从Socket发送出去比,少了一次内存拷贝。
I/O多路复用
对于I/O多路复用技术的实现主要有select、poll、epoll等实现,只有NIO与epoll才支持零拷贝,而NIO在Linux中的实现就采用了epoll。
关于epoll,原理是其函数epoll_ctl()
用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将I/O
准备好的描述符加入到一个链表中管理,进程调用epoll_wait()
便可以得到事件完成的描述符。
epoll
只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符,这也就是epoll
零拷贝的实现。
四、预置的ChannelHandle
- MessageToByteEncoder:抽象编码类
- ByteToMessageDecoder:抽象解码类
- HttpRequestEncoder、HttpRequestDecoder:对Http消息编解码
- IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler:ChannelHandle最大空闲时长处理器,可以用来实现心跳包
- DellimiterBasedFrameDecoder:使用任何用户提供的分隔符来提取帧的通用解码器
- LineBasedFrameDecoder:提取行尾符(\n 或者 \r\n)分隔的帧的解码器
- FixedLengthFrameDecoder:提取在调用构造函数时指定的定长帧
- LengthFieldBasedFrameDecoder:根据编码进帧头部中的长度值提取帧
Netty中解决粘包/拆包问题?
TCP粘包/分包的原因:
- 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
- 进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
- 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
解决方法
- 消息定长:FixedLengthFrameDecoder类
- 包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder或自定义分隔符类:DelimiterBasedFrameDecoder
- 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。