解讀Tomcat(三):請求處理解析Part_1

/**
*作者:annegu
*日期:2009-06-20
*/

在這第三部分裏面我們主要看一下tomcat是如何接收客戶端請求,並把這個請求一層一層的傳遞到子容器中,並最後交到應用程序中進行處理的。

首先,我們來了解一下什麼叫做NIO和BIO。
在前面的解讀tomcat裏面,我們已經說到過了線程池。線程池,顧名思義,裏面存放了一定數量的線程,這些線程用來處理用戶請求。現在我們要討論的NIO和BIO就是如何分配線程池中的線程來處理用戶請求的方式。

BIO(Block IO):阻塞式IO。在tomcat6之前一直都是採用的這種方式。當一個客戶端的連接請求到達的時候,ServerSocket.accept負責接收連接,然後會從線程池中取出一個空閒的線程,在該線程讀取InputStream並解析HTTP協議,然後把輸入流中的內容封裝成Request和Response,在線程中執行這個請求的Servlet ,最後把Response的內容發送到客戶端連接並關閉本次連接。這就完成了一個客戶端的請求過程。我們要注意的是在阻塞式IO中,tomcat是直接從線程池中取出一個線程來處理客戶端請求的,那麼如果這些處理線程在執行網絡操作期間發生了阻塞的話,那麼線程將一直阻塞,導致新的連接一直無法分配到空閒線程,得不到響應。



NIO(Non-blocking IO):tomcat中的非阻塞式IO與阻塞式的不同,它採用了一個主線程來讀取InputStream。也就是說當一個客戶端請求到達的時候,這個主線程會負責從網絡中讀取字節流,把讀入的字節流放入channel中。然後這個主線程就會到線程池中去找有沒有空閒的線程,如果找到了,那麼就會由空閒線程來負責從channel中取出字節,然後解析Http,轉換成request和response,進行處理。當處理線程把要返回給客戶端的內容放在Response之後,處理線程就可以把處理結束的字節流也放入channel中,最後主線程會給這個channel加個標識,表示現在需要操作系統去進行io操作,要把這個channel中的內容返回給客戶端。這樣的話,線程池中的處理線程的任務就集中在如何處理用戶請求上了,而把與網絡有交互的操作都交給主線程去處理。



對於這個非阻塞式IO,anne我想了一個很有趣的比喻,就好像去飯店吃飯點菜一樣。餐館的接待員就好像我們的操作系統,客人來了,他要負責記下客人的點菜,然後傳達給廚房,廚房裏面有好幾位燒菜廚師(處理線程),主廚(主線程)呢比較懶,不下廚,只負責分配廚師的工作。現在來了一個客人,跟接待員說要吃宮寶雞丁,然後接待員就寫了張紙條,上面寫了1號桌客人要點宮寶雞丁,從廚房櫃檯上的一摞盤子裏面拿了一個空的,把點菜單放在盤子裏面。然後主廚就時刻關注這這些盤子,看到有盤子裏面有點菜單的,就從廚房裏面喊一個空閒的廚子說,你來把這菜給燒一下,這個廚子就從這個盤子裏面拿出點菜單去燒菜了,好了這下這個盤子又空了,如果這時候還有客人來,那麼接待員還可以把點菜單放到這個盤子裏面去。等廚師燒好了菜,他就從櫃檯上找一個空盤子,把菜盛在裏面,貼上紙條說這個是1號桌客人點的宮寶雞丁。然後主廚看看,嗯,燒的還不錯,可以上菜了,就在盤子上貼個字條說這盤菜燒好了,上菜去吧。最後接待員就來了,他一直在這候着呢,看到終於有菜可以上了,趕緊端去。嗯,自我感覺挺形象的,你們說呢?

因此,我們可以分析出tomcat中的阻塞式IO與非阻塞式IO的主要區別就是這個主線程。Tomcat6之前的版本都是隻採用的阻塞式IO的方式,服務器接收了客戶端連接之後,馬上分配處理線程來處理這個連接;tomcat6中既提供了阻塞式的IO,也提供了非阻塞式IO處理方式,非阻塞式IO把接收連接的工作都交給主線程處理,處理線程只關心具體的如何處理請求。


