從源碼解讀 Node 服務原理

不少同窗或多或少都使用過 Node 建立 HTTP Server 處理 HTTP 請求,多是簡易的博客,或者已是負載千萬級請求的大型服務。可是咱們可能並無深刻了解過 Node 建立 HTTP Server 的過程,但願借這篇文章,讓你們對 Node 更加了解。html

先上流程圖,幫助你們更容易的理解源碼node

初探

咱們先看一個簡單的建立 HTTP Server 的例子,基本的過程能夠分爲兩步linux

  • 使用 createServer 獲取 server 對象
  • 調用 server.listen 開啓監聽服務
const http = require('http')

// 建立 server 對象
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('響應內容');
});

// 開始監聽 3000 端口的請求
server.listen(3000)

複製代碼

這個過程是很是簡單,下面咱們會根據這個過程,結合源碼,開始分析 Node 建立 HTTP Server 的具體內部過程。git

在此以前,爲了更好的理解代碼,咱們須要瞭解一些基本的概念:github

fd - 文件描述符數據庫

文件描述符(File descriptor)是一個用於表述指向文件的引用的抽象化概念。文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的,該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。服務器

handle - 句柄app

句柄(handle)是 Windows 操做系統用來標識被應用程序所創建或使用的對象的整數。其本質至關於帶有引用計數的智能指針。當一個應用程序要引用其餘系統(如數據庫、操做系統)所管理的內存塊或對象時,可使用句柄。Unix 系統的文件描述符基本上也屬於句柄。socket

本文中的 handle 能夠理解爲相關對象的引用。async

文中用 ... 符合表示略去了部分和本文討論內容關聯性較低的,不影響主要邏輯的代碼,如參數處理、屬性賦值等。

http.createServer

createServer 是個工廠方法,返回了 _http_server 模塊中的 Server 類的實例,而 Server 是從 _http_server 文件導出的

const {
  Server,
} = require('_http_server');

// http.createServer
function createServer(opts, requestListener) {
  return new Server(opts, requestListener);
}
複製代碼

_http_server

_http_server 模塊的 Server 類中能夠看出,http.Server 是繼承於 net.Server

function Server(options, requestListener) {
  // 能夠不使用 new 直接調用 http.Server()
  if (!(this instanceof Server)) return new Server(options, requestListener);
  
  // 參數適配
  // ...

	// 繼承
  net.Server.call(this, { allowHalfOpen: true });

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

	// ...
  this.on('connection', connectionListener);
	// ...
}

// http.Server 繼承自 net.Server
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
ObjectSetPrototypeOf(Server, net.Server);

...
複製代碼

這裏的繼承關係也比較好理解:Node 中的 net.Server 是用於建立 TCP 或 IPC 服務器的模塊。咱們都知道,HTTP 是應用層協議,而 TCP 是傳輸層協議。HTTP 經過 TCP 傳輸數據,並進行再次的解析。Node 中的 HTTP 模塊基於 TCP 模塊作了再封裝,實現了不一樣的解析處理邏輯,即出現了咱們看到的繼承關係。

相似的,net.Server 繼承了 EventEmitter 類,擁有許多事件觸發器,包含一些屬性信息,感興趣的同窗能夠自行查閱。

至此,咱們能夠看到,createServer 只是 net.Server 的實例化過程,並無建立服務監聽,而是由 server.listen 方法實現。

server.listen

當建立完成 server 實例後,一般須要調用 server.listen 方法啓動服務,開始處理請求,如 Koa 的 app.listenlisten 方法支持多種使用方式,下面咱們一一分析

1. server.listen(handle[, backlog][, callback])

第一種是不太常見的用法,Node 容許咱們啓動一個服務器,監聽已經綁定到端口、Unix 域套接字或 Windows 命名管道的,給定的 handle 上的鏈接。

handle 對象能夠是服務器、套接字(任何具備底層 _handle 成員的東西),也能夠是具備 fd(文件描述符) 屬性的對象,如咱們經過 createServer 建立的 Server 對象。

當識別到是 handle 對象以後,就會調用 listenInCluster 方法,從方法的名字,咱們能夠猜想到這個就是啓動服務監聽的方法:

// handle 是具備 _handle 屬性的對象
if (options instanceof TCP) {
  this._handle = options;
  this[async_id_symbol] = this._handle.getAsyncId();
  listenInCluster(this, null, -1, -1, backlogFromArgs);
  return this;
}

// 當 handle 是具備 fd 屬性的對象
if (typeof options.fd === "number" && options.fd >= 0) {
  listenInCluster(this, null, null, null, backlogFromArgs, options.fd);
  return this;
}
複製代碼

