Tomcat架构解析之3 Connector NIO
上文簡單記錄了默認的Connector的內部構造及消息流,同時此Connector也是基于BIO的實現。
除BIO,也可以通過配置快速部署NIO的connector。在server.xml中如下配置;
整個Tomcat是一個比較完善的框架體系,各組件間都是基于接口實現,方便擴展
像這里的org.apache.coyote.http11.Http11NioProtocol和BIO的org.apache.coyote.http11.Http11Protocol都是統一的實現org.apache.coyote.ProtocolHandler接口
ProtocolHandler的實現類
從整體結構上來說,NIO還是與BIO的實現保持大體一致
NIO connector的內部結構
還是可以看見Connector中三大件
- Http11NioProtocol
- Mapper
- CoyoteAdapter
基本功能與BIO的類似
重點看看Http11NioProtocol.
和JIoEndpoint一樣,NioEndpoint是Http11NioProtocol中負責接收處理socket的主要模塊
NioEndpoint的主要流程
Acceptor及Worker分別是以線程池形式存在
Poller是一個單線程
注意,與BIO的實現一樣,默認狀態下,在server.xml中
- 沒有配置<Executor>,則以Worker線程池運行
- 配置了<Executor>,則以基于juc 系列的ThreadPoolExecutor線程池運行。
Acceptor
- 接收socket線程,這里雖然是基于NIO的connector,但是在接收socket方面還是傳統的serverSocket.accept()方式,獲得SocketChannel對象
- 然后封裝在一個tomcat的實現類org.apache.tomcat.util.net.NioChannel對象中
- 然后將NioChannel對象封裝在一個PollerEvent對象中,并將PollerEvent對象壓入events queue里。這里是個典型的生產者-消費者模式,Acceptor與Poller線程之間通過queue通信,Acceptor是events queue的生產者,Poller是events queue的消費者。
Poller
Poller線程中維護了一個Selector對象,NIO就是基于Selector來完成邏輯的
在Connector中并不止一個Selector,在Socket的讀寫數據時,為了控制timeout也有一個Selector,在后面的BlockSelector中介紹。可以先把Poller線程中維護的這個Selector標為主Selector
Poller是NIO實現的主要線程。首先作為events queue的消費者,從queue中取出PollerEvent對象,然后將此對象中的channel以OP_READ事件注冊到主Selector中,然后主Selector執行select操作,遍歷出可以讀數據的socket,并從Worker線程池中拿到可用的Worker線程,然后將socket傳遞給Worker。整個過程是典型的NIO實現。
Worker
Worker線程拿到Poller傳過來的socket后,將socket封裝在SocketProcessor對象中。然后從Http11ConnectionHandler中取出Http11NioProcessor對象,從Http11NioProcessor中調用CoyoteAdapter的邏輯,跟BIO實現一樣。在Worker線程中,會完成從socket中讀取http request,解析成HttpServletRequest對象,分派到相應的servlet并完成邏輯,然后將response通過socket發回client。在從socket中讀數據和往socket中寫數據的過程,并沒有像典型的非阻塞的NIO的那樣,注冊OP_READ或OP_WRITE事件到主Selector,而是直接通過socket完成讀寫,這時是阻塞完成的,但是在timeout控制上,使用了NIO的Selector機制,但是這個Selector并不是Poller線程維護的主Selector,而是BlockPoller線程中維護的Selector,稱之為輔Selector。
NioSelectorPool
NioEndpoint對象中維護了一個NioSelecPool對象,這個NioSelectorPool中又維護了一個BlockPoller線程,這個線程就是基于輔Selector進行NIO的邏輯。以執行servlet后,得到response,往socket中寫數據為例,最終寫的過程調用NioBlockingSelector的write方法。
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout,MutableInteger lastWrite) throws IOException { SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); if ( key == null ) throw new IOException("Key no longer registered"); KeyAttachment att = (KeyAttachment) key.attachment(); int written = 0; boolean timedout = false; int keycount = 1; //assume we can write long time = System.currentTimeMillis(); //start the timeout timer try { while ( (!timedout) && buf.hasRemaining()) { if (keycount > 0) { //only write if we were registered for a write //直接往socket中寫數據 int cnt = socket.write(buf); //write the data lastWrite.set(cnt); if (cnt == -1) throw new EOFException(); written += cnt; //寫數據成功,直接進入下一次循環,繼續寫 if (cnt > 0) { time = System.currentTimeMillis(); //reset our timeout timer continue; //we successfully wrote, try again without a selector } } //如果寫數據返回值cnt等于0,通常是網絡不穩定造成的寫數據失敗 try { //開始一個倒數計數器 if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1); //將socket注冊到輔Selector,這里poller就是BlockSelector線程 poller.add(att,SelectionKey.OP_WRITE); //阻塞,直至超時時間喚醒,或者在還沒有達到超時時間,在BlockSelector中喚醒 att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS); }catch (InterruptedException ignore) { Thread.interrupted(); } if ( att.getWriteLatch()!=null && att.getWriteLatch().getCount()> 0) { keycount = 0; }else { //還沒超時就喚醒,說明網絡狀態恢復,繼續下一次循環,完成寫socket keycount = 1; att.resetWriteLatch(); } if (writeTimeout > 0 && (keycount == 0)) timedout = (System.currentTimeMillis() - time) >= writeTimeout; } //while if (timedout) throw new SocketTimeoutException(); } finally { poller.remove(att,SelectionKey.OP_WRITE); if (timedout && key != null) { poller.cancelKey(socket, key); } } return written; }也就是說當socket.write()返回0時,說明網絡狀態不穩定,這時將socket注冊OP_WRITE事件到輔Selector,由BlockPoller線程不斷輪詢這個輔Selector,直到發現這個socket的寫狀態恢復了,通過那個倒數計數器,通知Worker線程繼續寫socket動作。
看一下BlockSelector線程的邏輯;
public void run() { while (run) { try { ...... Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null; while (run && iterator != null && iterator.hasNext()) { SelectionKey sk = (SelectionKey) iterator.next(); KeyAttachment attachment = (KeyAttachment)sk.attachment(); try { attachment.access(); iterator.remove(); ; sk.interestOps(sk.interestOps() & (~sk.readyOps())); if ( sk.isReadable() ) { countDown(attachment.getReadLatch()); } //發現socket可寫狀態恢復,將倒數計數器置位,通知Worker線程繼續 if (sk.isWritable()) { countDown(attachment.getWriteLatch()); } }catch (CancelledKeyException ckx) { if (sk!=null) sk.cancel(); countDown(attachment.getReadLatch()); countDown(attachment.getWriteLatch()); } }//while }catch ( Throwable t ) { log.error("",t); } } events.clear(); try { selector.selectNow();//cancel all remaining keys }catch( Exception ignore ) { if (log.isDebugEnabled())log.debug("",ignore); } }使用這個輔Selector主要是減少線程間的切換,同時還可減輕主Selector的負擔。以上描述了NIO connector工作的主要邏輯,可以看到在設計上還是比較精巧的。NIO connector還有一塊就是Comet,有時間再說吧。需要注意的是,上面從Acceptor開始,有很多對象的封裝,NioChannel及其KeyAttachment,PollerEvent和SocketProcessor對象,這些不是每次都重新生成一個新的,都是NioEndpoint分別維護了它們的對象池;
ConcurrentLinkedQueue<SocketProcessor> processorCache = new ConcurrentLinkedQueue<SocketProcessor>() ConcurrentLinkedQueue<KeyAttachment> keyCache = new ConcurrentLinkedQueue<KeyAttachment>() ConcurrentLinkedQueue<PollerEvent> eventCache = new ConcurrentLinkedQueue<PollerEvent>() ConcurrentLinkedQueue<NioChannel> nioChannels = new ConcurrentLinkedQueue<NioChannel>()當需要這些對象時,分別從它們的對象池獲取,當用完后返回給相應的對象池,這樣可以減少因為創建及GC對象時的性能消耗
總結
以上是生活随笔為你收集整理的Tomcat架构解析之3 Connector NIO的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货|EOS和它引领的POS新时代
- 下一篇: Android及java中list循环添