Netty浅析
netty是jboss出品的高效的java nio开发框架,关于其使用,可参考我的另一篇文章 netty使用初步。本文将主要分析netty实现方面的东西,由于精力有限,本人并没有对其源码做了极细致的研 究。如果下面的内容有错误或不严谨的地方,也请指正和谅解。对于netty使用者来说,netty提供了几个典型的example,并有详尽的api doc和guide doc,本文的一些内容及图示也来自于netty的文档,特此致谢。
1、总体结构
先放上一张漂亮的netty总体结构图,下面的内容也主要围绕该图上的一些核心功能做分析,但对如container integration及security support等高级可选功能,本文不予分析。
2、网络模型
netty是典型的reactor模型结构,关于reactor的详尽阐释,可参考posa2,这里不做概念性的解释。而应用java nio构建reactor模式,doug lea(就是那位让人无限景仰的大爷)在“scalable io in java”中给了很好的阐述。这里截取其ppt中经典的图例说明 reactor模式的典型实现:
1、这是最简单的单reactor单线程模型。reactor线程是个多面手,负责多路分离套接字,accept新连接,并分派请求到处理器链中。该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。
2、相比上一种模型,该模型在处理器链部分采用了多线程(线程池),也是后端程序常用的模型。
3、 第三种模型比起第二种模型,是将reactor分成两部分,mainreactor负责监听server socket,accept新连接,并将建立的socket分派给subreactor。subreactor负责多路分离已连接的socket,读写网 络数据,对业务处理功能,其扔给worker线程池完成。通常,subreactor个数上可与cpu个数等同。
说完reacotr模型的三种形式,那么netty是哪种呢?其实,我还有一种reactor模型的变种没说,那就是去掉线程池的第三种形式的变种,这也 是netty nio的默认模式。在实现上,netty中的boss类充当mainreactor,nioworker类充当subreactor(默认 nioworker的个数是runtime.getruntime().availableprocessors())。在处理新来的请求 时,nioworker读完已收到的数据到channelbuffer中,之后触发channelpipeline中的channelhandler流。
netty是事件驱动的,可以通过channelhandler链来控制执行流向。因为channelhandler链的执行过程是在 subreactor中同步的,所以如果业务处理handler耗时长,将严重影响可支持的并发数。这种模型适合于像memcache这样的应用场景,但 对需要操作数据库或者和其他模块阻塞交互的系统就不是很合适。netty的可扩展性非常好,而像channelhandler线程池化的需要,可以通过在 channelpipeline中添加netty内置的channelhandler实现类–executionhandler实现,对使用者来说只是 添加一行代码而已。对于executionhandler需要的线程池模型,netty提供了两种可 选:1) memoryawarethreadpool[executor]可控制executor中待处理任务的上限(超过上限时,后续进来的任务将被阻 塞),并可控制单个channel待处理任务的上限;2) orderedmemoryawarethreadpoolexecutor 是 memoryawarethreadpoolexecutor 的子类,它还可以保证同一channel中处理的事件流的顺序性,这主要是控制事件在异步处 理模式下可能出现的错误的事件顺序,但它并不保证同一channel中的事件都在一个线程中执行(通常也没必要)。一般来 说,orderedmemoryawarethreadpoolexecutor 是个很不错的选择,当然,如果有需要,也可以diy一个。
3、 buffer
org.jboss.netty.buffer包的接口及类的结构图如下:
该包核心的接口是channelbuffer和channelbufferfactory,下面予以简要的介绍。
netty使用channelbuffer来存储并操作读写的网络数据。channelbuffer除了提供和bytebuffer类似的方法,还提供了 一些实用方法,具体可参考其api文档。channelbuffer的实现类有多个,这里列举其中主要的几个:
1)heapchannelbuffer:这是netty读网络数据时默认使用的channelbuffer,这里的heap就是java堆的意思,因为 读socketchannel的数据是要经过bytebuffer的,而bytebuffer实际操作的就是个byte数组,所以 channelbuffer的内部就包含了一个byte数组,使得bytebuffer和channelbuffer之间的转换是零拷贝方式。根据网络字 节续的不同,heapchannelbuffer又分为bigendianheapchannelbuffer和 littleendianheapchannelbuffer,默认使用的是bigendianheapchannelbuffer。netty在读网络 数据时使用的就是heapchannelbuffer,heapchannelbuffer是个大小固定的buffer,为了不至于分配的buffer的 大小不太合适,netty在分配buffer时会参考上次请求需要的大小。
2)dynamicchannelbuffer:相比于heapchannelbuffer,dynamicchannelbuffer可动态自适应大 小。对于在decodehandler中的写数据操作,在数据大小未知的情况下,通常使用dynamicchannelbuffer。
3)bytebufferbackedchannelbuffer:这是directbuffer,直接封装了bytebuffer的 directbuffer。
对于读写网络数据的buffer,分配策略有两种:1)通常出于简单考虑,直接分配固定大小的buffer,缺点是,对一些应用来说这个大小限制有时是不 合理的,并且如果buffer的上限很大也会有内存上的浪费。2)针对固定大小的buffer缺点,就引入动态buffer,动态buffer之于固定 buffer相当于list之于array。
buffer的寄存策略常见的也有两种(其实是我知道的就限于此):1)在多线程(线程池) 模型下,每个线程维护自己的读写buffer,每次处理新的请求前清空buffer(或者在处理结束后清空),该请求的读写操作都需要在该线程中完成。 2)buffer和socket绑定而与线程无关。两种方法的目的都是为了重用buffer。
netty对buffer的处理策略是:读 请求数据时,netty首先读数据到新创建的固定大小的heapchannelbuffer中,当heapchannelbuffer满或者没有数据可读 时,调用handler来处理数据,这通常首先触发的是用户自定义的decodehandler,因为handler对象是和channelsocket 绑定的,所以在decodehandler里可以设置channelbuffer成员,当解析数据包发现数据不完整时就终止此次处理流程,等下次读事件触 发时接着上次的数据继续解析。就这个过程来说,和channelsocket绑定的decodehandler中的buffer通常是动态的可重用 buffer(dynamicchannelbuffer),而在nioworker中读channelsocket中的数据的buffer是临时分配的 固定大小的heapchannelbuffer,这个转换过程是有个字节拷贝行为的。
对channelbuffer的创建,netty内部使用的是channelbufferfactory接口,具体的实现有 directchannelbufferfactory和heapchannelbufferfactory。对于开发者创建 channelbuffer,可使用实用类channelbuffers中的工厂方法。
4、channel
和channel相关的接口及类结构图如下:
从该结构图也可以看到,channel主要提供的功能如下:
1)当前channel的状态信息,比如是打开还是关闭等。
2)通过channelconfig可以得到的channel配置信息。
3)channel所支持的如read、write、bind、connect等io操作。
4)得到处理该channel的channelpipeline,既而可以调用其做和请求相关的io操作。
在channel实现方面,以通常使用的nio socket来说,netty中的nioserversocketchannel和niosocketchannel分别封装了java.nio中包含的 serversocketchannel和socketchannel的功能。
5、channelevent
如前所述,netty是事件驱动的,其通过channelevent来确定事件流的方向。一个channelevent是依附于channel的 channelpipeline来处理,并由channelpipeline调用channelhandler来做具体的处理。下面是和 channelevent相关的接口及类图:
对于使用者来说,在channelhandler实现类中会使用继承于channelevent的messageevent,调用其 getmessage()方法来获得读到的channelbuffer或被转化的对象。
6、channelpipeline
netty 在事件处理上,是通过channelpipeline来控制事件流,通过调用注册其上的一系列channelhandler来处理事件,这也是典型的拦截 器模式。下面是和channelpipeline相关的接口及类图:
事件流有两种,upstream事件和downstream事件。在channelpipeline中,其可被注册的channelhandler既可以 是 channelupstreamhandler 也可以是channeldownstreamhandler ,但事件在channelpipeline传递过程中只会调用匹配流的channelhandler。在事件流的过滤器链 中,channelupstreamhandler或channeldownstreamhandler既可以终止流程,也可以通过调用 channelhandlercontext.sendupstream(channelevent)或 channelhandlercontext.senddownstream(channelevent)将事件传递下去。下面是事件流处理的图示:
从上图可见,upstream event是被upstream handler们自底向上逐个处理,downstream event是被downstream handler们自顶向下逐个处理,这里的上下关系就是向channelpipeline里添加handler的先后顺序关系。简单的理 解,upstream event是处理来自外部的请求的过程,而downstream event是处理向外发送请求的过程。
服务端处 理请求的过程通常就是解码请求、业务逻辑处理、编码响应,构建的channelpipeline也就类似下面的代码片断:
channelpipeline pipeline = channels.pipeline(); pipeline.addlast("decoder", new myprotocoldecoder()); pipeline.addlast("encoder", new myprotocolencoder()); pipeline.addlast("handler", new mybusinesslogichandler());
其中,myprotocoldecoder是channelupstreamhandler类型,myprotocolencoder是 channeldownstreamhandler类型,mybusinesslogichandler既可以是 channelupstreamhandler类型,也可兼channeldownstreamhandler类型,视其是服务端程序还是客户端程序以及 应用需要而定。
补充一点,netty对抽象和实现做了很好的解耦。像org.jboss.netty.channel.socket包, 定义了一些和socket处理相关的接口,而org.jboss.netty.channel.socket.nio、 org.jboss.netty.channel.socket.oio等包,则是和协议相关的实现。
7、codec framework
对于请求协议的编码解码,当然是可以按照协议格式自己操作channelbuffer中的字节数据。另一方面,netty也做了几个很实用的codec helper,这里给出简单的介绍。
1)framedecoder:framedecoder内部维护了一个 dynamicchannelbuffer成员来存储接收到的数据,它就像个抽象模板,把整个解码过程模板写好了,其子类只需实现decode函数即可。 framedecoder的直接实现类有两个:(1)delimiterbasedframedecoder是基于分割符 (比如/r/n)的解码器,可在构造函数中指定分割符。(2)lengthfieldbasedframedecoder是基于长度字段的解码器。如果协 议 格式类似“内容长度”+内容、“固定头”+“内容长度”+动态内容这样的格式,就可以使用该解码器,其使用方法在api doc上详尽的解释。
2)replayingdecoder: 它是framedecoder的一个变种子类,它相对于framedecoder是非阻塞解码。也就是说,使用 framedecoder时需要考虑到读到的数据有可能是不完整的,而使用replayingdecoder就可以假定读到了全部的数据。
3)objectencoder 和objectdecoder:编码解码序列化的java对象。
4)httprequestencoder和 httprequestdecoder:http协议处理。
下面来看使用framedecoder和replayingdecoder的两个例子:
public class integerheaderframedecoder extends framedecoder { protected object decode(channelhandlercontext ctx, channel channel, channelbuffer buf) throws exception { if (buf.readablebytes() < 4) { return null; } buf.markreaderindex(); int length = buf.readint(); if (buf.readablebytes() < length) { buf.resetreaderindex(); return null; } return buf.readbytes(length); } }
而使用replayingdecoder的解码片断类似下面的,相对来说会简化很多。
public class integerheaderframedecoder2 extends replayingdecoder { protected object decode(channelhandlercontext ctx, channel channel, channelbuffer buf, voidenum state) throws exception { return buf.readbytes(buf.readint()); } }
就实现来说,当在replayingdecoder子类的decode函数中调用channelbuffer读数据时,如果读失败,那么 replayingdecoder就会catch住其抛出的error,然后replayingdecoder接手控制权,等待下一次读到后续的数据后继 续decode。
8、小结
尽管该文行至此处将止,但该文显然没有将netty实现原理深入浅出的说全说透。当我打算写这篇文章时,也是一边看netty的代码,一边总结些可写的东 西,但前后断断续续,到最后都没了多少兴致。我还是爱做一些源码分析的事情,但精力终究有限,并且倘不能把源码分析的结果有条理的托出来,不能产生有意义 的心得,这分析也没什么价值和趣味。而就分析netty代码的感受来说,netty的代码很漂亮,结构上层次上很清晰,不过这种面向接口及抽象层次对代码 跟踪很是个问题,因为跟踪代码经常遇到接口和抽象类,只能借助于工厂类和api doc,反复对照接口和实现类的对应关系。
对java架构技术感兴趣的同学,欢迎加qq群619881427,一起学习,相互讨论。
群内已经有小伙伴将知识体系整理好(源码,笔记,ppt,学习视频),欢迎加群免费领取。
分享给喜欢的java的,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们。
不是的java的程序员也没关系,帮忙转发给更多朋友!谢谢。
分享一个小技巧点击阅读原文也。。可以轻松获取学习资料哦!
关注微信公众号:java架构师学习,观看更多的java技术干货分享