2. server.listen([port[, host[, backlog]]][, callback])

第二種是咱們常見的監聽端口,Node 容許咱們建立一個服務器,監聽給定的 host 上的端口,host 能夠是 IP 地址,或者域名連接,當 host 是域名連接時,Node 會先使用 dns.lookup 獲取 IP 地址。最後,檢驗完端口合法後,一樣是調用了 listenInCluster方法,源碼🔗

3. [server.listen(path[, backlog][, callback])](http://nodejs.cn/s/yW8Zc1)

第三種,Node 容許啓動一個 IPC 服務器監聽指定的 IPC 路徑,即 Windows 上的命名管道 IPC以及 其餘類 Unix 系統中的 Unix Domain Socket。

這裏的 path 參數是識別 IPC 鏈接的路徑。 在 Unix 系統上,參數 path 表現爲文件系統路徑名,在 Windows 上,path 必須是以 \\?\pipe\\\.\pipe\ 爲入口。

而後,一樣是調用了 listenInCluster 方法,源碼🔗

還有一種調用方法 server.listen(options[, callback]) 是端口和 IPC 路徑的另一種調用方式,這裏就很少說了。

最後就是對不符合上述全部條件的異常狀況,拋出錯誤。

小結

至此,咱們能夠看到,server.listen 方法對不一樣的調用方式作了解析,並調用了 listenInCluster 方法。

listenInCluster

首先,咱們要對 clsuter 作一個簡單的介紹。

咱們都知道 JavaScript 是單線程運行的,一個線程只會在一個 CPU 核心上運行。而現代的處理都是多核心的,爲了充分利用多核,就須要啓用多個 Node.js 進程去處理負載任務。

Node 提供的 cluster 模塊解決了這個問題 ,咱們可使用 cluster 建立多個進程,而且同時監聽同一個端口,而不會發生衝突,是否是很神奇?不要着急,下面咱們就會解密這個神奇的 cluster 模塊。

先看一個 cluster 的簡單用法:

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {
  // 衍生工做進程。
  for (let i = 0; i < 4; i++) {
    cluster.fork();
  }
} else {
  // 工做進程能夠共享任何 TCP 鏈接。
  // 在本例子中,共享的是 HTTP 服務器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);
}
複製代碼

基於 cluster 的用法,負責啓動其餘進程的叫作 master 進程,不作具體的工做,只負責啓動其餘進程。其餘被啓動的進程則叫 worker 進程,它們接收請求,並對外提供服務。

listenInCluster 方法主要作了一件事:區分 master 進程(cluster.isMaster)和 worker 進程,採用不一樣的處理策略:

  • master 進程:直接調用 server._listen 啓動監聽
  • worker進程:使用 clsuter._getServer 處理傳入的 server 對象,修改 server._handle 再調用了 server._listen 啓動監聽
function listenInCluster(...) {
  // 引入 cluster 模塊
  if (cluster === undefined) cluster = require('cluster');

  // master 進程
  if (cluster.isMaster || exclusive) {
    server._listen2(address, port, addressType, backlog, fd, flags);
    return;
  }

  // 非 master 進程,即經過 cluster 啓動的子進程
  const serverQuery = {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags,
  };
  
  // 調用 cluster 的方法處理
  cluster._getServer(server, serverQuery, listenOnMasterHandle);

  function listenOnMasterHandle(err, handle) {
    // ...
    server._handle = handle;
    server._listen2(address, port, addressType, backlog, fd, flags);
  }
}

複製代碼

master 進程

咱們先看 master 進程的處理方法 server._listen2server._listen2setupListenHandle 的別名。

setupListenHandle 主要是負責根據 server 監聽鏈接的不一樣類型,調用 createServerHandle 方法獲取 handle 對象,並調用 handle.listen 方法開啓監聽。

function setupListenHandle(address, port, addressType, backlog, fd, flags) {
  // 若是是 handle 對象,須要創一個 handle 對象
  if (this._handle) {
    // do nothing
  } else {
    let rval = null;
    // 在 host 和 port 省略,且沒有指定 fd 的狀況下
    // 若是 IPv6 可用,服務器將會接收基於未指定的 IPv6 地址 (::) 的鏈接
    // 不然接收基於未指定的 IPv4 地址 (0.0.0.0) 的鏈接。
    if (!address && typeof fd !== 'number') {
      rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);

      if (typeof rval === 'number') {
        rval = null;
        address = DEFAULT_IPV4_ADDR;
        addressType = 4;
      } else {
        address = DEFAULT_IPV6_ADDR;
        addressType = 6;
      }
    }
    
    // fd 或 IPC
    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd, flags);

    // 若是 createServerHandle 返回的是數字,則代表出現了錯誤,進程退出
    if (typeof rval === 'number') {
      const error = uvExceptionWithHostPort(rval, 'listen', address, port);
      process.nextTick(emitErrorNT, this, error);
      return;
    }

    this._handle = rval;
  }
   
  ...

  // 開始監聽
  const err = this._handle.listen(backlog || 511);

  ...
  // 觸發 listening 方法
}
複製代碼

