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

之前看到一篇文章關(guān)于SpringMVC中Request線程安全問題强窖,文中提到每次請(qǐng)求服務(wù)器都會(huì)從線程池中取一個(gè)線程接收處理凸椿,而Request是每個(gè)線程的變量〕崮纾看完后不禁引起我的思考脑漫,Request是從怎樣產(chǎn)生的髓抑,是什么把請(qǐng)求數(shù)據(jù)封裝成Request的呢?帶著問題优幸,開始了我的研究道路吨拍。

Http請(qǐng)求處理流程

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

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

從宏觀角度看問題跛璧, Tomcat接收Http請(qǐng)求過程如下:

Http請(qǐng)求 -> Connector -> Protocol -> Endpoint

NioEndpoint是非阻塞IO,所以對(duì)請(qǐng)求進(jìn)行了Nio處理严里,它被AcceptorPoller(NioEndpoint的內(nèi)部類)追城、Worker分開處理刹碾。Acceptor只負(fù)責(zé)控制連接數(shù)和接收請(qǐng)求,Acceptor請(qǐng)求接收請(qǐng)求后會(huì)通過隊(duì)列(PollerEvent棧)發(fā)送請(qǐng)求給Poller座柱,使用了典型的生產(chǎn)者-消費(fèi)者模式迷帜。在Poller中,維護(hù)了一個(gè)Seletor對(duì)象色洞,

Acceptor戏锹、PollerWorker的工作流程可以總結(jié)如下圖:

Endpoint.png

下面以Tomcat9.0的Nio為例火诸,進(jìn)行分析源碼:

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

為了抓住重點(diǎn)锦针,我們從startInternal( )執(zhí)行完畢開始,此時(shí)Connector置蜀、Protocol奈搜、Endpoint已經(jīng)初始化好實(shí)例。Acceptor和Poller開始監(jiān)聽請(qǐng)求盯荤。

Acceptor

①只要endpoint處于運(yùn)行(running)狀態(tài)馋吗,Acceptor線程會(huì)不斷接受http請(qǐng)求;

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

③Acceptor會(huì)調(diào)用endpoint.serverSocketAccept( )接受請(qǐng)求獲取的SocketChannel。實(shí)際就是通過NioServerSocketChannel.accept( )獲取SocketChannel灼卢。

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

Acceptor.java: 代碼備注與上面序號(hào)對(duì)應(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ū)動(dòng)方式處理socket蛇摸,非阻塞交給Worker的線程池執(zhí)行。這也是NIO模式與BIO模式的最主要區(qū)別灿巧,在并發(fā)量大的場(chǎng)景下可以顯著提升Tomcat的效率赶袄。繼續(xù)上面的代碼分析:

1. 綁定PollerEvent

①Acceptor調(diào)用NioEndpoint.setSocketOptions( ),首先將SocketChannel設(shè)置為非阻塞狀態(tài)抠藕;然后獲取Socket將其封裝成NioChannel饿肺,注冊(cè)到NioEndpoint第一個(gè)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);
        ...
        //復(fù)用NioChannel池中的NioChannel盾似,如果沒有則使用socket新建一個(gè)
        ...
        getPoller0().register(channel); //將NioChannel注冊(cè)到第一個(gè)Poller(實(shí)際最多只能2個(gè))
    } catch (Throwable t) {
        ...
        // Tell to close the socket
        return false;
    }
    return true;
}

②Poller.register( )中會(huì)把NioChannel與當(dāng)前Poller綁定敬辣,并創(chuàng)建一個(gè)NioSocketWrapper賦值給NioChannel。NioSocketWrapper包含著很多重要的管理這次連接的屬性零院,如讀寫超時(shí)時(shí)間等溉跃。然后,Poller會(huì)用NioChannel封裝成PollerEvent告抄,如果eventCache有可復(fù)用則拿出來 reset( ) 沒有就 new 一個(gè)撰茎。

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(); //復(fù)用已用的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會(huì)輪詢通過events( )監(jiān)聽PollerEvent募疮,當(dāng)有新的PollerEvent加入棧,它會(huì)執(zhí)行PollerEvent.run把它消費(fèi)掉炫惩。消費(fèi)過程中會(huì)把NioChannel注冊(cè)到Poller的Selector中,類型為讀阿浓。典型的Nio操作channel.register(selector, SelectionKey.OP_READ)筐钟。

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

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

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

⑤processSocket( )會(huì)復(fù)用或創(chuàng)建一個(gè)SocketProcessor(相當(dāng)于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

大家最初學(xué)習(xí)Nio時(shí),大概都接觸過一個(gè)經(jīng)典的Demo苛聘。下面我們就用它來類比Tomcat接收請(qǐng)求的流程:

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

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

③Tomcat與demo最大不同之處在于设哗,它把a(bǔ)ccept( )抽出來唱捣,用一個(gè)線程接收請(qǐng)求,也就是Acceptor网梢。Acceptor將請(qǐng)求封裝成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 **/

對(duì)比后能發(fā)現(xiàn),Tomcat用Nio處理Socket其實(shí)萬變不離其中战虏,都源于這個(gè)demo拣宰;Tomcat只是將其中的步驟封裝成Acceptor党涕,Poller, Worker分工合作而已巡社。

小結(jié)

本文介紹了Tomcat使用Nio協(xié)議接收Http請(qǐng)求的過程膛堤,通過源碼分析了解Acceptor是如何接收請(qǐng)求,通過生產(chǎn)者-消費(fèi)者模式通知到Poller處理晌该。其中涉及到Nio接收socket的模型肥荔;最后用Nio的經(jīng)典demo與Tomcat進(jìn)行對(duì)比,更加簡(jiǎn)化朝群、深入理解當(dāng)中的原理燕耿。
寫到這里我們已經(jīng)知道Tomcat接收Http請(qǐng)求的實(shí)現(xiàn)原理(接收socket到處理socket),但仍未看見Request姜胖,我們一開始的目標(biāo)仍未實(shí)現(xiàn)誉帅。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市隧出,隨后出現(xiàn)的幾起案子踏志,更是在濱河造成了極大的恐慌,老刑警劉巖胀瞪,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件针余,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凄诞,警方通過查閱死者的電腦和手機(jī)圆雁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帆谍,“玉大人伪朽,你說我怎么就攤上這事⊙打” “怎么了烈涮?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)窖剑。 經(jīng)常有香客問我坚洽,道長(zhǎng),這世上最難降的妖魔是什么西土? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任讶舰,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跳昼。我一直安慰自己般甲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布庐舟。 她就那樣靜靜地躺著欣除,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挪略。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天滔岳,我揣著相機(jī)與錄音杠娱,去河邊找鬼。 笑死谱煤,一個(gè)胖子當(dāng)著我的面吹牛摊求,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刘离,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼室叉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了硫惕?” 一聲冷哼從身側(cè)響起茧痕,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恼除,沒想到半個(gè)月后踪旷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豁辉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年令野,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片徽级。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡气破,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出餐抢,到底是詐尸還是另有隱情现使,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布弹澎,位于F島的核電站朴下,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏苦蒿。R本人自食惡果不足惜殴胧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧团滥,春花似錦竿屹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至力惯,卻和暖如春碗誉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背父晶。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工哮缺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人甲喝。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓尝苇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親埠胖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糠溜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355