Netty使用

Posted MatrixLog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty使用相关的知识,希望对你有一定的参考价值。

1. 简介


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


与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开始。(示例代码在同性交友网站,jjmatrix/CommonProxy)


Netty使用


Netty使用

上面这段简单的代码基本包含了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线程模型

Netty使用


这种线程模型对于每个连接请求需要一个单独的线程来处理,因此完全无法应对高并发的场景。如果无限制的启动线程来处理并发请求,则系统资源将很快被耗尽,并且大量线程导致CPU都消耗在无用的线程switch中。

为了限制线程的增长,演变出了改进的BIO线程模型,如采用BIO的tomcat线程模型:

Netty使用


采用线程池的方式很好的限制了资源的消耗问题,但解决不了根本问题,每个client需要分配一个thread,限制了线程池的大小也就限制了Server的处理能力,Server无法承载更多的请求。

NIO的提出则很好的解决了这个问题。NIO采用的是Reactor模式,根据处理I/O操作的NIO线程的数量来区分。

  • Reactor单线程模型、

  • 多线程模型

  • 主从Reactor线程模型。

单线程模型

Netty使用

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


多线程模型

Netty使用

多线程模型区别于单线程模型的地方,在于增加一个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推荐采用的线程模型,如:

Netty使用

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的处理过程如:

Netty使用


<1> 在Server启动以后,boss线程会循环检测就绪SelectorKey来accept client请求;

<2> client发起连接请求,boss线程accept请求后,通过Unsafe触发read操作;

<3> 之后read请求在pipeline中流转,由处于末端的ServerBootstrapAcceptor将建立链路的NiosocketChannel注册到work线程;

<4> 之后便由work线程来处理channel上的链路操作;


2.2 IO数据流转

前面分析Netty的线程模型时提过,Netty的IO读写操作均在一个work线程中完成。work线程会循环检测channel就绪状态,当发生IO事件,触发相应的事件,经ChannelPipeline完成整个ChannelHandler链条的处理。ChannelPipeline是ChannelHandler的容器,负责每个Channel关联的ChannelHandler的管理、事件处理与调度。

Netty使用


ChannelHandler由ChannelPipeline维护,由我们的创建Netty应用的时候指定,并可以动态改变。如前面Netty Proxy Server的例子:

if (Configuration.getBooleanProperty("proxy.debug"false)) {

    ch.pipeline().addLast("log"new LoggingHandler(LogLevel.INFO));

}

ch.pipeline().addLast("idle"new IdleStateHandler(Configuration.getIntProperty("proxy.channel.timeout"30),

        Configuration.getIntProperty("proxy.channel.timeout"30), 0));

addExtendChannelHandler(ch);

ch.pipeline().addLast("handler"new ProxyHandler(dispatcherExtendLoader.acquireProvider(preferred, false)));

ChannelPipeline会根据添加的顺序将这些做不同事情(如编码、解码、超时处理等等)的ChannelHandler串成链条,并加入Head与Tail两个HandlerContext。当然,由于IO数据流是有Inbound与OutBound两个方向的,ChannelPipeline需要在read/write时区分对待。

举个栗子,用Netty做Redis Proxy服务,整个处理流程如:

<1> client发起get请求;

<2> ChannelPipeline通过Head的fireChannelRead触发Inbound流程;

<3> 负责解码的ChannelHandler解析redis协议;

<4> 根据redis请求,通过writeAndFlush返回结果;

<3> ChannelPipeline通过Tail触发OutBound流程;


2.3 状态信息保存

ChannelHandler有两种配置方式,一种是每次new新的实例,一种是采用共享的方式。我们知道ChannelHandler链条是关联到Channel的,因此除非必要,均采用new新实例的方式,如:

ch.pipeline().addLast("handler"new ProxyHandler(dispatcherExtendLoader.acquireProvider(preferred, false)));

采用这样的方式,每个Channel(也即每个链接)均有一个独立的ChannelHandler,这样便可以在ChannelHandler中采用成员变量来保存状态信息,而不会有线程同步问题。

