上次分析了服务端bind流程,今天继续看服务端读写流程。
术语:worker---NioWorker对象,BT---boss线程,IOT---worker线程,UT---用户线程
先说一下前提条件:所有与具体连接相关的IO操作都是由IOT负责完成的,并且handler也是在IOT执行的,所以才说耗时的操作要自己起线程,不要交给IOT,IOT不是拿给你独占的。
一. 服务端读
① bind并注册OP_ACCEPT到selector后,BT一直不停轮询selection key,当有连接上来时accept,new一个NioAcceptedSocketChannel对象channel,把它传给NioWorker.RegisterTask对象,投递给IOT,让IOT去注册OP_READ到它对应的worker的selector上(注册时把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去轮询上面的selector的selection key。
② 当收到数据时,selector返回selection key,从中可以获取注册时放进去的channel(个人认为attachment可以作为一种保存物理连接和逻辑连接映射关系的手段),然后从channel中读取数据。
③ NioWorker.read方法负责具体读数据,主要逻辑是调channel.read读数据,前面是根据预设的size从recvBufferPool缓存池中获得一个ByteBuffer,以及设置字节顺序等操作。成功读完就清理ByteBuffer,把channel和buffer传给handler,然后fireMessageReceived。通常我们处理request的业务逻辑就放在这里,所以从这儿也可以看出确实是IOT在执行messageReceived,IOT耗不起啊。。。如果读失败则fireExceptionCaught。
二. 服务端写
写是IOT负责的,发起写数据请求的可能是IOT(messageReceived发起的写就是IOT本身)或者其它线程(如UT)
① 某线程在Netty层发起写操作,经过之前讲的down stream层层处理、转发后,最终到NioServerSocketPipelineSink.eventSunkàhandleAcceptedSocket,从MessageEvent中获取channel和消息,将消息放入channel对应的writeBufferQueue中,这一步实现了数据的入队操作。
② 然后继续调worker.writeFromUserCode,其中对当前线程是否为IOT作了判断:
1) 如果不是则投递一个writeTask到worker.taskQueue上,等IOT下次processTaskQueue时poll出来执行,这一步实现了writeTask的入队操作。
2) 如果是则直接调write0写数据。
这样设计的优势:可以避免当前线程是IOT时,投递task带来的线程切换开销,因为当前线程是IOT时,如果直接投递,则只能等下一次IOT获取到CPU进行循环时才能从taskQueue poll出writeTask了,这样的话既有线程切换开销,还会带来延迟,所以判断一次可以优化写的效率。
③ 如果是UT投递的任务,会调writeFromTaskLoop,另外还有个writeFromSelectorLoop,是当select到OP_WRITE时IOT发起的,用户发起的写操作都是调用writeFromUserCode,这三个write方法最终都调用write0。
④ write0内部流程:
1) 对channel.writeLock加锁,锁住写操作,目的是防止其它线程调cleanUpWriteBuffer,从writeBufferQueue中poll,导致write0 poll不到任务(例如其它线程调channel.close就会去操作writeBufferQueue)
2) 检查channel.currentWriteEvent,若未被清空则说明之前的写操作还未完成,则继续从currentWriteBuffer中获取之前的byteBuf;若已清空则说明上次写操作成功完成,此时则从writeBufferQueue中poll出byteBuf,然后将byteBuf包装为sendBuffer。
3) 根据预先配置的writeSpinCount,尝试多次写入数据,类似于自旋,这里作了写优化:当select到OP_WRITE,而在写入时返回0,不一定代表连接被关闭。在该情况下,一般可通过再次注册OP_WRITE等待下次select,但缺点是select是OS发起的系统调用,涉及到用户态和内核态的切换,开销大。所以这里的优化方式是通过自旋多尝试几次,尽量延迟注册。
4) 发送完后清空currentWriteBuffer等,若未写完(可能kernel buffer满了)则设addOpWrite和channel.writeSuspended标记,前者用于再次注册OP_WRITE,等kernel buffer可用时由发起writeFromSelectorLoop,并根据channel是否open确定是addOpWrite or removeOpWrite;后者控制的是用户发起的和taskQueue /selector发起的写操作,同时只能有一个。
5) 最后根据当前线程是否为IOT,确定fireWriteComplete/fireWriteCompleteLater。
注:write时判断若为IOT则直接写的缺点是当前线程若被中断会引起channel关闭,这个还不理解。。。
总的来说读写都不复杂,读比写简单,最近时间稍微多一点,简单分析了一下服务端的线程模型,有些自己的理解,也不晓得对不对,改天贴上来大家讨论。
本人辛苦分析、码字,请尊重他人劳动成果,转载不注明出处的诅咒你当一辈子一线搬砖工,嘿嘿~
欢迎讨论、指正~~
下篇预告:服务端线程模型分析