Netty探秘——Netty概述与总结

一、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框架就是异步非阻塞的了,比如一个新的连接建立时,ChannelHandlechannelActive()回调方法就会被调用。

3. Future

Future利用Call接口来实现一种在操作完成时通知应用程序的方法。可以看做是一种占位符,它在操作完成时会返回一个预定的结果,根据这个结果就可以判断结果是否是符合开发者所预期的。比如JDK预置的Future接口Netty中的ChannelFutureFuture的基础上增加了额外的方法,使得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

  1. EventLoopGroupNetty的事件循环组,里面包含了多个EventLoop,服务端一般具有两个事件循环组,一个用于监听新的连接,处理三次握手(Boss线程组),另外一个来处理已连接的客户端,处理读写数据(Work线程组),当然也可以使用一个,那么这两者都在一个事件循环组里进行。

  2. 每个EventLoop代表着一个线程,用来处理其绑定的Channel的所有事件,如果是采用NIO传输,那么每个EventLoop可以被绑定多个Channel,如果是BIO传输,那么每个EventLoop只能绑定一个Channel。

  3. EventLoopGroup负责为每个新创建的Channel分配一个EventLoop,使用顺序循环的方式进行分配,保证ChannelEventLoop中有一个均衡的分布。借于2、3点的设计,Netty可以只需要在启动类更改传输的方式就可以完成I/O模型的转变。

  4. 每个EventLoop都有自己的任务队列,独立于任何其他的EventLoopNetty调度线程池继承并扩充了ScheduledExcutorService,在事件任务进入对应的任务队列的时候判断当前线程是否是当前EventLoop绑定的那个线程,是就直接执行,不是则进入任务队列。

ChannelHandle、ChannelPipline、ChannelHandleContext

  1. ChannelPiplineChannelHandle实例链的抽象,可以看做是对应Channel所有ChannelHandle处理器的容器,采用了责任链模式。

  2. 每个Channel都有一条ChannelPipline,且不允许更改,每条ChannelPipline也只对应一个Channel,每个ChannelPipline里面包括多个ChannelHandle

  3. ChannelHandle用来处理流经Channel的数据,通过ChannelHandleContext将每个ChannelHandle处理的结果连接起来。

  4. 对于在Channel上直接写入,数据会从ChannelPipline头部传到尾部,而在ChannelHandleContext上写入数据,则只会从下一个ChannelHandle开始传播。

ServerBootstrap、Bootstrap

  1. ServerBootstrap对应服务端的启动类,而Bootstrap对应着客户端的启动类
  2. 大部分的配置都在启动类上进行的,包括指明传输方式、配置ChannelHandle、连接方式等
  • childHandler方法、handle方法
  1. childHandler方法为已被接受的子Channel处理,代表着一个绑定到远程节点的套接字,也就是为每个客户端连接设置ChannelHandle,用来处理每个客户端除了连接的操作
  2. handle方法添加的ChannelHandleServerChannel处理,代表着为服务端创建新的子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类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。

你可能感兴趣的:(Netty)