1. 程式人生 > 程式設計 >Tomcat原理系列之七:詳解socket如何封裝成request(下)

Tomcat原理系列之七:詳解socket如何封裝成request(下)

@TOC

推薦閱讀Tomcat原理系列之二:由點到線,請求主幹對於理解本文有很多幫助。

Tomcat版本8.

1. 接收連線:

Accptor在接受到socket請求後,執行setSocketOptions方法對socket進行初步的封裝。 封裝: 首先建立一個SocketBufferHandler用於socket輸入輸出的緩衝(SocketBuffer)。將SocketBufferHandler與socket一同封裝成NioChannel.

public SocketBufferHandler(int readBufferSize,int writeBufferSize,boolean
direct)
{ this.direct = direct; if (direct) { readBuffer = ByteBuffer.allocateDirect(readBufferSize);//預設8k writeBuffer = ByteBuffer.allocateDirect(writeBufferSize); } else { readBuffer = ByteBuffer.allocate(readBufferSize); writeBuffer = ByteBuffer.allocate(writeBufferSize); } } 複製程式碼

2. 註冊:

呼叫Poller.register()將NioChannel(socket)先進一步封裝成NioSocketWrapper類,再封裝成PollerEvent然後註冊到Poller的events佇列中去。

3. 消費:

Poller.run()消費佇列的PollerEvent事件。將PollerEvent中準備就緒的socketChannel註冊到Selector。

4. 處理請求:

Poler.run() 從Selector選擇處就緒的Channel。呼叫NioEndpoint.processKey(),processKey()方法中,根據讀寫事件呼叫processSocket()處理。

5. Worker執行緒:

processSocket()會根據(NioSocketWrapper)socket建立一個SocketProcessor處理器。SocketProcessor本身實現了Runnable介面。可以作為任務。被Endpoint的Executor執行緒池執行。

try {
            if (socketWrapper == null) {
                return false;
            }
            SocketProcessorBase<S> sc = processorCache.pop();
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper,event);
            } else {
                sc.reset(socketWrapper,event);
            }
            Executor executor = getExecutor();//執行緒池
            if (dispatch && executor != null) {
                executor.execute(sc);
            } else {
                sc.run();
            }
        } catch (RejectedExecutionException ree) {
複製程式碼

SocketProcessor在連線握手成功的情況下,呼叫ConnectionHandler.process()方法開始socket內容的讀取

6. HTTP1.1協議處理器初始化:

ConnectionHandler.process()方法會建立Http11Processor處理器用於http協議的處理. Http11Processor構造方法主要做了,

  • 首先會建立一對org.apache.coyote.Request和org.apache.coyote.Response內部coyoteRequest與coyoteResponse物件.
  • 並建立Http11InputBuffer與Http11OutputBuffer用於coyoteRequest與coyoteResponse。Http11InputBuffer提供HTTP請求頭的解析與編碼功能。Http11InputBuffer在建立的時候會指定headerBufferSize的大小.預設也是8k.

7. Http11Processor.service()[HTTP協議頭部的解析]:

拿到Http11Processor後.執行核心方法service(); 第一步:初始化讀寫緩衝區

 		// Setting up the I/O
        setSocketWrapper(socketWrapper);
        inputBuffer.init(socketWrapper);
        outputBuffer.init(socketWrapper);
複製程式碼

init()方法為Http11InputBuffer內部建立一個讀緩衝區byteBuffer.大小為headerBufferSize+socketbuffer的大小.也就是預設是2*8k

 void init(SocketWrapperBase<?> socketWrapper) {

        wrapper = socketWrapper;
        wrapper.setAppReadBufHandler(this);

        int bufLength = headerBufferSize +
                wrapper.getSocketBufferHandler().getReadBuffer().capacity();
        if (byteBuffer == null || byteBuffer.capacity() < bufLength) {
            byteBuffer = ByteBuffer.allocate(bufLength);
            byteBuffer.position(0).limit(0);
        }
    }
複製程式碼

第二步開始請求行的解析 在解析之前我先來看看HTTP請求報文格式.

在這裡插入圖片描述
在這裡插入圖片描述
inputBuffer.parseRequestLine()方法用來讀取請求行。inputBuffer中有個parsingRequestLinePhase屬性值.parsingRequestLinePhase值不同代表讀取請求行的不同位置.

  • 0:表示解析開始前跳過空行
  • 2: 開始解析請求方法: POST
  • 3: 跳過請求方法和請求uri之間的空格或製表符
  • 4: 開始解析請求URI: chapter17/user.html
  • 5:跳過請求URI與版本之間的空格
  • 6:解析協議版本: HTTP/1.1

parseRequestLine()方法, 每讀一個位置時,都會判斷inputBuffer.bytebuffer中是否讀取完畢。position = limit 即已經讀完了,需要執行fill重新填充,引數是false表示非阻塞讀(那什麼時候阻塞讀呢,是我們在呼叫getInputStream()時,是阻塞的)

// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {//判斷
   if (keptAlive) {
       // Haven't read any request data yet so use the keep-alive
       // timeout.
       wrapper.setReadTimeout(wrapper.getEndpoint().getKeepAliveTimeout());
   }
   if (!fill(false)) {//填充
       // A read is pending,so no longer in initial state
       parsingRequestLinePhase = 1;
       return false;
   }
   // At least one byte of the request has been received.
   // Switch to the socket timeout.
   wrapper.setReadTimeout(wrapper.getEndpoint().getConnectionTimeout());
}
複製程式碼

