Netty框架及主要特性浅析

        根据官方定义:Netty 是一款异步、事件驱动的网络应用程序框架,支持快速地开发可维护、高性能、面向协议的服务器和客户端。接下来我们由最基础概念逐渐深入到Netty功能特性。

IO模型

        1)BIO:同步阻塞IO

                每建立一个连接需要创建一个线程,用户线程通过系统调用read向Socket发起IO读操作(用户空间转到内核空间),内核等到数据包到达后将数据拷贝到用户空间的buffer,完成read操作。整个IO读取过程中用户线程是被阻塞的。 

        2)NIO:非阻塞同步IO(Linux非Java)

                在同步阻塞IO的基础上,将socket设置为non-blocking。当用户线程发出read操作,若内核的数据还没有准备好,并不会阻塞用户线程,而是立马返回一个error,并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。

        3)IO多路复用 (事件驱动IO)

               IO多路复用模型是建立在内核提供的多路复用函数select基础之上,只用select函数可以避免BIO中的多线程、NIO中的轮询等待问题。使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。

                在IO多路复用模型中,每个socket都被设置为non-blocking,但是整个流程都被select函数block,而不是被socket IO阻塞的。

        select、poll、epoll的机制及其区别

                以上均为IO多路复用的机制,通过该机制,可以监视多个文件描述符(fd),一旦某个描述符读/写就绪,能够通知程序进行相应的读写操作

        select:仅支持最大1024的fd数组(32位系统,64位支持2048)   ;每次调用select,都需要把fd集合从用户态拷贝到内核态;无状态,每次需要遍历。

        poll:实现机制类似于select,主要区别是实现基于链表,所以没有最大连接数限制(cat /proc/sys/fs/file-max)。

        epoll:基于红黑树实现,无连接数限制(cat /proc/sys/fs/file-max)。其内部通过三个函数来实现,epoll_create在内核中创建一个epfd句柄对象;epoll_ctl将需要监听的socket从epfd中添加/删除/修改(相比于select/poll每次调用都需要拷贝,此处仅拷贝一次);epoll_wait阻塞获取事件数据,轮询就绪链表,仅有活跃可用fd才会调用回调函数,将就绪的fd放入就绪链表中。

        4)AIO:异步IO

                异步非阻塞,一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

Reactor线程模型与Netty

        常见的reactor模式有三种:单reactor单线程/单进程(单线程模型)、单reactor多线程/多进程(多线程模型)​​​​​​​、多reactor多线程/多进程(主从 Reactor 多线程模型),各模型​及组件汇总如下。​​​​​​​​​​​​​

         1)单线程模型

                单线程模式是指所有的I/O操作都在一个NIO线程完成。如下图,Reactor对象通过select 监控连接事件,收到事件后通过dispatcher 进行转发;如果是连接事件,则由acceptor接收连接,并创建 handler处理后续事件;如果不是建立连接事件,则 Reactor会调用 Handler来响应。handler会完成read、业务处理、send的完整业务流程。

        尽管Reactor 模式使用的是异步非阻塞的I/O,所有的I/O操作都不会阻塞,理论上一个线程可以独立处理所有的I/O相关的操作。但是当面对高并发、大流量时,就会出现消息积压、处理超时等系统瓶颈,因此该模式比较适合任务短小、并发不高的情况。

        2)多线程模型

                 与单线程的最大区别在于对Handler功能拆分,使得能够充分利用多核 CPU 的能力,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。如下图,前面的流程同单线程模型,区别在于Handler只负责数据的接收和发送,Handler对象通过read读取数据后,会将数据发送到子线程里的Processor对象进行业务处理;子线程处理完成后,会将结果发送给Handler对象,然后Handler通过send方法将结果发送给客户端。

        多数情况下,多线程模型可以满足性能需求。但是在并发量极高的情况下,就会存在性能瓶颈,因为一个Reactor对象承担了所有事件的监听及响应。         

        3)主从 Reactor 多线程模型

                相比于多线程Reactor模型,主从多线程模型拥有了一个独立处理连接的线程池。其中MainReactor负责监听ServerSocketChannel、建立与SocketChannel的连接、将完成建立连接之后的Socket 交给SubReactor,SubReactor负责监听SocketChannel的 I/O事件,完成编解码、相应的业务处理(默认为CPU个数)。其余工程同前面模型

        4)Netty线程模型

                通过调整线程池的线程个数,是否共享线程池等方式,Netty 的 Reactor 线程模型可以在单线程、多线程和主从多线程间切换。

//单线程
NioEventLoopGroup loopGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(loopGroup, loopGroup)
......

//主从
NioEventLoopGroup mainGroup = new NioEventLoopGroup(1);
NioEventLoopGroup subGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(mainGroup, subGroup)
......

Netty零拷贝

        1)Netty的接受和发送ByteBuffer采用direct byte buffer,使用堆外直接内存进行Socket读写。绕过Java堆栈,直接只用内核的内存,减少了数据拷贝以及用户态切换内核态的次数。

        2)Netty提供了CompositeByteBuf类,通过该类可以将多个ByteBuf组合成一个逻辑上的Buffer,避免通过内存拷贝的方式将几个Buffer合并成一个。

        3)Netty在文件传输FileChannel类中提供了transferTo方法,使文件内容被DMA引擎复制到读取缓冲区中之后,数据被内核复制到与输出套接字关联的内核缓冲区中,该方法不仅减少了多个上下文切换,而且消除了CPU参与的重复数据副本。

Netty粘包/拆包

        操作系统在发送TCP数据的时候,底层有一个缓冲区,如一次请求发送的数据比较小,不足缓冲区大小,TCP会将多个请求数据合并为一个请求发送出去,这就形成了粘包的问题;如一次请求数据比较大,超出缓冲区大小,TCP就会对其拆分多次发送,这就是拆包问题。在Netty中如何解决这些问题。

        1)定长拆包

                FixedLengthFrameDecoder该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。编码器用户可以自己编写,只需要将不足长度的部分补齐即可。

        2)换行拆包

                LineBasedFrameDecoder该解码器以换行符作为分隔符,进行分割拆分。

        3)自定义拆包字符

                DelimiterBasedFrameDecoder该解码器通过自定义字符,进行分割拆分。

        4)长度拆包

                LengthFieldBasedFrameDecoder该解码器主要思想是在生成的数据包中添加一个长度字段(通过LengthFieldPrepender将当前发送消息的二进制字节长度,添加到缓冲区头部),用于记录当前数据包的长度,然后解码器参照指定的包长度对收到的数据进行解码,进而得到目标数据。

        5)自定义协议

                一般来说,前面几种方式已经可以处理大多数情景,但对于一些特殊的需求,可能需要自行设计开发,可以通过实现MessageToByteEncoderByteToMessageDecoder来处理,也可以基于继承LengthFieldBasedFrameDecoderLengthFieldPrepender来实现粘包和拆包的处理。

Netty epoll空轮询

        JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。

Netty的解决办法

        1)对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数。

        2)若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。

        3)重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值