經過源碼解析 Node.js 中一個 HTTP 請求到響應的歷程

若是你們使用 Node.js 寫過 web 應用,那麼你必定使用過 http 模塊。在 Node.js 中,起一個 HTTP server 十分簡單,短短數行便可:node

'use stirct'
const { createServer } = require('http')

createServer(function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' })
  res.end('Hello World\n')
})
.listen(3000, function () { console.log('Listening on port 3000') })
$ curl localhost:3000
Hello World

就這麼簡單,由於 Node.js 把許多細節都已在源碼中封裝好了,主要代碼在 lib/_http_*.js 這些文件中,如今就讓咱們照着上述代碼,看看從一個 HTTP 請求的到來直到響應,Node.js 都爲咱們在源碼層作了些什麼。git

HTTP 請求的來到

在 Node.js 中,若要收到一個 HTTP 請求,首先須要建立一個 http.Server 類的實例,而後監聽它的 request 事件。因爲 HTTP 協議屬於應用層,在下層的傳輸層一般使用的是 TCP 協議,因此 net.Server 類正是 http.Server 類的父類。具體的 HTTP 相關的部分,是經過監聽 net.Server 類實例的 connection 事件封裝的:github

// lib/_http_server.js
// ...

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.addListener('request', requestListener);
  }

  // ...
  this.addListener('connection', connectionListener);

  // ...
}
util.inherits(Server, net.Server);

這時,則須要一個 HTTP parser 來解析經過 TCP 傳輸過來的數據:web

// lib/_http_server.js
const parsers = common.parsers;
// ...

function connectionListener(socket) {
  // ...
  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;
  // ...
}

值得一提的是,parser 是從一個「池」中獲取的,這個「池」使用了一種叫作 free listwiki)的數據結構,實現很簡單,我的以爲是爲了儘量的對 parser 進行重用,並避免了不斷調用構造函數的消耗,且設有數量上限(http 模塊中爲 1000):緩存

// lib/freelist.js
'use strict';

exports.FreeList = function(name, max, constructor) {
  this.name = name;
  this.constructor = constructor;
  this.max = max;
  this.list = [];
};


exports.FreeList.prototype.alloc = function() {
  return this.list.length ? this.list.pop() :
                            this.constructor.apply(this, arguments);
};


exports.FreeList.prototype.free = function(obj) {
  if (this.list.length < this.max) {
    this.list.push(obj);
    return true;
  }
  return false;
};

因爲數據是從 TCP 不斷推入的,因此這裏的 parser 也是基於事件的,很符合 Node.js 的核心思想。使用的是 http-parser 這個庫:數據結構

// lib/_http_common.js
// ...
const binding = process.binding('http_parser');
const HTTPParser = binding.HTTPParser;
const FreeList = require('internal/freelist').FreeList;
// ...

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);
  // ...
  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete; 
  parser[kOnBody] = parserOnBody; 
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null; 

  return parser;
});
exports.parsers = parsers;

// lib/_http_server.js
// ...

function connectionListener(socket) {
  parser.onIncoming = parserOnIncoming;
}

因此一個完整的 HTTP 請求從接收到徹底解析,會挨個經歷 parser 上的以下事件監聽器:app

  1. parserOnHeaders:不斷解析推入的請求頭數據。curl

  2. parserOnHeadersComplete:請求頭解析完畢,構造 header 對象,爲請求體建立 http.IncomingMessage 實例。socket

  3. parserOnBody:不斷解析推入的請求體數據。函數

  4. parserOnExecute:請求體解析完畢,檢查解析是否報錯,若報錯,直接觸發 clientError 事件。若請求爲 CONNECT 方法,或帶有 Upgrade 頭,則直接觸發 connectupgrade 事件。

  5. parserOnIncoming:處理具體解析完畢的請求。

因此接下來,咱們的關注點天然是 parserOnIncoming 這個監聽器,正是這裏完成了最終 request 事件的觸發,關鍵步驟代碼以下:

// lib/_http_server.js
// ...

function connectionListener(socket) {
  var outgoing = [];
  var incoming = [];
  // ...
  
  function parserOnIncoming(req, shouldKeepAlive) {
    incoming.push(req);
    // ...
    var res = new ServerResponse(req);
    
    if (socket._httpMessage) { // 這裏判斷若爲真,則說明 socket 正在被隊列中以前的 ServerResponse 實例佔用
      outgoing.push(res);
    } else {
      res.assignSocket(socket);
    }
    
    res.on('finish', resOnFinish);
    function resOnFinish() {
      incoming.shift();
      // ...
      var m = outgoing.shift();
      if (m) {
        m.assignSocket(socket);
      }
    }
    // ...
    self.emit('request', req, res);
  }
}

能夠看出,對於同一個 socket 發來的請求,源碼中分別維護了兩個隊列,用於緩衝 IncomingMessage 實例和對應的 ServerResponse 實例。先來的 ServerResponse 實例先佔用 socket ,監聽其 finish 事件,從各自隊列中釋放該 ServerResponse 實例和對應的 IncomingMessage 實例。

比較繞,以一個簡化的圖示來總結這部分邏輯:
3.pic_hd.jpg

響應該 HTTP 請求

到了響應時,事情已經簡單許多了,傳入的 ServerResponse 已經獲取到了 socket。http.ServerResponse 繼承於一個內部類 http.OutgoingMessage,當咱們調用 ServerResponse#writeHead 時,Node.js 爲咱們拼湊好了頭字符串,並緩存在 ServerResponse 實例內部的 _header 屬性中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
  // ...
  if (headers) {
    var keys = Object.keys(headers);
    var isArray = Array.isArray(headers);
    var field, value;

    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      if (isArray) {
        field = headers[key][0];
        value = headers[key][1];
      } else {
        field = key;
        value = headers[key];
      }

      if (Array.isArray(value)) {
        for (var j = 0; j < value.length; j++) {
          storeHeader(this, state, field, value[j]);
        }
      } else {
        storeHeader(this, state, field, value);
      }
    }
  }
  // ...
  this._header = state.messageHeader + CRLF; 
}

緊接着在調用 ServerResponse#end 時,將數據拼湊在頭字符串後,添加對應的尾部,推入 TCP ,具體的寫入操做在內部方法 ServerResponse#_writeRaw 中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype.end = function(data, encoding, callback) {
  // ...
  if (this.connection && data)
    this.connection.cork();
    
  var ret;
  if (data) {
    this.write(data, encoding);
  }
  
  if (this._hasBody && this.chunkedEncoding) {
    ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish);
  } else {
    ret = this._send('', 'binary', finish);
  }
  
  if (this.connection && data)
    this.connection.uncork();
    
  // ...
  return ret;
}

OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) {
  if (typeof encoding === 'function') {
    callback = encoding;
    encoding = null;
  }

  var connection = this.connection;
  // ...
  return connection.write(data, encoding, callback);
};

最後

到這,一個請求就已經經過 TCP ,發回給客戶端了。其實本文中,只涉及到了一條主線進行解析,源碼中還考慮了更多的狀況,如超時,socket 被佔用時的緩存,特殊頭,上游忽然出現問題,更高效的已寫頭的查詢等等。很是值得一讀。

參考:

相關文章
相關標籤/搜索