createServerHandle 負責調用 C++tcp_warp.ccpipe_wrap 模塊建立 PIPETCP 服務。PIPETCP 對象都擁有 listen 方法,listen 方法是對 uvlib 中的 [uv_listen](http://docs.libuv.org/en/v1.x/stream.html?highlight=uv_listen#c.uv_listen) 方法的封裝,與 Linux 中的 [listen(2)](https://man7.org/linux/man-pages/man2/listen.2.html) 相似。能夠調用系統能力,開始監聽傳入的鏈接,並在收到新鏈接後回調請求信息。

PIPE 是對 Unix 上的流文件(包括 socket,pipes)以及 Windows 上的命名管道的抽象封裝,TCP 就是對 TCP 服務的封裝。

function createServerHandle(address, port, addressType, fd, flags) {
  // ...
  let isTCP = false;
  // 當 fd 選項存在時
  if (typeof fd === 'number' && fd >= 0) {
    try {
      handle = createHandle(fd, true);
    } catch (e) {
      debug('listen invalid fd=%d:', fd, e.message);
      // uvlib 中的錯誤碼,表示非法的參數,是個負數
      return UV_EINVAL;
    }
    ...
  } else if (port === -1 && addressType === -1) {
    // 當 port 和 address 不存在時,即監聽 Socket 或 IPC 等
    // 建立 Pipe Server
    handle = new Pipe(PipeConstants.SERVER);
    ...
  } else {
    // 建立 TCB SERVER
    handle = new TCP(TCPConstants.SERVER);
    isTCP = true;
  }
  // ...
  return handle;
}
複製代碼

小結

master 進程的 server.listen 處理邏輯較爲簡單,能夠歸納爲直接調用 libuv ,使用系統能力,開啓監聽服務。

worker 進程

若是當前進程不是 master 進程,事情就會變得複雜許多。

listenInCluster 方法會調用 cluster 模塊導出的 _getServer 方法,cluster 模塊會經過當前進程是否包含 NODE_UNIQUE_ID 判斷當前進程是否子進程,分別使用 childmaster 文件的導出變量,相應的處理方法也會有所不一樣

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';

module.exports = require(`internal/cluster/${childOrMaster}`);
複製代碼

咱們所說的 worker 進程,沒有 NODE_UNIQUE_ID 環境變量,會使用 child 模塊導出的 _getServer 方法。

worker 進程的 _getServer 方法主要作了如下兩件事情:

  • 經過發送 internalMessage,即進程間通訊的方式,向 master 進程傳遞消息,調用 queryServe,註冊當前 worker 進程的信息。若 master 進程是第一次接收到監聽此端口/fdworker,則起一個內部 TCP 服務器,來承擔監聽該端口/fd 的職責,隨後在 master 中記錄下該 worker
  • 若是是輪訓監聽(RoundRobinHandle),就修改掉 worker 進程中的 net.Server 實例的 listen 方法裏監聽端口/fd的部分,使其再也不承擔監聽職責。
// obj 是 net.Server 或 Socket 的實例
cluster._getServer = function(obj, options, cb) {
  let address = options.address;
  // ...
  // const indexesKey = ...;
  // indexes 爲 Map 對象
  indexes.set(indexesKey, index);

  const message = {
    act: 'queryServer',
    index,
    data: null,
    ...options
  };

  message.address = address;

  // 發送 internalMessage 通知 Master 進程
  // 接受 Master 進程的回調
  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      // 關閉鏈接時,移除 handle 避免內存泄漏
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      // 僞造了 listen 等方法
      rr(reply, indexesKey, cb);              // Round-robin.
  });

  // ...
};
複製代碼

master 中的 queryServer 接收到到消息後,會根據不一樣的條件(平臺、協議等)分別建立 RoundRobinHandleSharedHandle ,即 cluster 兩種分發處理鏈接的方法。

同時 master 進程會將監聽端口、地址等信息組成的 key 做爲惟一標誌,記錄 handle 和對應 worker 的信息。