好了,我們現在知道了tomcat是採用非阻塞式IO來分配請求的了。那麼接下來我們就可以從發出一個請求開始看看tomcat是怎麼把它傳遞到我們自己的應用程序中的。

程序員最愛看類圖了,所以anne畫了個類圖,我們來照着類圖,一個一個類來看。



我們首先從NioEndPoint開始,這個類是實際處理tcp連接的類,它裏面包括一個操作線程池,socket接收線程(acceptorThread),socket輪詢線程(pollerThread)。

首先我們看到的是start()方法,在這個方法裏面我們可以看到啓動了線程池,acceptorThread和pollerThread。然後,在這個類中還定義了一些子類,包括SocketProcessor,Acceptor,Poller,Worker,NioBufferHandler等等。SocketProcessor,Acceptor,Poller和Worker都實現了Runnable接口。
我想還是按照接收請求的調用順序來講會比較清楚,所以我們從Acceptor開始。

1、Acceptor負責接收socket,一旦得到一個tcp連接,它就會嘗試去從nioChannels中去取出一個空閒的nioChannel,然後把這個連接的socket交給它,接着它會告訴輪詢線程poller,我這裏有個channel已經準備好了,你注意着點,可能不久之後就要有數據過來啦。下面的事情它就不管了,接着等待下一個tcp連接的到來。
我們可以看一下它是怎麼把socket交給channel的:

Java代碼  收藏代碼
  1.    protected boolean setSocketOptions(SocketChannel socket) {  
  2.        try {  
  3.            ... //ignore  
  4.         //從nioChannels中取出一個channel  
  5.            NioChannel channel = nioChannels.poll();  
  6.         //若沒有可用的channel,根據不同情況新建一個channel  
  7.            if ( channel == null ) {  
  8.                if (sslContext != null) {  
  9.                    ...//ignore  
  10.                    channel = new SecureNioChannel(...);  
  11.                } else {  
  12.                    ...// normal tcp setup  
  13.                    channel = new NioChannel(...);  
  14.                }  
  15.            } else {                  
  16.                channel.setIOChannel(socket);  
  17.            ...//根據channel的類型做相應reset  
  18.            }  
  19.            getPoller0().register(channel); // 把channel交給poller  
  20.        } catch (Throwable t) {  
  21.            try {              
  22.            return false;   // 返回false,關閉socket  
  23.        }  
  24.        return true;  
  25. }  


要說明的是,Acceptor這個類在BIO的endpoint類中也是存在的。對於BIO來說acceptor就是用來接收請求,然後給這個請求分配一個空閒的線程來處理它,所以是起到了一個連接請求與處理線程的作用。現在在NIO中,我們可以看到Acceptor.run()裏面是把processSocket(socket);給註釋掉了(processSocket這個方法就是分配線程來處理socket的方法,這個anne打算在後面講)。

2、Poller這個類其實就是我們在前面說到的nio的主線程。它裏面也有一個run()方法,在這裏我們就會輪詢channel啦。看下面的代碼:

Java代碼  收藏代碼
  1. Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;  
  2. while (iterator != null && iterator.hasNext()) {  
  3.             SelectionKey sk = (SelectionKey) iterator.next();  
  4.          KeyAttachment attachment = (KeyAttachment)sk.attachment();  
  5.          attachment.access();  
  6.          iterator.remove();  
  7.          processKey(sk, attachment);  
  8.  }  


我們可以看到,程序遍歷了所有selectedKeys,這個SelectionKey就是一種可以用來讀取channel的鑰匙。這個KeyAttachment又是個什麼類型的對象呢?其實它記錄了包括channel信息在內的又與這個channel息息相關的一些附加信息。MS很長的一句話,這麼說吧,它裏面有channel對象,還有lastAccess(最近一次訪問時間),error(錯誤信息),sendfileData(發送的文件數據)等等。然後在processKey這個方法裏面我們就可以把channel裏面的字節流交給處理線程去處理了。
然後我們來看一下這個processKey方法:

Java代碼  收藏代碼
  1. protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {  
  2.             boolean result = true;  
  3.             try {  
  4.                 if ( close ) {  
  5.                     cancelledKey(sk, SocketStatus.STOP, false);  
  6.                 } else if ( sk.isValid() && attachment != null ) {  
  7.                     attachment.access();  
  8.                     sk.attach(attachment);  
  9.                     NioChannel channel = attachment.getChannel();  
  10.             ①        if (sk.isReadable() || sk.isWritable() ) {  
  11.             ②            if ( attachment.getSendfileData() != null ) {  
  12.                             processSendfile(sk,attachment,true);  
  13.                         } else if ( attachment.getComet() ) {  
  14.                             if ( isWorkerAvailable() ) {  
  15.                                 reg(sk, attachment, 0);  
  16.                                 if (sk.isReadable()) {  
  17.                                     if (!processSocket(channel, SocketStatus.OPEN))  
  18.                                         processSocket(channel, SocketStatus.DISCONNECT);  
  19.                                 } else {  
  20.                                     if (!processSocket(channel, SocketStatus.OPEN))  
  21.                                         processSocket(channel, SocketStatus.DISCONNECT);  
  22.                                 }  
  23.                             } else {  
  24.                                 result = false;  
  25.                             }  
  26.                         } else {  
  27.                             if ( isWorkerAvailable() ) {  
  28.                                 unreg(sk, attachment,sk.readyOps());  
  29.      ③                           boolean close = (!processSocket(channel));  
  30.                                 if (close) {  
  31.                                     cancelledKey(sk,SocketStatus.DISCONNECT,false);  
  32.                                 }  
  33.                             } else {  
  34.                                 result = false;  
  35.                             }  
  36.                         }                      }  
  37.                     }   
  38.                 } else {  
  39.                     //invalid key  
  40.                     cancelledKey(sk, SocketStatus.ERROR,false);  
  41.                 }  
  42.             } catch ( CancelledKeyException ckx ) {  
  43.                 cancelledKey(sk, SocketStatus.ERROR,false);  
  44.             } catch (Throwable t) {  
  45.                 log.error("",t);  
  46.             }  
  47.             return result;  
  48. }  


