Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序[官方定义],总体来看其包含了如下内容:1.提供了丰富的协议编解码支持,2.实现自有的buffer系统,减小复制所带来的消耗,3.整套channel的实现,4.基于事件的过程流转以及完整的网络事件响应与扩展,5.丰富的example。本文并不对Netty实际使用中可能出现的问题作分析,只是从代码角度分析它的架构以及实现上的一些关键细节。java
首先来看下最如何使用Netty(其自带example很好展现了使用),Netty普通使用通常是经过BootStrap来启动,BootStrap主要分为两类:1.面向链接(TCP)的BootStrap(ClientBootStrap和ServerBootstrap),2.非面向链接(UDP) 的(ConnectionlessBootstrap)。bootstrap
Netty总体架构很清晰的分红2个部分,ChannelFactory 和ChannelPipelineFactory,前者主要生产网络通讯相关的Channel实例和ChannelSink实例,Netty提供的 ChannelFactory实现基本可以知足绝大部分用户的需求,固然你也能够定制本身的ChannelFactory,后者主要关注于具体传输数据的处理,同时也包括其余方面的内容,好比异常处理等等,只要是你但愿的,你均可以往里添加相应的handler,通常 ChannelPipelineFactory由用户本身实现,由于传输数据的处理及其余操做和业务关联比较紧密,须要自定义处理的handler。api
如今,使用Netty的步骤实际上已经很是明确了,好比面向链接的Netty服务端客户端使用,第一步:实例化一个BootStrap,而且经过构造方法指定一个ChannelFactory实现,第二步:向bootstrap实例注册一个本身实现的ChannelPipelineFactory,第三步:若是是服务器端,bootstrap.bind(new InetSocketAddress(port)),而后等待客户端来链接,若是是客户端,bootstrap.connect(new InetSocketAddress(host,port))取得一个future,这个时候Netty会去链接远程主机,在链接完成后,会发起类型为 CONNECTED的ChannelStateEvent,而且开始在你自定义的Pipeline里面流转,若是你注册的handler有这个事件的响应方法的话那么就会调用到这个方法。在此以后就是数据的传输了。下面是一个简单客户端的代码解读。
[java]
// 实例化一个客户端Bootstrap实例,其中NioClientSocketChannelFactory实例由Netty提供
ClientBootstrap bootstrap = new ClientBootstrap(
new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));服务器
// 设置PipelineFactory,由客户端本身实现
bootstrap.setPipelineFactory(new FactorialClientPipelineFactory(count));网络
//向目标地址发起一个链接
ChannelFuture connectFuture =
bootstrap.connect(new InetSocketAddress(host, port));多线程
// 等待连接成功,成功后发起的connected事件将会使handler开始发送信息而且等待messageRecive,固然这只是示例。
Channel channel = connectFuture.awaitUninterruptibly().getChannel();架构
// 获得用户自定义的handler
FactorialClientHandler handler =
(FactorialClientHandler) channel.getPipeline().getLast();app
// 从handler里面取数据而且打印,这里须要注意的是,handler.getFactorial使用了从结果队列result take数据的阻塞方法,而结果队列会在messageRecieve事件发生时被填充接收回来的数据
System.err.format(
"Factorial of %,d is: %,d", count, handler.getFactorial());
[/java]
Netty提供了NIO与BIO(OIO)两种模式处理这些逻辑,其中NIO主要经过一个BOSS线程处理等待连接的接入,若干个WORKER线程(从worker线程池中挑选一个赋给Channel实例,由于Channel实例持有真正的 java网络对象)接过BOSS线程递交过来的CHANNEL进行数据读写而且触发相应事件传递给pipeline进行数据处理,而BIO(OIO)方式服务器端虽然仍是经过一个BOSS线程来处理等待连接的接入,可是客户端是由主线程直接connect,另外写数据C/S两端都是直接主线程写,而数据读操做是经过一个WORKER 线程BLOCK方式读取(一直等待,直到读到数据,除非channel关闭)。框架
网络动做归结到最简单就是服务器端bind->accept->read->write,客户端 connect->read->write,通常bind或者connect后会有屡次read、write。这种特性致使,bind,accept与read,write的线程分离,connect与read、write线程分离,这样作的好处就是不管是服务器端仍是客户端吞吐量将有效增大,以便充分利用机器的处理能力,而不是卡在网络链接上,不过一旦机器处理能力充分利用后,这种方式反而可能会由于过于频繁的线程切换致使性能损失而得不偿失,而且这种处理模型复杂度比较高。less
采用什么样的网络事件响应处理机制对于网络吞吐量是很是重要的,Netty采用的是标准的SEDA(Staged Event-Driven Architecture)架构[http://en.wikipedia.org/wiki/ Staged_event-driven_architecture],其所设计的事件类型,表明了网络交互的各个阶段,而且在每一个阶段发生时,触发相应事件交给初始化时生成的pipeline实例进行处理。事件处理都是经过Channels类的静态方法调用开始的,将事件、channel传递给 channel持有的Pipeline进行处理,Channels类几乎全部方法都为静态,提供一种Proxy的效果(整个工程里不管什么时候何地均可以调用其静态方法触发固定的事件流转,但其自己并不关注具体的处理流程)。
Channels部分事件流转静态方法
1.fireChannelOpen 2.fireChannelBound 3.fireChannelConnected 4.fireMessageReceived 5.fireWriteComplete 6.fireChannelInterestChanged
7.fireChannelDisconnected 8.fireChannelUnbound 9.fireChannelClosed 10.fireExceptionCaught 11.fireChildChannelStateChanged
Netty提供了全面而又丰富的网络事件类型,其将java中的网络事件分为了两种类型Upstream和Downstream。通常来讲,Upstream类型的事件主要是由网络底层反馈给Netty的,好比messageReceived,channelConnected等事件,而Downstream类型的事件是由框架本身发起的,好比bind,write,connect,close等事件。
Netty的Upstream和Downstream网络事件类型特性也使一个Handler分为了3种类型,专门处理Upstream,专门处理Downstream,同时处理Upstream,Downstream。实现方式是某个具体Handler经过继承ChannelUpstreamHandler和ChannelDownstreamHandler类来进行区分。PipeLine在Downstream或者Upstream类型的网络事件发生时,会调用匹配事件类型的Handler响应这种调用。ChannelPipeline维持有全部handler有序链表,而且由handler自身控制是否继续流转到下一个handler(ctx.sendDownstream(e),这样设计有个好处就是随时终止流转,业务目的达到无需继续流转到下一个handler)。下面的代码是取得下一个处理Downstream事件的处理器。
[java]
DefaultChannelHandlerContext realCtx = ctx;
while (!realCtx.canHandleUpstream()) {
realCtx = realCtx.next;
if (realCtx == null) {
return null;
}
}
return realCtx;
[/java]
若是是一个网络会话最末端的事件,好比messageRecieve,那么可能在某个handler里面就直接结束整个会话,并把数据交给上层应用,可是若是是网络会话的中途事件,好比connect事件,那么当触发connect事件时,通过pipeline流转,最终会到达挂载pipeline最底下的ChannelSink实例中,这类实例主要做用就是发送请求和接收请求,以及数据的读写操做。
NIO方式ChannelSink通常会有1个BOSS实例(implements Runnable),以及若干个worker实例(不设置默认为cpu cores*2个worker),这在前面已经提起过,BOSS线程在客户端类型的ChannelSink和服务器端类型的ChannelSink触发条件不同,客户端类型的BOSS线程是在发生connect事件时启动,主要监听connect是否成功,若是成功,将启动一个worker线程,将connected的channel交给这个线程继续下面的工做,而服务器端的BOSS线程是发生在bind事件时启动,它的工做也相对比较简单,对于channel.socket().accept()进来的请求向Nioworker进行工做分配便可。这里须要提到的是,Server端ChannelSink实现比较特别,不管是NioServerSocketPipelineSink 仍是OioServerSocketPipelineSink的eventSunk方法实现都将channel分为 ServerSocketChannel和SocketChannel分开处理。这主要缘由是Boss线程accept()一个新的链接生成一个 SocketChannel交给Worker进行数据接收。
[java]
public void eventSunk(
ChannelPipeline pipeline, ChannelEvent e) throws Exception {
Channel channel = e.getChannel();
if (channel instanceof NioServerSocketChannel) {
handleServerSocket(e);
} else if (channel instanceof NioSocketChannel) {
handleAcceptedSocket(e);
}
}
NioWorker worker = nextWorker();
worker.register(new NioAcceptedSocketChannel(
channel.getFactory(), pipeline, channel,
NioServerSocketPipelineSink.this, acceptedSocket,
worker, currentThread), null);
[/java]
另外二者实例化时都会走一遍以下流程:
[java]
setConnected();
fireChannelOpen(this);
fireChannelBound(this, getLocalAddress());
fireChannelConnected(this, getRemoteAddress());
[/java]
而对应的ChannelSink里面的处理代码就不一样于ServerSocketChannel了,由于走的是 handleAcceptedSocket(e)这一块代码,从默认实现代码来讲,实例化调用 fireChannelOpen(this);fireChannelBound(this,getLocalAddress());fireChannelConnected(this,getRemoteAddress())没有什么意义,可是对于本身实现的ChannelSink有着特殊意义。具体的用途我没去了解,可是可让用户插手Server accept链接到准备读写数据这一个过程的处理。
[java]
switch (state) {
case OPEN:
if (Boolean.FALSE.equals(value)) {
channel.worker.close(channel, future);
}
break;
case BOUND:
case CONNECTED:
if (value == null) {
channel.worker.close(channel, future);
}
break;
case INTEREST_OPS:
channel.worker.setInterestOps(channel, future, ((Integer) value).intValue());
break;
}
[/java]
Netty提供了大量的handler来处理网络数据,可是大部分是CODEC相关的,以便支持多种协议,下面一个图绘制了现阶段Netty提供的Handlers(红色部分不彻底)
Netty实现封装实现了本身的一套ByteBuffer系统,这个ByteBuffer系统对外统一的接口就是ChannelBuffer,这个接口从总体上来讲定义了两类方法,一种是相似getXXX(int index…),setXXX(int index…)须要指定开始操做buffer的起始位置,简单点来讲就是直接操做底层buffer,并不用到Netty特有的高可重用性buffer特性,因此Netty内部对于这类方法调用很是少,另一种是相似readXXX(),writeXXX()不须要指定位置的buffer操做,这类方法实现放在了AbstractChannelBuffer,其主要的特性就是维持buffer的位置信息,包括readerIndex,writerIndex,以及回溯做用的markedReaderIndex和markedWriterIndex,当用户调用readXXX()或者writeXXX()方法时,AbstractChannelBuffer会根据维护的readerIndex,writerIndex计算出读取位置,而后调用继承本身的ChannelBuffer的getXXX(int index…)或者setXXX(int index…)方法返回结果,这类方法在Netty内部被大量调用,由于这个特性最大的好处就是很方便地重用buffer而没必要去费心费力维护index或者新建大量的ByteBuffer。
另外WrappedChannelBuffer接口提供的是对ChannelBuffer的代理,他的用途说白了就是重用底层buffer,可是会转换一些buffer的角色,好比本来是读写皆可 ,wrap成ReadOnlyChannelBuffer,那么整个buffer只能使用readXXX()或者getXXX()方法,也就是只读,而后底层的buffer仍是原来那个,再如一个已经进行过读写的ChannelBuffer被wrap成TruncatedChannelBuffer,那么新的buffer将会忽略掉被wrap的buffer内数据,而且能够指定新的writeIndex,至关于slice功能。
Netty实现了本身的一套完整Channel系统,这个channel说实在也是对java 网络作了一层封装,加上了SEDA特性(基于事件响应,异步,多线程等)。其最终的网络通讯仍是依靠底下的java网络api。提到异步,不得不提到Netty的Future系统,从channel的定义来讲,write,bind,connect,disconnect,unbind,close,甚至包括setInterestOps等方法都会返回一个channelFuture,这这些方法调用都会触发相关网络事件,而且在pipeline中流转。Channel不少方法调用基本上不会立刻就执行到最底层,而是触发事件,在pipeline中走一圈,最后才在channelsink中执行相关操做,若是涉及网络操做,那么最终调用会回到Channel中,也就是serversocketchannel,socketchannel,serversocket,socket等java原生网络api的调用,而这些实例就是jboss实现的channel所持有的(部分channel)。
Netty新版本出现了一个特性zero-copy,这个机制可使文件内容直接传输到相应channel上而不须要经过cpu参与,也就少了一次内存复制。Netty内部ChunkedFile 和 FileRegion 构成了non zero-copy 和zero-copy两种形式的文件内容传输机制,前者须要CPU参与,后者根据操做系统是否支持zero-copy将文件数据传输到特定channel,若是操做系统支持,不须要cpu参与,从而少了一次内存复制。ChunkedFile主要使用file的read,readFully等API,而FileRegion使用FileChannel的transferTo API,2者实现并不复杂。Zero-copy的特性仍是得看操做系统的,自己代码没有很大的特别之处。
最后总结下,Netty的架构思想和细节能够说让人眼前一亮,对于java网络IO的各个注意点,能够说Netty已经解决得比较彻底了,同时Netty 的做者也是另一个NIO框架MINA的做者,在实际使用中积累了丰富的经验,可是本文也只是一个新手对于Netty的初步理解,尚未足够的能力指出某一细节的所发挥的做用。
若有问题请直接到群:457036818或者登录http://java.tanzhouedu.net