function queryServer(worker, message) {
  // ...
  const key = `${message.address}:${message.port}:${message.addressType}:` +
              `${message.fd}:${message.index}`;
  let handle = handles.get(key);

  if (handle === undefined) {
    let address = message.address;
    ...
    let constructor = RoundRobinHandle;
    if (schedulingPolicy !== SCHED_RR ||
        message.addressType === 'udp4' ||
        message.addressType === 'udp6') {
      constructor = SharedHandle;
    }

    handle = new constructor(key, address, message);
    handles.set(key, handle);
  }

  // ...
  handle.add(worker, (errno, reply, handle) => {
    const { data } = handles.get(key);
    // ...
    send(worker, {
      errno,
      key,
      ack: message.seq,
      data,
      ...reply
    }, handle);
  });
}
複製代碼

RoundRobinHandle

RoundRobinHandle(也是除 Windows 外全部平臺的默認方法)的處理模式爲:由 master 進程負責監聽端口,接收新鏈接後再將鏈接循環分發給 worker 進程,即將請求放到一個隊列中,從空閒的 worker 池中分出一個處理請求,處理完成後在放回 worker 池中,以此類推

function RoundRobinHandle(key, address, { port, fd, flags }) {
  this.key = key;
  this.all = new Map();
  this.free = new Map();
  this.handles = [];
  this.handle = null;
  // 建立 Server
  this.server = net.createServer(assert.fail);

  // 開啓監聽,多種狀況,省略
  // this.server.listen(...)

  this.server.once('listening', () => {
    this.handle = this.server._handle;
    // 收到請求,分發處理
    this.handle.onconnection = (err, handle) => this.distribute(err, handle);
    this.server._handle = null;
    this.server = null;
  });
}

// ...

RoundRobinHandle.prototype.distribute = function(err, handle) {
  this.handles.push(handle);
  const [ workerEntry ] = this.free;

  if (ArrayIsArray(workerEntry)) {
    const [ workerId, worker ] = workerEntry;
    this.free.delete(workerId);
    this.handoff(worker);
  }
};

RoundRobinHandle.prototype.handoff = function(worker) {
  if (!this.all.has(worker.id)) {
    return;  // Worker is closing (or has closed) the server.
  }

  const handle = this.handles.shift();

  if (handle === undefined) {
    this.free.set(worker.id, worker);  // Add to ready queue again.
    return;
  }

  const message = { act: 'newconn', key: this.key };

  sendHelper(worker.process, message, handle, (reply) => {
    if (reply.accepted)
      handle.close();
    else
      this.distribute(0, handle);

    this.handoff(worker);
  });
};
複製代碼

SharedHandle

SharedHandle 的處理模式爲:master 進程建立監聽服務器 ,再將服務器的 handle 發送 worker 進程,由 worker 進程負責直接接收鏈接

function SharedHandle(key, address, { port, addressType, fd, flags }) {
  this.key = key;
  this.workers = new Map();
  this.handle = null;
  this.errno = 0;

  let rval;
  if (addressType === 'udp4' || addressType === 'udp6')
    rval = dgram._createSocketHandle(address, port, addressType, fd, flags);
  else
    rval = net._createServerHandle(address, port, addressType, fd, flags);

  if (typeof rval === 'number')
    this.errno = rval;
  else
    this.handle = rval;
}

// 添加存儲 worker 信息
SharedHandle.prototype.add = function(worker, send) {
  assert(!this.workers.has(worker.id));
  this.workers.set(worker.id, worker);
  // 向 worker 進程發送 handle
  send(this.errno, null, this.handle);
};
// ..
複製代碼

PS:Windows 之因此不採用 RoundRobinHandle 的緣由是由於性能緣由。從理論上來講,第二種方法應該是效率最佳的。 但在實際狀況下,因爲操做系統調度機制的難以捉摸,會使分發變得不穩定,可能會出現八個進程中有兩個分擔了 70% 的負載。相比而言,輪訓的方法會更加高效。

小結

worker 進程中,每一個 worker 再也不獨立開啓監聽服務,而是由 master 進程開啓一個統一的監聽服務,接受請求鏈接,再將請求轉發給 worker 進程處理。

總結

在不一樣的狀況下,Node 建立 HTTP Server 的流程是不一致的。當進程爲 master 進程時,Node 會直接經過 libuv 調用系統能力開啓監聽。當進程爲 child 進程(worker 進程)時,Node 會使用 master 進程開啓間監聽,並經過輪訓或共享 Handle 的方式將鏈接分發給 child 進程處理。

最後,寫文章不容易,若是你們喜歡的話,歡迎一鍵三聯~

相關文章
相關標籤/搜索