Http請求是如何轉(zhuǎn)化成Request的(一)

之前看到一篇文章關(guān)于SpringMVC中Request線程安全問題,文中提到每次請求服務(wù)器都會從線程池中取一個線程接收處理,而Request是每個線程的變量??赐旰蟛唤鹞业乃伎迹琑equest是從怎樣產(chǎn)生的,是什么把請求數(shù)據(jù)封裝成Request的呢?帶著問題,開始了我的研究道路。

Http請求處理流程

從本質(zhì)來說,Http請求其實是客戶端與服務(wù)器建立socket進行數(shù)據(jù)通訊。

為什么我會這么說,希望看完這篇文章你能心領(lǐng)神會。

從宏觀角度看問題, Tomcat接收Http請求過程如下:

Http請求 -> Connector -> Protocol -> Endpoint

NioEndpoint是非阻塞IO,所以對請求進行了Nio處理,它被Acceptor、Poller(NioEndpoint的內(nèi)部類)、Worker分開處理。Acceptor只負責控制連接數(shù)和接收請求,Acceptor請求接收請求后會通過隊列(PollerEvent棧)發(fā)送請求給Poller,使用了典型的生產(chǎn)者-消費者模式。在Poller中,維護了一個Seletor對象,

Acceptor、Poller、Worker的工作流程可以總結(jié)如下圖:

Endpoint.png

下面以Tomcat9.0的Nio為例,進行分析源碼:

Connector的生命周期:構(gòu)造器 -> initInternal( ) -> startInternal( ) -> stopInternal( )

為了抓住重點,我們從startInternal( )執(zhí)行完畢開始,此時Connector、Protocol、Endpoint已經(jīng)初始化好實例。Acceptor和Poller開始監(jiān)聽請求。

Acceptor

①只要endpoint處于運行(running)狀態(tài),Acceptor線程會不斷接受http請求;

②如果當前endpoint連接數(shù)大于最大連接數(shù)(maxConnections)事,它會阻塞等待至有空閑連接后繼續(xù)輪詢。

③Acceptor會調(diào)用endpoint.serverSocketAccept( )接受請求獲取的SocketChannel。實際就是通過NioServerSocketChannel.accept( )獲取SocketChannel。

④隨后會把獲取的NioChannel綁定一個PollerEvent加入到Poller的PollerEvent棧中(見NioEndpoint.java)

Acceptor.java: 代碼備注與上面序號對應(yīng)

public void run() {
    int errorDelay = 0;
    // Loop until we receive a shutdown command
    while (endpoint.isRunning()) { //①
       ...
        try {
            //if we have reached max connections, wait
            endpoint.countUpOrAwaitConnection();  //②
            // Endpoint might have been paused while waiting for latch
            // If that is the case, don't accept new connections
            if (endpoint.isPaused()) {
                continue;
            }
            U socket = null; //Http11NioProtocol中的U是SocketChannel
            try {
                // Accept the next incoming connection from the server
                // socket
                socket = endpoint.serverSocketAccept(); //③
            } catch (Exception ioe) {
              ...
            }
            // Successful accept, reset the error delay
            errorDelay = 0;
            // Configure the socket
            if (endpoint.isRunning() && !endpoint.isPaused()) {
                // setSocketOptions() will hand the socket off to
                // an appropriate processor if successful
                if (!endpoint.setSocketOptions(socket)) {
                    endpoint.closeSocket(socket); //④
                }
            }
           ...
    }
    state = AcceptorState.ENDED;
}

Poller

Poller是NioEndpoint的內(nèi)部類,是Nio協(xié)議與其他協(xié)議不同的特殊處理類,也是關(guān)鍵類。它使用事件驅(qū)動方式處理socket,非阻塞交給Worker的線程池執(zhí)行。這也是NIO模式與BIO模式的最主要區(qū)別,在并發(fā)量大的場景下可以顯著提升Tomcat的效率。繼續(xù)上面的代碼分析:

1. 綁定PollerEvent

①Acceptor調(diào)用NioEndpoint.setSocketOptions( ),首先將SocketChannel設(shè)置為非阻塞狀態(tài);然后獲取Socket將其封裝成NioChannel,注冊到NioEndpoint第一個Poller。

NioEndpoint.java:

@Override
protected boolean setSocketOptions(SocketChannel socket) {
    // Process the connection
    try {
        //disable blocking, APR style, we are gonna be polling it
        socket.configureBlocking(false); //設(shè)置為非阻塞
        Socket sock = socket.socket();
        socketProperties.setProperties(sock);
        ...
        //復用NioChannel池中的NioChannel,如果沒有則使用socket新建一個
        ...
        getPoller0().register(channel); //將NioChannel注冊到第一個Poller(實際最多只能2個)
    } catch (Throwable t) {
        ...
        // Tell to close the socket
        return false;
    }
    return true;
}

②Poller.register( )中會把NioChannel與當前Poller綁定,并創(chuàng)建一個NioSocketWrapper賦值給NioChannel。NioSocketWrapper包含著很多重要的管理這次連接的屬性,如讀寫超時時間等。然后,Poller會用NioChannel封裝成PollerEvent,如果eventCache有可復用則拿出來 reset( ) 沒有就 new 一個。

NioEndpoint.Poller.java:

public class Poller implements Runnable {

    private Selector selector;
    private final SynchronizedQueue<PollerEvent> events =
            new SynchronizedQueue<>();
    ...
    public void register(final NioChannel socket) {
            socket.setPoller(this);
            NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
            socket.setSocketWrapper(ka);
            ka.setPoller(this);
            ka.setReadTimeout(getConnectionTimeout());
            ka.setWriteTimeout(getConnectionTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());
            PollerEvent r = eventCache.pop(); //復用已用的PollerEvent
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);
            addEvent(r);
        }
    private void addEvent(PollerEvent event) {
            events.offer(event); //添加PollerEvent到棧,給Poller輪詢調(diào)用
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }
}