fill()填充方法:填充buffer fill()的填充功能是通過呼叫socket的包裝類NioSocketWrapper.read()方法實現的. 在read()方法中 首先會嘗試從socketBufferHandler.readbuffer讀,

  • 如果socketBufferHandler.readbuffer有資料,把資料填充到inputBuffer.bytebuffer中。不需要從socket通道讀取。
  • 如果socketBufferHandler.readbuffer沒有資料可讀,且inputBuffer.bytebuffer的可寫空間大於socketBufferHandler.readbuffer的容量: 則直接從socket通道中讀取。設定該次讀取的最大值limit,為socket buffer的大小
  • 如果socketBufferHandler.readbuffer沒有資料可讀,且inputBuffer.bytebuffer的可寫空間小於socketBufferHandler.readbuffer的容量:則先從socket通道讀入socketBuffer(因為此時socketBuffer的容量大於inputBuffer.bytebuffer的可寫空間,可以一次從OS讀取更多資料)。然後再從socketBuffer填充到inputBuffer.bytebuffer.(此時填充的是剩餘可寫空間,這樣socketBuffer也會剩餘一些,當inputBuffer.bytebuffer讀取完畢時,再呼叫fill()方法時,將剩餘socketBuffer的資料填充到inputBuffer.bytebuffer,不需要去socket通道內讀,本質上時減少OSread.然後這樣迴圈執行下去,直到所有的讀操作完成)
@Override
        public int read(boolean block,ByteBuffer to) throws IOException {
        	//先從tomcat 底層socket buffer 緩衝區讀,如果buffer緩衝區還有未讀的buffer,則不需要到OS底層讀緩衝區讀
            int nRead = populateReadBuffer(to);
            if (nRead > 0) {
                return nRead;
                /*
                 * Since more bytes may have arrived since the buffer was last
                 * filled,it is an option at this point to perform a
                 * non-blocking read. However correctly handling the case if
                 * that read returns end of stream adds complexity. Therefore,* at the moment,the preference is for simplicity.
                 */
            }

            // The socket read buffer capacity is socket.appReadBufSize
            int limit = socketBufferHandler.getReadBuffer().capacity();
            if (to.remaining() >= limit) {
                to.limit(to.position() + limit);
                nRead = fillReadBuffer(block,to);
                if (log.isDebugEnabled()) {
                    log.debug("Socket: [" + this + "],Read direct from socket: [" + nRead + "]");
                }
                updateLastRead();
            } else {
                // Fill the read buffer as best we can.
                nRead = fillReadBuffer(block);
                if (log.isDebugEnabled()) {
                    log.debug("Socket: [" + this + "],Read into buffer: [" + nRead + "]");
                }
                updateLastRead();

                // Fill as much of the remaining byte array as possible with the
                // data that was just read
                if (nRead > 0) {
                    nRead = populateReadBuffer(to);
                }
            }
            return nRead;
        }
