Java IO模型

一、           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(Object.class) {

 

                         @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版本更新比较快,网上很多文章说的是早起版本,现在结构上有了很大变化,请下载最新的源代码

你可能感兴趣的:(JAVA)