Netty源码分析1---服务端绑定流程

前言:

一直自认为水平很差,什么都不懂,想找个源码来看看,无奈时间不够(码农搬砖很辛苦的诶),最近终于抽了点时间,看了一下Netty,感觉代码确实写得干净规范,看着舒服(吐槽一下有些开源代码,简直惨不忍睹,神马代码风格和规范通通木有,无力吐槽)。之前看的Netty源码分析感觉没说透彻,索性干脆自己写,也不知道对不对,和大家分享讨论一下吧,也算是复习巩固,欢迎交流。。。

 

注:

以下分析基于Netty 3.6.6 Final版本,虽然Netty4,5都出来了,但是3.x是经典啊(况且不知道用4 or 5的多不多,感觉一般公司都不太愿意用太新的东西吧),读代码是看别人怎么设计架构,业务逻辑实现不是最重要的,so,就这么愉快地决定了。。。

 

大家知道Netty是基于NIO的异步网络通信框架,下层使用Java NIO库(可以看成实际的网络通信层),上层对应于Netty自己的一些逻辑抽象。知道了这两层,对后面的理解就容易多了,下面是个草图(这两层之间的关系后面再讲):

 

 

下面是一个EchoServer的代码(EchoHandler原样回写,代码就不贴了):

ServerBootstrap bootstrap = new ServerBootstrap(

new NioServerSocketChannelFactory(

Executors.newCachedThreadPool(),

Executors.newCachedThreadPool()));

bootstrap.setPipelineFactory(new ChannelPipelineFactory() {

@Override

public ChannelPipeline getPipeline() throws Exception {

return Channels.pipeline(new EchoHandler());

}

}); 

bootstrap.bind(new InetSocketAddress(1210));

1. ServerBootstrap是一个辅助类,用于设置一些启动服务端时需要的参数,本身与服务端逻辑无关。这里用NioServerSocketChannelFactory初始化,传入了两个线程池(一个boss,一个worker,boss监听端口,worker处理具体的连接),隐约感觉有点往Reactor模式上靠的倾向。

2. 然后就是设置pipeline,即配置自己的业务逻辑handler。

3. 最后bind

1,2步很简单,直接看bind方法:

 

ChannelFuture future = bindAsync(localAddress);

future.awaitUninterruptibly();

if (!future.isSuccess()) {

    future.getChannel().close().awaitUninterruptibly();

    throw new ChannelException("Failed to bind to: " + localAddress,

    future.getCause());

}

return future.getChannel();

 

发现同步bind不过也就在异步bind上封了一下,所以现在你知道为啥Netty是全异步的了吧。。。bindAsync是关键,继续进去:

 

Binder binder = new Binder(localAddress);

ChannelHandler parentHandler = getParentHandler();

ChannelPipeline bossPipeline = pipeline();

bossPipeline.addLast("binder", binder);

if (parentHandler != null) {

    bossPipeline.addLast("userHandler", parentHandler);

}

Channel channel = getFactory().newChannel(bossPipeline);

final ChannelFuture bfuture = new DefaultChannelFuture(channel, false);

binder.bindFuture.addListener(new ChannelFutureListener() {

public void operationComplete(ChannelFuture future)

throws Exception {

if (future.isSuccess()) {

    bfuture.setSuccess();

} else {

    bfuture.getChannel().close();

    bfuture.setFailure(future.getCause());

}}});

return bfuture;

 

bindAsnyc首先new了一个binder,光看名字就知道是干啥的了,然后把binder和parent handler挂到了pipeline上,binder是一个UpStreamHandler:

 

public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent evt) {

    ......

    evt.getChannel().bind(localAddress).addListener(new ChannelFutureListener() {

        public void operationComplete(ChannelFuture future) throws Exception {

            if (future.isSuccess()) {

                bindFuture.setSuccess();

            } else {

                bindFuture.setFailure(future.getCause());

            }}});

}

 

binder监听channelOpen事件,得到channel,然后bind,binder是UpStream的,这个up或down就对应上面的两层结构,所以不难猜到up就是Java NIO(下面简称网络层)到Netty的,down反之。所以可以假设是网络层去触发的chennalOpen事件,由binder响应(后面验证)。evt.getChannel().bind依次调用Channels.bind:

channel.getPipeline().sendDownstream(

new DownstreamChannelStateEvent(channel, future,

ChannelState.BOUND, localAddress));

 

构造了一个DownStream事件,再调DefaultChannelPipeline.sendDownstream向下发送:

DefaultChannelHandlerContext tail = getActualDownstreamContext(this.tail);

if (tail == null) {

    try {

        getSink().eventSunk(this, e);

        return;

    } catch (Throwable t) {

        notifyHandlerException(e, t);

        return;

    }

}

               sendDownstream(tail, e);

 