至此,Acceptor的工作已完成,可以去接收新的連接。接下來的工作由Poller完成

2. 處理PollerEvent與Socket

①Poller會輪詢通過events( )監(jiān)聽PollerEvent,當有新的PollerEvent加入棧,它會執(zhí)行PollerEvent.run把它消費掉。消費過程中會把NioChannel注冊到Poller的Selector中,類型為讀。典型的Nio操作channel.register(selector, SelectionKey.OP_READ)

②SocketChannel事件注冊好了,自然會觸發(fā)阻塞等待的selector.select(selectorTimeout)

③接下來就是Nio的操作了。遍歷selectedKeys獲取SelectionKey逐個處理。這里Poller交給了processKey( )

④processSocket中會根據(jù)SelectionKey的讀寫類型執(zhí)行processSocket( )

⑤processSocket( )會復用或創(chuàng)建一個SocketProcessor(相當于Worker)使用線程池執(zhí)行SocketChannel

NioEndpoint.Poller.java:

@Override
public void run() {
    // Loop until destroy() is called
    while (true) {
        boolean hasEvents = false;
        try {
            if (!close) {
                hasEvents = events(); //   ①
                if (wakeupCounter.getAndSet(-1) > 0) {
                    //if we are here, means we have other stuff to do
                    //do a non blocking select
                    keyCount = selector.selectNow();
                } else {
                    keyCount = selector.select(selectorTimeout); //  ②
                }
                wakeupCounter.set(0);
            }
            ....
        //either we timed out or we woke up, process events first
        if ( keyCount == 0 ) hasEvents = (hasEvents | events());
        Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
        // Walk through the collection of ready keys and dispatch
        // any active event.
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
            // Attachment may be null if another thread has called
            // cancelledKey()
            if (attachment == null) {
                iterator.remove();
            } else {
                iterator.remove();
                processKey(sk, attachment); //  ③
            }
        }//while
        //process timeouts
        timeout(keyCount,hasEvents);
    }//while
    getStopLatch().countDown();
}

protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
        ...
  if ( sk.isValid() && attachment != null ) {
        ...
        if (sk.isReadable()) {
            if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) { //  ④
                closeSocket = true;
            }
        }
        if (!closeSocket && sk.isWritable()) {
            if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) { //  ④
                closeSocket = true;
            }
    ...
}

//AbstractEndpoint.java
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
                             SocketEvent event, boolean dispatch) {
    try {
        ...
        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();
        }
        ...
    }
}

至此,Poller的工作已完成,可以去接收新的連接。接下來的工作由Worker完成

類比Nio Demo

大家最初學習Nio時,大概都接觸過一個經(jīng)典的Demo。下面我們就用它來類比Tomcat接收請求的流程:

①對應(yīng)的是NioEndpoint.bind()->initServerSocket()它是在NioPoint初始化時執(zhí)行的

②對應(yīng)的是Poller.run( )輪詢監(jiān)聽selector。得到SelectionKey后根據(jù)類型執(zhí)行對應(yīng)的操作,即執(zhí)行Poller.processKey( )

③Tomcat與demo最大不同之處在于,它把accept( )抽出來,用一個線程接收請求,也就是Acceptor。Acceptor將請求封裝成PollerEvent丟給Poller處理。

/** ① Begin **/
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
/** ① End **/
/** ② Begin **/
while(true) {
  int readyChannels = selector.selectNow();
  if(readyChannels == 0) continue;
  Set<SelectionKey> selectedKeys = selector.selectedKeys();
  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        /**endpoint.serverSocketAccept()**/
        accept(selectionKey);
        /** End **/
        channel.register(selector, SelectionKey.OP_READ); //endpoint.setSocketOptions(socket)
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
  }
  /** ② End **/
}
/** ③ Begin **/
private void accept(SelectionKey selectionKey) throws IOException {
        ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
        SocketChannel channel = ssc.accept(); //endpoint.serverSocketAccept()
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_READ); //endpoint.setSocketOptions(socket)
}
/** ③ End **/

對比后能發(fā)現(xiàn),Tomcat用Nio處理Socket其實萬變不離其中,都源于這個demo;Tomcat只是將其中的步驟封裝成Acceptor,Poller, Worker分工合作而已。

小結(jié)

本文介紹了Tomcat使用Nio協(xié)議接收Http請求的過程,通過源碼分析了解Acceptor是如何接收請求,通過生產(chǎn)者-消費者模式通知到Poller處理。其中涉及到Nio接收socket的模型;最后用Nio的經(jīng)典demo與Tomcat進行對比,更加簡化、深入理解當中的原理。
寫到這里我們已經(jīng)知道Tomcat接收Http請求的實現(xiàn)原理(接收socket到處理socket),但仍未看見Request,我們一開始的目標仍未實現(xiàn)。

想知道Work是如何將socket一步步處理轉(zhuǎn)化成servlet的Request。由于篇幅有限,欲知后事如何請關(guān)注Http請求是如何轉(zhuǎn)化成Request的(二)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容