Netty使用

1. 简介

Netty是一个NIO框架,能帮助开发者方便快捷的开发网络应用。先上一个官方的整体框架图感受下(虽然并没啥用):
01

与Java NIO相比(也比较官方):
Java NIO
<1> NIO的类库和API繁杂,使用麻烦;
<2> 需要具备其他的技能做铺垫,如Java多线程编程,NIO编程涉及到Reactor模式,必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
<3> 可靠性,如断连重连,网络闪断、半包读取、失败缓存、网络拥塞、异常码流等等,完成功能编写可能很容易,但可靠性补齐则难度较大;
<4> JDK NIO bug重重。
Netty:
<1> API使用简单,开发门槛低;
<2> 功能强大,预置了多种编码功能,支持多种主流协议;
<3> 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
<4> 性能高,通过与其他业界主流的NIO框架对比;
<5> 成熟、稳定、社区活跃;

2. 实现原理(Netty4)

让我们从一个简单的Netty Proxy Server开始。
02
03

上面这段简单的代码基本包含了Netty几个核心部分:
  • 高效的线程模型
  • IO数据流转(Pipeline)
  • 编解码(ByteBuf,状态信息保存)
  • 网络配置(backlog,timeout等)
2.1 高效线程模型

先来看看传统的四种IO模型,或者应该说是三种。
<1> 同步堵塞:最简单的一种IO模型,用户线程发起IO操作之后堵塞等待数据,待内核数据包准备好之后用户线程便可继续执行。
<2>同步非堵塞:在同步堵塞IO中将socket设置为NONBLOCK,这样用户线程在发起IO操作之后可以立即返回,不过得通过轮询来确定请求数据是否准备好。
<3> IO多路复用:由操作系统提供一种机制,通过它监视IO句柄的就绪状态,之后通知程序进行处理。又可细分为Reactor和Proactor两种模式。
Reactor模式基于同步I/O,典型的实现有select,poll,epoll(Linux环境中jdk的SelectorProvider会选择epoll,其它操作系统有其它对应的实现,如Mac用KQueue等等)。Reactor模式中,epoll等系统调用不负责IO操作,只负责告诉你当前IO句柄的就绪状态(可读、可写),并且将数据填充到读写缓冲区,读写控制由用户负责。
Proactor模式则基于异步I/O,典型实现有微软的IOCP。在Proactor中,由操作系统直接负责I/O读写操作,完成以后通过回调通知用户,这样带来的缺点是我们无法控制I/O通道,假如发生堵塞等都无能为力。
最传统的BIO线程模型
04
这种线程模型对于每个连接请求需要一个单独的线程来处理,因此完全无法应对高并发的场景。如果无限制的启动线程来处理并发请求,则系统资源将很快被耗尽,并且大量线程导致CPU都消耗在无用的线程switch中。
为了限制线程的增长,演变出了改进的BIO线程模型,如采用BIO的tomcat线程模型:
05

采用线程池的方式很好的限制了资源的消耗问题,但解决不了根本问题,每个client需要分配一个thread,限制了线程池的大小也就限制了Server的处理能力,Server无法承载更多的请求。
NIO的提出则很好的解决了这个问题。NIO采用的是Reactor模式,根据处理I/O操作的NIO线程的数量来区分。
  • Reactor单线程模型、
  • 多线程模型
  • 主从Reactor线程模型。

单线程模型
06

因为Reactor模式采用的是异步非阻塞I/O,所有的I/O操作都不会堵塞,因此可以使用单线程处理所有相关的I/O操作,如accept请求、数据读取,请求dispatch等。这种模型只适合小应用,在高并发应用中则不合适。一个NIO线程无法同时处理上千的链路,如果NIO线程过载,导致大量的消息积压和处理超时,则会成为系统的瓶颈。
多线程模型
07

多线程模型区别于单线程模型的地方,在于增加一个Thread Pool用于处理网络IO操作,负责读取、编解码、发送。而Acceptor线程只负责监听服务端,处理Client链路。多数场景下,Reactor多线程模型可以满足性能需求。个别场景,如需承载百万并发、增加SSL验证等(认证本身非常损耗系能),在这类场景中,采用单个线程处理client链路无法满足要求。
主从Reactor多线程模型,也即Netty典型采用的模型
Netty的线程模型比较比较灵活,根据不同的配置,也即组合boss与work的NioEventLoopGroup即可采用上述三种模型,如:
bossGroup = new NioEventLoopGroup(Configuration.getIntProperty("proxy.nio.reactor.thread", 1), new NamedThreadFactory
        ("proxy-reactor"));
workGroup = new NioEventLoopGroup(Configuration.getIntProperty("proxy.nio.work.thread", Runtime.getRuntime().availableProcessors()),
        new NamedThreadFactory("proxy-work"));

<1> 将bossGroup==workGroup,不开启单独的线程池处理IO操作,则是单线程模型;
<2> 将bossGroup==workGroup,开启单独的线程池处理IO操作,在ChannelHandler中将请求dispatch到线程池;
<3> 如示例代码,bossGroup!=workGroup,则是主从Reactor模型,这也是Netty推荐采用的线程模型,如:
08

boss与work线程的职责为:
boss:请求accept
work:链路读写,验证验证,定时任务(如处理连接超时,由于nio采用非堵塞,不同通过直接设置socket timeout的方式)
这便是Netty采用的主从Reactor模型,Server端不再使用一个单独的NIO线程处理所有链路,而是采用独立的NIO线程池。MainReactor,也即boss线程负责accept连接,之后注册到SubReactor,也即work nio线程池。
另外,Netty将channel的读写均放在一个NIO线程中完成,这样可以避免线程切换带来的开销,同时也可避免多线程的同步问题。当然这要建立在你的业务处理逻辑比较简单,不涉及其它需要等待的网络IO,比如数据库读写等操作的前提下,否则你应该启独立的应用线程池去处理耗时的业务逻辑,以避免堵塞IO线程。(可以通过测试对比使用业务线程池与不使用业务线程池的性能情况)
如果深入到类层次,则Channel的处理过程如:
09

<1> 在Server启动以后,boss线程会循环检测就绪SelectorKey来accept client请求;
<2> client发起连接请求,boss线程accept请求后,通过Unsafe触发read操作;
<3> 之后read请求在pipeline中流转,由处于末端的ServerBootstrapAcceptor将建立链路的NioSocketChannel注册到work线程;
<4> 之后便由work线程来处理channel上的链路操作;


如果未说明,本Blog中文章皆为原创文章,请尊重他人劳动,转载请注明: 转载自jmatrix

本文链接地址: Netty使用

(注:一般引用了,我都会添加引用,如果有侵权的请联系我)



This entry was posted in 日常文章 and tagged . Bookmark the permalink. Follow any comments here with the RSS feed for this post. Trackbacks are closed, but you can post a comment.