当然,如果你理由充足(多new一个ChannelHandler要命),也可以采用共享ChannelHandler的方式。这个时候,如果要保存状态信息需要采用ChannelHandlerContext,通过AttributeKey。ChannelHandlerContext是Netty提供的上下文对象,通过它可以动态修改pipeline,在inbound,outbound,甚至其它线程中传递,用于触发事件等。在共享模式中,ChannelHandlerContext之所以可以保存状态信息,是因为尽管ChannelHandler可以在Channel中共享,但是ChannelHandlerContext却不是,也即一个ChannelHandler可以有多个ChannelHandlerContext。

综上所述,结合2.2的数据流转图,可梳理这几个核心实例的对应关系如:

Netty使用


2.4 ByteBuf

ByteBuf是对Netty对Java NIO中ByteBuffer的扩展,相比较ByteBuffer。

  • 它提供了更友好的API。

  • 内存池技术

  • 引用计数

Java NIO的ByteBuffer提供四个position指针,如:

Netty使用


只有一个position指针标记读写的位置,在进行读写操作时要通过对应的接口调整好各个指针的位置,比如在写入完成以后如果要读取数据,需要调用flip调整position,mark,limit的指向。


而ByteBuf提供了独立的读写指针readerIndex与writerIndex,操作上更方便。

ByteBuf类型:


<1> 从内存在哪里分配看,分HeapByteBuf、DirectByteBuf以及组合两者的CompositeByteBuf;

严格来说应该是两种,Heap与Direct内存,CompositeByteBuf是前面两种类型的组合。在网络IO中,Direct内存有其天然的优势,Direct是直接操作内存区域,相比heap可以减少内存的拷贝动作。因此,默认情况下,只要系统支持,Netty采用的是Direct方式。

<2> 从是否使用内存池看,分Pooled与Unpooled;

我们可以使用不同的组合(配置参数见附录1)来测试ByteBuf的性能,如:

(指标包括gc与响应)

pooled

unpooled

direct



heap



从理论上看,direct相比heap而言无论是性能还是gc应该都有提升(待验证,特别是GC方面,需要调整JVM参数来排除干扰)。而如果均采用direct的情况下,采用pooled的方式,则性能更好,因为direct方式下,频繁创建销毁direct内存开销较大(实验确实如此)。

引用计数:

从Netty4开始,Netty开始通过引用计数来管理ByteBuf对象的生命周期,当引用计数为0的时候则将对象返回给对象池或者直接释放。

谁负责释放对象?

第一原则是谁最后使用谁负责释放(通过ByteBuf的release或借助ReferenceCountUtil)。如果是在Inbound,则由ChannelHandler负责释放ByteBuf对象。在Outbound中则不同,ByteBuf最终应该由Netty负责释放,除非是你创建的临时ByteBuf对象。

另外,Netty也提供了内存泄漏的检测功能,通过配置参数-Dio.netty.leakDetectionLevel,可选配置参数有:

  • disabled:禁用检测

  • simple(默认):提示是否存在内存泄漏

  • advanced:提示那里存在泄漏

  • paranoid:提示那里存在泄漏,不过比前面的检测标准更严格,只要有一个buffer泄漏都不行。


2.5 编解码

当了解了IO数据流转,状态信息保存及ByteBuf的应用以后,实现协议的编码与解码就简单了。只需要记住一点,由于TCP是分段发送的,你不一定能一次性读到你想要的数据,因此在读数据之前要先判断ByteBuf中的数据是否准备好(长度符合?已有分隔符?)。

当然你可以借助Netty提供的一些现有的编解码工具类快速完成这些工作,如:

DelimiterBasedFrameDecoder(直接可用)

如果你的协议是根据某个delimiter分隔。

FixedLengthFrameDecoder(直接可用)

如果协议是长度固定。

ReplayingDecoder(辅助,用需要注意一些问题)

提供简单的支持,让你可以放心大胆的读ByteBuf,而无需担心数据是否准备好。


2.6 网络配置

这个涉及的内容较多,需要通过TCP/IP协议三件套,Unix网络编程两件套来深入了解。


3. 参考资料


Netty Doc:http://netty.io/wiki/index.html,官方资料,想要的都有。

书籍:Netty in Action,TCP/IP协议三件套,Unix网络编程两件套。


以上是关于Netty使用的主要内容,如果未能解决你的问题,请参考以下文章

Netty基本使用-Netty组件

Netty基本使用-Netty组件

Netty基本使用-Netty组件

Netty网络编程实战4,使用Netty实现心跳检测机制

netty系列之:在netty中使用protobuf协议

netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》