写在前面:
本来想自己写个Netty入门的,但是发现官网的入门例子写的太特么好了,连例子都直接提供下载,就失去动力鸟。然后对于Netty的特性和框架,无意中看到一位仁兄在11年的总结,虽然部分现在已经不适用了,但总体来讲写的非常地道,可以作为对Netty框架了解的一个非常号的参考。直接转过来。
原文地址:http://blog.sina.com.cn/s/blog_3fe961ae01011oob.html
1. 简介
Java1.4提供了NIO使开发者可以使用Java编写高性能的服务端程序,但使用原生的NIO API就像Linux C中网络编程一样,还是需要做IO处理、协议处理等低层次工作。所以,就像C服务端程序大量使用libevent作为网络应用框架一样,Java社区也不断涌现出基于NIO的网络应用框架。在这其中,Jboss出品的Netty就是个中翘楚。Netty是个异步的事件驱动网络应用框架,具有高性能、高扩展性等特性。Netty提供了统一的底层协议接口,使得开发者从底层的网络协议(比如TCP/IP、UDP)中解脱出来。就使用来说,开发者只要参考 Netty提供的若干例子和它的指南文档,就可以放手开发基于Netty的服务端程序了。
在Java社区,最知名的开源Java NIO框架要属Mina和Netty,而且两者渊源颇多,对两者的比较自然不少。实际上,Netty的作者原来就是Mina作者之一,所以可以想到,Netty和Mina在设计理念上会有很多共同点。我对Mina没什么研究,但其作者介绍,Netty的设计对开发者有更友好的扩展性,并且性能方面要优于Mina,而Netty完善的文档也很吸引人。所以,如果你在寻找Java NIO框架,Netty是个很不错的选择。本文的内容就是围绕一个demo介绍使用Netty的点点滴滴。
(引用自: http://www.chinasb.org/archives/2012/11/4966.shtml)
2. 总体框架
2.1. 丰富的缓冲数据结构
Netty使用自建的buffer API,而不是使用NIO的ByteBuffer来代表一个连续的字节序列。与ByteBuffer相比这种方式拥有明显的优势。Netty使用新的buffer类型ChannelBuffer,ChannelBuffer被设计为一个可从底层解决ByteBuffer问题,并可满足日常网络应用开发需要的缓冲类型。这些很酷的特性包括:
1. 如果需要,允许自定义缓冲类型。
2. 通过内置复合缓冲类型实现透明的零拷贝。
3. 提供开箱即用的动态缓冲类型,其容量可以根据需要扩充,类似于StringBuffer。
4. 不再需要调用的flip()方法。
5. 一般来说比ByteBuffer更快。
2.2. 统一的异步 I/O API
这种模式上的不匹配使得在更换一个网络应用的传输协议时变得繁杂和困难。由于(Java I/O API)缺乏协议间的移植性,当你试图在不修改网络传输层的前提下增加多种协议的支持,这时便会产生问题。并且理论上讲,多种应用层协议可运行在多种传输 层协议之上例如TCP/IP,UDP/IP,SCTP和串口通信。
让这种情况变得更糟的是,Java新的I/O(NIO)API与原有的阻塞式的I/O(OIO)API并不兼容,NIO.2(AIO)也是如此。由于所有的API无论是在其设计上还是性能上的特性都与彼此不同,在进入开发阶段,你常常会被迫的选择一种你需要的API。
例如,在用户数较小的时候你可能会选择使用传统的OIO(Old I/O) API,毕竟与NIO相比使用OIO将更加容易一些。然而,当你的业务呈指数增长并且服务器需要同时处理成千上万的客户连接时你便会遇到问题。这种情况下 你可能会尝试使用NIO,但是复杂的NIO Selector编程接口又会耗费你大量时间并最终会阻碍你的快速开发。
Netty有一个叫做Channel的统一的异步I/O编程接口,这个编程接口抽象了所有点对点的通信操作。也就是说,如果你的应用是基于Netty的某一种传输实现,那么同样的,你的应用也可以运行在Netty的另一种传输实现上。Netty提供了几种拥有相同编程接口的基本传输实现:
1 NIO-based TCP/IP transport (See org.jboss.netty.channel.socket.nio),
2 OIO-based TCP/IP transport (See org.jboss.netty.channel.socket.oio),
3 OIO-based UDP/IP transport, and
4 Local transport (See org.jboss.netty.channel.local).
切换不同的传输实现通常只需对代码进行几行的修改调整,例如选择一个不同的ChannelFactory实现。
此外,你甚至可以利用那些还没有写入的新的传输协议,只需替换一些构造器的调用方法即可,例如串口通信。而且由于核心API具有高度的可扩展性,你还可以完成自己的传输实现。
2.3. 基于拦截链模式的事件模型
在一个ChannelPipeline内部一个ChannelEvent被一组ChannelHandler处理。这个管道是拦截过滤器模式的一种高级形式的实现,因此对于一个事件如何被处理以及管道内部处理器间的交互过程,你都将拥有绝对的控制力。例如,你可以定义一个从socket读取到数据后的操作:
public class MyReadHandler implements SimpleChannelHandler {
public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
同时你也可以定义当处理器接收入到写请求时的动作:
public class MyWriteHandler implements SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
有关事件模型的更多信息,参考API文档中的ChannelEvent 和ChannelPipeline部分。
2.4. 适用快速开发的高级组件
上述所提及的核心组件已经足够实现各种类型的网络应用,除此之外,Netty也提供了一系列的高级组件来加速你的开发过程。2.4.1. Codec框架
从业务逻辑代码中分离协议处理部分总是一个很不错的想法。然而如果一切从零开始便会遭遇 到实现上的复杂性。你不得不处理分段的消息。一些协议是多层的(例如构建在其他低层协议之上的协议)。一些协议过于复杂以致难以在一台主机(single state machine)上实现。因此,一个好的网络应用框架应该提供一种可扩展,可重用,可单元测试并且是多层的codec框架,为用户提供易维护的codec代码。
Netty提供了一组构建在其核心模块之上的codec实现,这些简单的或者高级的codec实现帮你解决了大部分在你进行协议处理开发过程会遇到的问题,无论这些协议是简单的还是复杂的,二进制的或是简单文本的。
2.4.2. SSL / TLS 支持
不同于传统阻塞式的I/O实现,在NIO模式下支持SSL功能是一个艰难的工作。你不能只是简单的包装一下流数据并进行加密或解密工作,你不得不借助于 javax.net.ssl.SSLEngine,SSLEngine是一个有状态的实现,其复杂性不亚于SSL自身。你必须管理所有可能的状态,例如密 码套件,密钥协商(或重新协商),证书交换以及认证等。此外,与通常期望情况相反的是SSLEngine甚至不是一个绝对的线程安全实现。在Netty内部,SslHandler 封装了所有艰难的细节以及使用SSLEngine可能带来的陷阱。你所做的仅是配置并将该SslHandler插入到你的ChannelPipeline中。同样Netty也允许你实现像StartTlS 那样所拥有的高级特性,这很容易。
2.4.3. HTTP实现
HTTP无疑是互联网上最受欢迎的协议,并且已经有了一些例如Servlet容器这样的HTTP实现。因此,为什么Netty还要在其核心模块之上构建一套HTTP实现?与现有的HTTP实现相比Netty的HTTP实现是相当与众不同的。在HTTP消息的低层交互过程中你将拥有绝对的控制力。这是因为Netty的 HTTP实现只是一些HTTP codec和HTTP消息类的简单组合,这里不存在任何限制——例如那种被迫选择的线程模型。你可以随心所欲的编写那种可以完全按照你期望的工作方式工作的客户端或服务器端代码。这包括线程模型,连接生命期,快编码,以及所有HTTP协议允许你做的,所有的一切,你都将拥有绝对的控制力。
由于这种高度可定制化的特性,你可以开发一个非常高效的HTTP服务器,例如:
1 要求持久化链接以及服务器端推送技术的聊天服务(e.g. Comet )
2 需要保持链接直至整个文件下载完成的媒体流服务(e.g. 2小时长的电影)
3 需要上传大文件并且没有内存压力的文件服务(e.g. 上传1GB文件的请求)
4 支持大规模mash-up应用以及数以万计连接的第三方web services异步处理平台
2.4.4. Google Protocol Buffer 整合
Google Protocol Buffers 是快速实现一个高效的二进制协议的理想方案。通过使用 ProtobufEncoder 和ProtobufDecoder,你可以把Google Protocol Buffers 编译器(protoc)生成的消息类放入到Netty的codec实现中。请参考官方示例中的“LocalTime”示例,这个例子也同时显示出开发一个由简单协议定义的客户及服务端是多么的容易。2.5. 总述
在这一章节,我们从功能特性的角度回顾了Netty的整体架构。Netty有一个简单却不失强大的架构。这个架构由三部分组成——缓冲(buffer), 通道(channel),事件模型(event model)——所有的高级特性都构建在这三个核心组件之上。一旦你理解了它们之间的工作原理,你便不难理解在本章简要提及的更多高级特性。3. 比较重要的几个特性
3.1. buffer
org.jboss.netty.buffer包的接口及类的结构图如下: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,分配策略有两种:
4 通常出于简单考虑,直接分配固定大小的buffer,缺点是,对一些应用来说这个大小限制有时是不合理的,并且如果buffer的上限很大也会有内存上的浪费。
5 针对固定大小的buffer缺点,就引入动态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中的工厂方法。
3.2. Channel
和Channel相关的接口及类结构图如下:l 当前Channel的状态信息,比如是打开还是关闭等。
l 通过ChannelConfig可以得到的Channel配置信息。
l Channel所支持的如read、write、bind、connect等IO操作。
l 得到处理该Channel的ChannelPipeline,既而可以调用其做和请求相关的IO操作。
在Channel实现方面,以通常使用的nio socket来说,Netty中的NioServerSocketChannel和NioSocketChannel分别封装了java.nio中包含的 ServerSocketChannel和SocketChannel的功能。
3.3. ChannelEvent
如前所述,Netty是事件驱动的,其通过ChannelEvent来确定事件流的方向。一个ChannelEvent将被ChannelPipeline的一系列ChannelHandler处理。下面是和 ChannelEvent相关的接口及类图:对于使用者来说,在ChannelHandler实现类中会使用继承于ChannelEvent的MessageEvent,调用其 getMessage()方法来获得读到的ChannelBuffer或被转化的对象。
3.4. ChannelPipeline
Netty 在事件处理上,是通过ChannelPipeline来控制事件流,通过调用注册其上的一系列ChannelHandler来处理事件,这也是典型的拦截器模式。下面是和ChannelPipeline相关的接口及类图:举例来说,假设我们创建了以下的管道:
ChannelPipeline p = Channels.pipeline();
p.addLast("1", new UpstreamHandlerA());
p.addLast("2", new UpstreamHandlerB());
p.addLast("3", new DownstreamHandlerA());
p.addLast("4", new DownstreamHandlerB());
p.addLast("5", new UpstreamHandlerX());
其中类名以Upstream开头的是一个upstream handler,类名以Downstream开头的是一个downstream handler。当一个upstream event发生的时候,处理的顺序应该是1,2,3,4,5,而当一个downstream event发生的时候,处理的顺序应该是5,4,3,2,1,在这个原则之上,ChannelPipeline会跳过一些handler:
- 3和4号handler没有实现ChannelUpstreamHandler,因此实际上upstream event 被处理的顺序为:1,2,5。
- 1,2和5号handler没有实现ChannelDownstreamHandler,因此实现上downstream event 被处理的顺序为4,3。
- 如果5号handler继承了SimpleChannelHandler(SimpleChannelHandler实现了ChannelUpstreamHandler 和ChannelDownstreamHandler接口),那么upstream event和downstream event被处理的顺序就应该是125和543。
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等包,则是和协议相关的实现。
3.5. 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。
4. 源代码分析
4.1. org.jboss.netty.bootstrap
提供了一些辅助类用于实现典型的服务器和客户端初始化。4.2. org.jboss.netty.buffer
提供Netty中的byte buffer的抽象和实现。4.3. org.jboss.netty.channel
channel核心API,包括异步和事件驱动等各种传送接口。4.4. org.jboss.netty.channel.group
channel group,帮助用户维护channel列表。4.5. org.jboss.netty.local
一种虚拟传输方式,允许同一个虚拟机上的两个部分可以互相通信。4.6. org.jboss.netty.socket
TCP、UDP接口,继承了核心的channel API。4.7. org.jboss.netty.socket.nio
基于nio的socket channel实现4.8. org.jboss.netty.socket.oio
基于老io的socket channel实现4.9. org.jboss.netty.socket.http
基于http的客户端和相应的server端的实现,工作在有防火墙的情况4.10. org.jboss.netty.container
各种容器的兼容4.11. org.jboss.netty.container.microcontainer
JBoss Microcontainer集成接口4.12. org.jboss.netty.container.osgi
OSGi framework集成接口4.13. org.jboss.netty.container.spring
Spring framework集成接口4.14. org.jboss.netty.handler
处理器4.15. org.jboss.netty.handler.codec
编码解码器4.16. org.jboss.netty.handler.execution
基于Executor的实现4.17. org.jboss.netty.handler.queue
将event存入内部队列的处理4.18. org.jboss.netty.handler.ssl
基于SSLEngine的SSL以及TLS实现4.19. org.jboss.netty.handler.stream
异步写入大数据,不会产生OutOfMemory也不会花费很多内存4.20. org.jboss.netty.handler.timeout
通过Timer来对读写超时或者闲置链接进行通知4.21. org.jboss.netty.handler.codec.base64
Base64编码4.22. org.jboss.netty.handler.codec.compression
压缩格式4.23. org.jboss.netty.handler.codec.embedder
嵌入模式下编码和解码4.24. org.jboss.netty.handler.codec.frame
评估流的数据的排列和内容4.25. org.jboss.netty.handler.codec.http.websocket
websocket编码和解码4.26. org.jboss.netty.handler.codec.http
http的编码解码以及类型信息4.27. org.jboss.netty.handler.codec.oneone
对象到对象编码解码4.28. org.jboss.netty.handler.codec.protobuf
Protocol Buffers的编码解码4.29. org.jboss.netty.handler.codec.replay
在阻塞io中实现非阻塞解码4.30. org.jboss.netty.handler.codec.rtsp
RTSP的编码解码4.31. org.jboss.netty.handler.codec.serialization
序列化对象到bytebuffer实现4.32. org.jboss.netty.handler.codec.string
字符串编码解码,继承oneone4.33. org.jboss.netty.logging
根据不同的log framework实现的类4.34. org.jboss.netty.util
Netty util类4.35. org.jboss.netty.internal
Netty 内部util类,不被外部使用5. 参考资料
Netty官网:http://www.jboss.org/nettyNetty文档及范例:http://www.jboss.org/netty/documentation