複製程式碼

read()呼叫fillReadBuffer()方法來完成從socket通道內讀資料。fillReadBuffer有兩種讀模式阻塞讀和非阻塞讀.非阻塞讀會呼叫socket的初始包裝類NioChannel.read()方法,NioChannel.read()呼叫SocketChannel.read()此處是真正從通道里讀資料.

總結起來說。填充功能其實從socket通道把資料讀到inputBuffer.byteBuffer中

解析:inputBuffer從byteBuffer中解析報文內容.例如請求方法,請求URI。inputBuffer並沒有把位元組轉義。而是使用byte[]陣列的包裝類MessageBytes來表示請求行的各部分,在需要的時候進行轉移並緩衝。

我們以請求方法讀取為例:

		if (parsingRequestLinePhase == 2) {
            //
            // Reading the method name
            // Method name is a token
            //
            boolean space = false;
            while (!space) {
                // Read new bytes if needed
                if (byteBuffer.position() >= byteBuffer.limit()) {
                    if (!fill(false)) // request line parsing
                        return false;
                }
                // Spec says method name is a token followed by a single SP but
                // also be tolerant of multiple SP and/or HT.
                int pos = byteBuffer.position();
                byte chr = byteBuffer.get();
                if (chr == Constants.SP || chr == Constants.HT) {
                    space = true;
                    //請求的方法(get/post)
                    request.method().setBytes(byteBuffer.array(),parsingRequestLineStart,pos - parsingRequestLineStart);
                } else if (!HttpParser.isToken(chr)) {
                    byteBuffer.position(byteBuffer.position() - 1);
                    throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                }
            }
複製程式碼

看程式碼段,request.method().setBytes並沒有把請求報文的請求方法轉義為GET/POST字元,而是使用MessageBytes儲存了請求報文(即inputBuffer.byteBuffer)起始位到第一個空格之前的位元組陣列的下標。 在使用的時候將位元組轉為GET/POST

第三步就是讀取請求頭inputBuffer.parseHeaders():過程類似讀取請求行

第四步讀取請求頭後會執行prepareRequest():此方法設定request的filters和一些資訊的設定。

第五步呼叫Adapter.service(request,response):將tomcat的內部coyoteRequest和coyoteReponse轉換為servlet規範request ,response物件。這裡有個一轉換的過程。 就是建立servlet規範request ,response物件。然後將coyoteRequest,coyoteReponse分別設定給request,response 接下來就是呼叫各級容器,走過filter到達servlet中

 // Calling the container
 connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request,response);
複製程式碼

8. HTTP協議body的解析

HTTP協議請求body的解析延遲到servlet中在獲取引數的時候解析的。body的解析放到其他章節在講。

總結下:

資料從連線通道copy到堆外記憶體,然後從堆外記憶體copy到 tomcat Http11InputBuffer的堆內byteBuffer。然後根據HTTP協議解析byteBuffer中的位元組陣列。變成HTTP協議的coyoteRequest,coyoteReponse。最後包裝成我們常用的request,response物件。

重用:

Tomcat中有很多重用的元件.以減少頻繁建立和銷燬的開銷

  • NioChannel:NioChannel channel = nioChannels.pop();
  • PollerEvent: PollerEvent r = eventCache.pop();
  • SocketProcessor:SocketProcessorBase sc = processorCache.pop();
  • Processor:Processor processor = connections.get(socket);