一、 IO模型
所谓IO,就是计算机的输入输出系统,大多IO都会和硬件打交道,比如内存、硬盘、光驱等。由于要操作硬件,IO一般都比较耗时,所以操作系统都支持同步和异步两种IO方式,同步IO就是要等到操作完成才继续执行,而异步IO不需要等待。异步IO在各操作系统上的实现方式也不太一样,以下列举出windows和linux操作系统上的异步IO模型。
1、Windows
Ø 选择(Select)
Ø 异步选择(Async Select)
Ø 事件选择(Event Select)
Ø 重叠IO(Overlapped IO)
Ø 完成端口(Completion Port)
2、Linux
Ø 非阻塞I/O
Ø I/O复用(select和poll)
Ø 信号驱动I/O(SIGIO)
Ø 异步I/O(Posix.1的aio_系列函数)
Ø 增强版复用IO(epoll)(参考:http://blog.csdn.net/ljx0305/article/details/4065058)
Windows IO模型请参考:http://blog.pfan.cn/article.asp?id=51699
Linux IO模型请参考:http://blog.csdn.net/z3410218746/article/details/7563379
其中Windows的IOCP和linux的epoll被广泛用于高容量、大并发服务器。
同步IO和异步IO的使用场景:
同步IO一般用于对性能、并发量要求不是太高的场景,主要是客户端,比如一个社交应用需要读写文件,这时候就没有必要使用异步IO。
异步IO大多用于服务器端,因为服务器端要处理很大容量的数据以及保持很高的并发量。
二、 Javaio模型
1、OIO
OIO就是同步IO
2、NIO
在JDK 1.4版本之后才开始支持异步IO,其实NIO也是使用操作系统的IO模型,但在各操作系统上的实现方式也不太一样
在Windows系统使用的是Select模型,而不是性能更高的IOCP,原因就是并非所有Windows都支持IOCP,IOCP在windows NT 3.5中被引入,只支持WindowsNT和windows 2000。(参考:http://www.cnblogs.com/jobs/archive/2006/11/22/568023.html)
在Linux系统上使用的是多路复用IO,JDK 6之前用的是poll模型,JDK 7中使用epoll。(参考:http://www.cnblogs.com/jobs/archive/2006/11/22/568022.html)
3、AIO
JDK 7中出现了AIO,其实AIO就是NIO的增强版,因为JDK 6之前用的是poll,JDK 7用的是epoll,而epoll就是poll的增强版。
NIO三大概念:
1、Buffer
用于管理缓冲区,发送和接收都要使用Buffer
2、Channel
封装了各种IO,主要有FileChannel、SocketChannel、ServerSocketChannel等,并提供异步操作的方法
3、Selector
用于检索哪些Channel有事件发生,一共定义了四种事件,分别是accept、connect、read、write。
Java NIO的详细用法请参考:http://blog.csdn.net/geli_hero?viewmode=contents
AIO:
其实NIO和AIO的区别就是poll和epoll的区别。
poll和epoll的区别:(参考:http://blog.csdn.net/xuexi1028/article/details/7631567)
poll需要主动轮询描述符来探测事件,而epoll模型会主动上报事件。
AIO为每个Channel增加了对应的Asynchronous…Channel,这些Channel的操作都增加了一个回调,当此操作成功时就会调用回调。
下面说下nio和aio的流程(以接收数据为例):
Nio:
Ø 注册事件(channel.register(selector))
Ø 轮询事件(selector.select())
Ø 获得事件(selectKeys)
Ø 读取数据(channel.read(byteBuffer))
Aio:
Ø 创建线程池(AsynchronousChannelGroup)
Ø 绑定到线程池(async…Channel.open(group))
Ø 读取操作并绑定事件(read(buf,readCompletedHandler))
Ø 对应端口接收到数据,系统主动把数据放到buf里,然后放到完成列表
Ø 线程池从完成列表里取出,并触发回调。
底层的区别:
1、获取事件的方式不同
主要体现在Epoll和poll之间的区别
poll要通过select操作来取出事件,系统层面的流程如下:
Ø 注册描述符
Ø 端口产生数据
Ø 修改描述符
Ø 应用程序线程通过select查找有事件的描述符
而epool比poll多出一个描述符列表,流程如下:
Ø 注册描述符
Ø 端口产生事件
Ø 把此描述符放到已完成列表里
Ø 应用程序线程从已完成列表里取出事件
Poll要从一个很大的列表里查找出所有有事件的描述符,是比较耗时的,而epoll直接去已完成的列表里去拿就行了
Windows的IOCP可以注册多个端口(已完成描述符列表),系统会把事件放到其中一个端口上,设计上一般线程池里的每个线程都检测一个对应的端口
Epoll是否项IOCP一样可以创建多个端口?这个问题还没查到,以后有时间再查
2、读取数据的方式不同
Poll事件产生之后,其实数据还在对应的端口上,需要程序去再到这个端口上拿数据。
而epoll就不需要,因为系统已经把数据附加到事件上,并放到了已完成描述符列表里,程序拿到事件的时候就已经拿到了数据
应用上的区别:
由于底层的区别造成了应用层面上的设计也是不一样的,NIO一般采用Reactor模式,AIO使用Proactor模式。
关于这两种模式的详解和区别请参考: http://www.cnblogs.com/dawen/archive/2011/05/18/2050358.html
Reactor模式(参考:http://www.oschina.net/question/16_9863):
1、单线程
2、多线程
3、主Reactor加子Reactor
Proactor模式(下面几张图片用于描述这种模式,详细请参考: http://www.61ic.com/Technology/Communicate/200811/21669.html):
三、 开源服务器框架
一直以来,IO是服务器端程序的最大瓶颈,选择合适的IO模型会大大提高服务器的性能,之前很多Web服务器采用的是同步IO,Java开发者也大都习惯于同步IO,如今很多Web服务器开始支持异步IO,比如Tomca 6,Resin 3,还有些开源框架,如netty,mina。
注:Tomca使用的是java的NIO,而Resin使用的是JNI调用本地代码(参考:http://bbs.linuxtone.org/thread-1484-1-1.html)
开源框架Netty和Mina。
首先看一下Netty和Mina的简单介绍:
Netty和Mina都是Socket通信框架,它们都采用开性能的IO模型,并简化了通信程序的开发工作。
Ø mina比netty出现的早,都是Trustin Lee的作品;、
Ø mina将内核和一些特性的联系过于紧密,使得用户在不需要这些特性的时候无法脱离,相比下性能会有所下降;netty解决了这个设计问题;
Ø netty的文档更清晰,很多mina的特性在netty里都有;
Ø netty更新周期更短,新版本的发布比较快;
Ø 它们的架构差别不大,mina靠apache生存,而netty靠jboss,和jboss的结合度非常高,netty有对google protocal buf的支持,有更完整的ioc容器支持(spring,guice,jbossmc和osgi);
Ø netty比mina使用起来更简单,netty里你可以自定义的处理upstream events 或/和 downstream events,可以使用decoder和encoder来解码和编码发送内容;
Ø netty和mina在处理UDP时有一些不同,netty将UDP无连接的特性暴露出来;而mina对UDP进行了高级层次的抽象,可以把UDP当成"面向连接"的协议,而要netty做到这一点比较困难。
由于Netty比Mina更优秀,这里我们主要学习下Netty。
Netty的实现原理
Netty有几个概念:
1、Buffer
Netty的ByteBuffer是在Java NIO的Buffer基础上做了一些封装,主要是便于使用,在netty-buffer包里。下面是几个比较重要的Buffer:
a) HeapChannelBuffer
这是Netty读网络数据时默认使用的ChannelBuffer,这里的Heap就是Java堆的意思,因为 读SocketChannel的数据是要经过ByteBuffer的,而ByteBuffer实际操作的就是个byte数组,所以 ChannelBuffer的内部就包含了一个byte数组,使得ByteBuffer和ChannelBuffer之间的转换是零拷贝方式。根据网络字 节续的不同,HeapChannelBuffer又分为BigEndianHeapChannelBuffer和 LittleEndianHeapChannelBuffer,默认使用的是BigEndianHeapChannelBuffer。Netty在读网络 数据时使用的就是HeapChannelBuffer,HeapChannelBuffer是个大小固定的buffer,为了不至于分配的Buffer的 大小不太合适,Netty在分配Buffer时会参考上次请求需要的大小。
b) DynamicChannelBuffer
相比于HeapChannelBuffer,DynamicChannelBuffer可动态自适应大 小。对于在DecodeHandler中的写数据操作,在数据大小未知的情况下,通常使用DynamicChannelBuffer。
c) ByteBufferBackedChannelBuffer
这是directBuffer,直接封装了ByteBuffer的 directBuffer。对于读写网络数据的buffer,分配策略有两种:1)通常出于简单考虑,直接分配固定大小的buffer,缺点是,对一些应用来说这个大小限制有时是不 合理的,并且如果buffer的上限很大也会有内存上的浪费。2)针对固定大小的buffer缺点,就引入动态buffer,动态buffer之于固定 buffer相当于List之于Array。
2、ChannelBuf
ChannelBuf是对ChannelPipeline上数据的描述,一共有两种类型的ChannelBuf,分别是:
ByteBuf:里面的数据是二进制Buffer,一般刚从socket读出来或刚要写进去时的数据都是ByteBuf,也就是ChannelPipeline上第一个写Handler和第一个读Handler接收到的数据都是ByteBuf
MessageBuf:里面的数据是Java对象,可以是任意的数据
3、Channel
Netty的Channel也是对Java NIO的Channel的扩展,在包netty-transport里面。
这里的Channel不提供直接的操作,可以通过Channel获取当前的状态,也可以获取ChannelPipeline,主要的操作都封装在ChannelPipeline里面。
Netty分别封装了OIO、NIO、AIO的Channel,只需修改一下配置就可以使用不同的IO模型。
4、ChannelPipeline
ChannelPipeline就类似一个生产线,如下图所示:
* I/O Request
* via {@link Channel} or
* {@link ChannelHandlerContext}
* |
* +----------------------------------------+---------------+
* | ChannelPipeline | |
* | \|/ |
* | +----------------------+ +-----------+------------+ |
* | | Upstream Handler N | | Downstream Handler 1 | |
* | +----------+-----------+ +-----------+------------+ |
* | /|\ | |
* | | \|/ |
* | +----------+-----------+ +-----------+------------+ |
* | | Upstream Handler N-1 | |Downstream Handler 2 | |
* | +----------+-----------+ +-----------+------------+ |
* | /|\ . |
* | . . |
* | [ sendUpstream()] [ sendDownstream()] |
* | [ + VAL_INBOUND data] [ + VAL_OUTBOUND data ] |
* | . . |
* | . \|/ |
* | +----------+-----------+ +-----------+------------+ |
* | | Upstream Handler 2 | | Downstream Handler M-1| |
* | +----------+-----------+ +-----------+------------+ |
* | /|\ | |
* | | \|/ |
* | +----------+-----------+ +-----------+------------+ |
* | | Upstream Handler 1 | | Downstream Handler M | |
* | +----------+-----------+ +-----------+------------+ |
* | /|\ | |
* +-------------+--------------------------+---------------+
* | \|/
* +-------------+--------------------------+---------------+
* | | | |
* | [ Socket.read()] [ Socket.write()] |
* | |
* | Netty Internal I/O Threads (Transport Implementation) |
* +--------------------------------------------------------+
发送的时候,数据经过一系列的处理最终有socket发送出去,接收的时候socket拿到数据然后经过一些列的处理交给业务处理层。就像生产线上有很多工人,每个工人负责的具体工作也不一样,有负责解包的,有负责解析协议的等等,这些在Netty里都是Handler完成。
一个ChannelPipeline里可以注册多个Handler,并且提供了一系列管理Handler的方法,每个Handler处理完成自己的工作之后再交给下一个Channel。
ChannelPipeline还提供获取生产线上当前的数据的方法:
Ø inboundMessageBuffer和inboundByteBuffer:读取的数据
Ø outboundMessageBuffer和outboundByteBuffer:写入的数据
5、ChannelHandlerContext
表示ChannelPipeline上当前上下文环境
Ø channel():获取当前的Channel
Ø pipeline():获取当前管道
Ø executor():获取当前的处理器(线程池里正在运行的线程)
Ø handler()
Ø types()
Ø hasInboundByteBuffer():是否ChannelPipeline中有读取的数据
Ø hasInboundMessageBuffer()
Ø inboundByteBuffer()
Ø inboundMessageBuffer()
Ø hasOutboundByteBuffer():获取ChannelPipeline中是否有写入的数据
Ø hasOutboundMessageBuffer()
Ø outboundByteBuffer()
Ø outboundMessageBuffer()
Ø hasNextInboundByteBuffer()
Ø hasNextInboundMessageBuffer()
Ø nextInboundByteBuffer()
Ø nextInboundMessageBuffer()
Ø hasNextOutboundByteBuffer()
Ø hasNextOutboundMessageBuffer()
Ø nextOutboundByteBuffer()
Ø nextOutboundMessageBuffer()
Ø isReadable()
Ø readable(boolean readable)
Ø DefaultChannelHandlerContext. Bind
Ø DefaultChannelHandlerContext. Connect
Ø DefaultChannelHandlerContext disconnect
Ø DefaultChannelHandlerContext.close
Ø DefaultChannelHandlerContext. Deregister
Ø DefaultChannelHandlerContext. Flush
Ø DefaultChannelHandlerContext. Write:写入数据
Handler的每个事件触发都会包含这个参数
6、Handler
Handler是ChannelPipeline上处理器,一共定义了四种类型的Handler,分别是:
STATE:状态
INBOUND:处理读取数据
OPERATION:操作
OUTBOUND:处理写入数据
基类是ChannelHandler,它定义了几个基本的事件,分别是:
Ø beforeAdd
Ø afterAdd
Ø beforeRemove
Ø afterRemove
Ø exceptionCaught:当有异常时触发
Ø userEventTriggered:用于触发用户注册的事件(如SctpNotificationEvent、ChannelInputShutdownEvent)
ChannelStateHandler集成自ChannelHandler,定义了一些状态事件:
Ø channelRegistered
Ø channelUnregistered
Ø channelActive
Ø channelInactive
Ø inboundBufferUpdated:ChannelPipeline上的数据发生变化时触发,一般由这个事件触发下个Handler处理数据
ChannelInboundHandler集成自ChannelStateHandler
Ø ChannelInboundByteHandler.newInboundBuffer:创建新的读取Buf(二进制)
Ø ChannelInBoundMessageHandler.newInboundBuffer:创建新的读取Buf(Java对象)
ChannelOperationHandler继承自ChannelHandler
Ø Bind:绑定到端口
Ø Connect:连接到远程服务器
Ø Disconnect:断开连接
Ø Close:关闭Channel
Ø Deregister:取消监听
Ø Flush:刷新流使其马上发送出去
ChannelOutboundHandler继承自ChannelOperationHandler
Ø ChannelOutboundByteHandler.newOutboundBuffer:创建新的写入Buf(二进制)
Ø ChannelOutboundMessageHandler.newOutboundBuffer:创建新的写入Buf(Java对象)
以上这些类也都定义对应的Adapter,用于实现对应的功能,主要是ChannelInboundHandlerAdapter:
Ø ChannelInboundByteHandlerAdapter.inboundBufferUpdated:接收到Byte类型Buf
Ø ChannelInboundMessageHandlerAdapter.messageReceived:接收到Message类型的Buf
以上都是Handler的基本类型,在实际应用都会扩展这些Handler,一般都会以下几种Handler
Ø decodeFrameHandler:用于解包(TCP协议会产生粘包、半包的问题)
Ø decodeHandler:用于根据协议解析数据包
Ø encodeHandler:用于封包(把数据按照协议封装成二进制数据包)
Ø executorHandler:用于处理协议
Ø businessHandler:根据不同的协议产生不同的业务逻辑
netty-codec封装了常用的decodeFrameHandler、decodeHandler、encodeHandler,另外还有加密解密的Handler(JZlibDecodeHandler、JZlibEncodeHandler、ZLibDecodeHandler、ZLibEncodeHandler),还有序列化等。
netty-http封装了http协议的一些解析方法
重点说下几个常用的解包Handler:
a) FixedLengthFrameDecoder
适用于每个数据的大小固定的协议,比如:
* +---+----+------+----+ * | A | BC | DEFG | HI | * +---+----+------+----+ 如果一个数据包的大小为3字节,则上面的数据经过解包分为下面3个数据包 * +-----+-----+-----+ * | ABC | DEF | GHI | * +-----+-----+-----+ |
b) LengthFieldBasedFrameDecoder
这个稍微负责一些,这种协议要求有协议头,协议头里有个字段表示后面数据的长度
* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) * +------+--------+------+----------------+ +------+----------------+ * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | * +------+--------+------+----------------+ +------+----------------+ |
c) DelimiterBasedFrameDecoder
这个比较好理解,协议里面有一个固定的数据作为结尾,DelimiterBasedFrameDecoder就以这个固定的分隔符来拆分数据包。
7、EventLoopGroup
EventLoopGroup就是线程池,整个程序都要依赖于线程池工作。
8、Bootstrap
Bootstrap是一个程序的入口,我们看一下它的定义:
Ø group(EventLoopGroup):绑定到线程池
Ø channel(Channel):指定Channel的类型
Ø channelFactory(ChannelFactory):指定ChannelFactory
Ø localAddress(host,port):配置本地端口
Ø option(ChannelOption)
Ø attr(AttributeKey)
Ø shutdown():停止服务
Ø bind()
Ø handler(channelHandler)
Ø client.remoteAddress(host,port):配置远程端口
Ø client.connect():连接到远程服务器
Ø server. childOption(ChannelOption)
Ø server. childHandler
Ø server. Bind()
Ø server.group(EventLoopGroup):绑定到线程池
Ø server.group(EventLoopGroup,EventLoopGroup)
Bootstrap用于client端,ServerBootstrap用于服务端
在Java IO模型里面我们讲到了两种设计模式,Reactor和Proactor,在这里就体现出了Netty如何使用Reactor模式。
其实EventLoopGroup就相当于Reactor,Bootstrap.group方法就是要创建Reactor模式,在client端只能创建单Reactor模式,在server端可以创建mainReactor加childReactor
一个简单的server端程序:
public class Server { public static void main(String[] args) { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup()) .channel(NioServerSocketChannel.class).localAddress(9001) .childHandler(new ChannelInitializer @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast( "1", new DelimiterBasedFrameDecoder( Integer.MAX_VALUE, new HeapByteBuf("\n" .getBytes(), 1)) { @Override public void inboundBufferUpdated( ChannelHandlerContext ctx) throws Exception { super.inboundBufferUpdated(ctx); } }); pipeline.addLast("2", new StringDecoder() { @Override public String decode(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { return super.decode(ctx, msg); } }); pipeline.addLast( "3", new ChannelInboundMessageHandlerAdapter Object.class, ByteBuf.class) {
@Override public void messageReceived( ChannelHandlerContext ctx, Object msg) throws Exception { if (ChannelHandlerUtil.unfoldAndAdd( ctx, msg, true)) { ctx.fireInboundBufferUpdated(); } } }); pipeline.addLast("4",new ChannelInboundMessageHandlerAdapter
@Override public void messageReceived( ChannelHandlerContext ctx, String msg) throws Exception { ByteBuf buf = Unpooled.copiedBuffer(msg, Charset.defaultCharset()); if (ChannelHandlerUtil.unfoldAndAdd( ctx, buf, true)) { ctx.fireInboundBufferUpdated(); } } }); pipeline.addLast("handler", new ChannelInboundByteHandlerAdapter() {
@Override public void inboundBufferUpdated( ChannelHandlerContext ctx, ByteBuf in) throws Exception { String msg = in.toString(Charset .defaultCharset()); System.out.println(msg);
} }); } }); bootstrap.bind(); } } |
Netty官方网站:https://netty.io/
由于时间有限,没有更深入的了解其架构,但是我觉得大致的结构和原理基本上清楚了,另外,netty版本更新比较快,网上很多文章说的是早起版本,现在结构上有了很大变化,请下载最新的源代码