前言
本文是作者在做rpc框架项目时,需要使用Netty网络框架进行实现,因此学习了一些Netty相关的知识,在次仅仅是为了记录所学过的知识,可能并不全面,仅作为日后复习使用
Netty
上文我们讲了几种IO模式,但都是使用的java的原生io
使用原生io有以下问题:
- NIO的类库和API繁杂,使用麻烦
- 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他还有业界主流的RPC框架(dubbo等),也使用Netty来构建高性能的异步通信能力。
以下是Netty的主要优点:
- API使用简单,开发门槛低
- 功能强大,预置了多种编解码功能,支持多种主流协议
- 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
- 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优
在讲Netty之前,必须先要了解一些Reactor设计模式的相关知识
Reactor模式
Netty的NIO和上一章所说的NIO是有区别的,Netty的采用了Reactor模式
常见的Reactor线程模型有三种:
- Reactor单线程模型
- Reactor多线程模型
- 主从Reactor多线程模型(主要)
Reactor单线程模型
Reactor单线程模型,指的是所有的I/O操作都在同一个NIO线程上面完成,NIO线程的职责如下:
- 作为NIO服务端,接收客户端的TCP连接
- 作为NIO客户端,向服务端发起TCP连接
- 读取通信对端的请求或者应答消息
- 向通信对端发送消息请求或者应答消息
Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到处理器链中。
单线程模型不能充分利用多核资源,所以实际使用的不多
对于一些小容量应用场景,可以使用单线程模型,但是对于高负载、大并发的应用却不合适,主要原因如下:
- 一个NIO线程同时处理成百上千的链路,性能上无法支撑。即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送
- NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往进行重发,这更加重了NIO线程的负载,最终导致大量消息积压和处理超时,NIO线程会成为系统的性能瓶颈
- 可靠性问题。一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通讯模块不可用,不能接收和处理外部信息,造成节点故障
Reactor多线程模型
多线程模型和单线程模型最大的区别是 多线程有一组NIO线程来处理I/O操作,而且
- 有一个专门的NIO线程--acceptor线程用于监听服务端,接收客户端的TCP连接请求
- 网络I/O操作--读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送
- 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题
主从Reactor多线程模型
这个和多线程模型最大的区别是:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。
这样处理大量连接时,不再是一个线程负责连接的建立以及分配业务处理等操作,而是一个线程池(主Reactor)来处理。然后有读写请求时,会将该连接的读写请求分配到另一个线程池(从Reactor)的某个线程来处理。
Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。
Reactor部分分为mainReactor和subReactor两部分。mainReactor负责监听处理新的连接,然后将后续的事件处理交给subReactor,subReactor对事件处理的方式,也由阻塞模式变成了多线程处理,引入了任务队列的模式。