Netty源码分析2---服务端读写流程

上次分析了服务端bind流程,今天继续看服务端读写流程。

 

术语:worker---NioWorker对象,BT---boss线程,IOT---worker线程,UT---用户线程

 

先说一下前提条件:所有与具体连接相关的IO操作都是由IOT负责完成的,并且handler也是在IOT执行的,所以才说耗时的操作要自己起线程,不要交给IOTIOT不是拿给你独占的。

 

服务端读

① bind并注册OP_ACCEPTselector后,BT一直不停轮询selection key,当有连接上来时acceptnew一个NioAcceptedSocketChannel对象channel,把它传给NioWorker.RegisterTask对象,投递给IOT,让IOT去注册OP_READ到它对应的workerselector上(注册时把channel当做attachment同时注册上去):

 

public void run() {

            SocketAddress localAddress = channel.getLocalAddress();

            SocketAddress remoteAddress = channel.getRemoteAddress();

            if (localAddress == null || remoteAddress == null) {

                if (future != null) {

                    future.setFailure(new ClosedChannelException());

                }

                close(channel, succeededFuture(channel));

                return;

            }

            try {

                if (server) {

                    channel.channel.configureBlocking(false);

                }

                channel.channel.register(

                        selector, channel.getRawInterestOps(), channel);

……

NioWorker内部类RegisterTask.run

 

然后IOT去轮询上面的selectorselection key

 

② 当收到数据时,selector返回selection key,从中可以获取注册时放进去的channel(个人认为attachment可以作为一种保存物理连接和逻辑连接映射关系的手段),然后从channel中读取数据。

 

③ NioWorker.read方法负责具体读数据,主要逻辑是调channel.read读数据,前面是根据预设的sizerecvBufferPool缓存池中获得一个ByteBuffer,以及设置字节顺序等操作。成功读完就清理ByteBuffer,把channelbuffer传给handler,然后fireMessageReceived。通常我们处理request的业务逻辑就放在这里,所以从这儿也可以看出确实是IOT在执行messageReceivedIOT耗不起啊。。。如果读失败则fireExceptionCaught

 

服务端写

写是IOT负责的,发起写数据请求的可能是IOTmessageReceived发起的写就是IOT本身)或者其它线程(如UT

 某线程在Netty层发起写操作,经过之前讲的down stream层层处理、转发后,最终到NioServerSocketPipelineSink.eventSunkàhandleAcceptedSocket,从MessageEvent中获取channel和消息,将消息放入channel对应的writeBufferQueue中,这一步实现了数据的入队操作。

 

 然后继续调worker.writeFromUserCode,其中对当前线程是否为IOT作了判断:

    1) 如果不是则投递一个writeTaskworker.taskQueue上,等IOT下次processTaskQueuepoll出来执行,这一步实现了writeTask的入队操作。

    2) 如果是则直接调write0写数据。

  这样设计的优势:可以避免当前线程是IOT时,投递task带来的线程切换开销,因为当前线程是IOT时,如果直接投递,则只能等下一次IOT获取到CPU进行循环时才能从taskQueue pollwriteTask了,这样的话既有线程切换开销,还会带来延迟,所以判断一次可以优化写的效率。

 

③ 如果是UT投递的任务,会调writeFromTaskLoop,另外还有个writeFromSelectorLoop,是当selectOP_WRITEIOT发起的,用户发起的写操作都是调用writeFromUserCode,这三个write方法最终都调用write0

 

④ write0内部流程:

    1) channel.writeLock加锁,锁住写操作,目的是防止其它线程调cleanUpWriteBuffer,从writeBufferQueuepoll,导致write0 poll不到任务(例如其它线程调channel.close就会去操作writeBufferQueue

    2) 检查channel.currentWriteEvent,若未被清空则说明之前的写操作还未完成,则继续从currentWriteBuffer中获取之前的byteBuf;若已清空则说明上次写操作成功完成,此时则从writeBufferQueuepollbyteBuf,然后将byteBuf包装为sendBuffer

    3) 根据预先配置的writeSpinCount,尝试多次写入数据,类似于自旋,这里作了写优化:当selectOP_WRITE,而在写入时返回0,不一定代表连接被关闭。在该情况下,一般可通过再次注册OP_WRITE等待下次select,但缺点是selectOS发起的系统调用,涉及到用户态和内核态的切换,开销大。所以这里的优化方式是通过自旋多尝试几次,尽量延迟注册。

    4) 发送完后清空currentWriteBuffer等,若未写完(可能kernel buffer满了)则设addOpWritechannel.writeSuspended标记,前者用于再次注册OP_WRITE,等kernel buffer可用时由发起writeFromSelectorLoop,并根据channel是否open确定是addOpWrite or removeOpWrite;后者控制的是用户发起的和taskQueue /selector发起的写操作,同时只能有一个。

    5) 最后根据当前线程是否为IOT,确定fireWriteComplete/fireWriteCompleteLater

注:write时判断若为IOT则直接写的缺点是当前线程若被中断会引起channel关闭,这个还不理解。。。

 

总的来说读写都不复杂,读比写简单,最近时间稍微多一点,简单分析了一下服务端的线程模型,有些自己的理解,也不晓得对不对,改天贴上来大家讨论。

 

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

欢迎讨论、指正~~

 

下篇预告:服务端线程模型分析

猜你喜欢

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