首先是判斷一下這個selection key是否可用,沒有超時,然後從sk中取出channel備用。然後看一下這個sk的狀態是否是可讀的,或者可寫的,代碼①處。代碼②處是返回階段,要往客戶端寫數據時候的路徑,程序會判斷是否有要發送的數據,這部分我們後面再看,先往下看request進來的情況。然後我們就可以在下面看到開始進行真正的處理socket的工作了,代碼③處,進入processSocket()方法了。

Java代碼  收藏代碼
  1. protected boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {  
  2.     try {  
  3.         KeyAttachment attachment = (KeyAttachment)socket.getAttachment(false);  
  4.         attachment.setCometNotify(false);   
  5.         if (executor == null) {  
  6.        ④     getWorkerThread().assign(socket, status);  
  7.         } else {  
  8.             SocketProcessor sc = processorCache.poll();  
  9.             if ( sc == null ) sc = new SocketProcessor(socket,status);  
  10.             else sc.reset(socket,status);  
  11.             if ( dispatch ) executor.execute(sc);  
  12.             else sc.run();  
  13.         }  
  14.     } catch (Throwable t) {  
  15.         return false;  
  16.     }  
  17.     return true;  


從④處可以看到明顯是取了線程池中的一個線程來操作這個channel,也就是說在這個方法裏面我們就開始進入線程池了。那麼executor呢?executor可以算是一個配置項,如果使用了executor,那麼線程池就使用java自帶的線程池,如果不使用executor的話,就使用tomcat的線程池WorkerStack,這個WrokerStack我在後面有專門寫它,現在先跳過。我們可以看到在start()方法裏面,是這樣寫的:

Java代碼  收藏代碼
  1.       if (getUseExecutor()) {  
  2.            if ( executor == null ) {  
  3.                executor = new ThreadPoolExecutor(...);  
  4.            }  
  5.        } else if ( executor == null ) {  
  6.            workers = new WorkerStack(maxThreads);  
  7. }  


好了,現在回到processSocket(),我們先來看有executor的情況,就是使用java自己的線程池。首先從processorCache中取出一個線程socketProcessor,然後把channel交給這個線程,啓動線程的run方法。於是我們終於脫離主線程,進入了SocketProcessor的run方法啦!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章