sendDownstream(tail,e)中先处理事件,再向下传递事件,从这我们就知道handler链表是到底怎么处理的,而不只是一个宽泛的概念。继续,如果tail为null,就将事件传递到sink中(sink是水槽的意思,计算机里叫汇,还有个对应的叫源,想到水槽放水时的漩涡你就知道这个汇是干啥的了。。。无比形象),sink作为所有handler最终的汇集,起到连接Netty和网络层的作用。getSink().eventSunk()最终调到NioServerSocketPipelineSink.evenSunk,其中的关键是:

switch (state) {

        case BOUND:

            if (value != null) {

                ((NioServerBoss) channel.boss).bind(channel, future, (SocketAddress) value);

            } else {

                ((NioServerBoss) channel.boss).close(channel, future);

            }

            break;

 

前面构造的下行BOUND事件(前面红色部分)在这里匹配,然后调boss.bind:

registerTask(new RegisterTask(channel, future, localAddress));

registerTask往taskQueue中投递了一个Runnable任务(代码不贴了),Runnable干了3件事:

channel.socket.socket().bind(localAddress,channel.getConfig().getBacklog());

fireChannelBound(channel, channel.getLocalAddress());

channel.socket.register(selector, SelectionKey.OP_ACCEPT,channel);

 

1. 调Java NIO的接口完成真正的bind操作。

2. 触发了一个channelBound上行事件。

3. 在监听端口的socket上注册了OP_ACCEPT的事件,等待连接上来。

调boss.bind的线程(即用户线程,后面验证)投递runnable到queue,必然有另一个线程poll出来,典型的生产者消费者模式,用queue解耦。再看NioServerBoss,它和NioClientBoss,AbstractNioWorker都继承自AbstractNioSelector,都有自己的taskQueue。不难猜到,poll queue的线程就是构造ServerBootstrap时传入的boss线程池,对应地,假设poll AbstractNioWorker.taskQueue是worker线程池(这两个线程池后面再验证)。

channelBound上行事件类似downStream,在handler中处理,从DefaultChannelPipeline.sendUpstream中可以看到,无head的时候会自动丢弃报文。这里会传给binder,binder没实现,继续向上传。

到这里bind流程结束,接下来看前面遗留的待验证的问题:

1. Netty层和网络层的关系

    Netty层代表对应网络层的抽象,比如ServerSocket抽象成NioServerSocketChannel等,网络层代表真正的网络操作。从网络层到Netty层称为UpStream,反之DownStream。以UpStream为例,表示网络层操作导致了物理状态的变化,然后网络层通过上行事件将状态的转换通知给Netty层。所以上下行事件反映的是这两层之间的状态流转,互为因果。

2. Binder响应channelOpen,如何确定是用户线程触发的fireChannelOpen?

    ServerBootstrap.bindAsync中调了getFactory().newChannel(),即NioServerSocketChannelFactory.newChannel(),其中在new NioServerSocketChannel时直接调了fireChannelOpen,所以是用户线程触发的。(另外一个角度:还没bind,只剩用户线程去触发了)

3. 如何确定poll各自的taskQueue的线程是boss和worker线程?这两个线程池是何时启动的?

    NioServerBoss,AbstractNioWorker和NioClientBoss是AbstractNioSelector的子类,在new子类实例时会先调AbstractNioSelector.openSelector,其中:

DeadLockProofWorker.start(executor, newThreadRenamingRunnable(id, determiner));

这里传入了各自对应的executor,而newThreadRenamingRunnable则将这三个类自身传了进去(AbstractNioSelector实现了Runnable,其run方法中poll了queue),因此是boss或worker线程守在各自的 taskQueue上。

总结:

简单来说,整个流程可以看成是:用户线程调bind---serverSocket.open,fireChannelOpen---上行事件触发binder,binder产生下行事件---最终到serverSocket.bind,以用户线程为起点,先上后下才bind成功。

为什么设计得这么复杂?

个人理解的原因是:

(1) Netty由事件驱动,除了收发的消息,bind和connect等本身也是一种事件,两种事件有必要统一。将socket操作也构造成事件,就能使用统一的处理流程而不会有例外情况需要考虑,且handler链表的机制也更容易扩展代码。

(2) 这样设计更漂亮,理解透了会感觉很舒服,如果分开会觉得别扭(人类的强迫症,就像为啥物理学家一直在研究统一场论一样。。。)

参考资料:

http://ifeve.com/netty-reactor-4

本人辛苦分析、码字,请尊重他人劳动成果,转载不注明出处的诅咒你当一辈子一线搬砖工,嘿嘿~

欢迎讨论、指正~

下篇预告:服务端读写流程分析

猜你喜欢

转载自vinceall.iteye.com/blog/2068877