(給前端大全加星標,提升前端技能)
作者: 前端巔峰 公號 / Peter 譚金傑
寫在開頭:
作為一名曾經重度使用Node.js作為即時通訊客戶端接入層的開發人員,無法避免調試V8,配合開發addon。於是對Node.js源碼產生了很大的興趣~
順便吐槽一句,Node的內存控制,由於是自動回收,我之前做的產品是20萬人超級群的IM產品,像一秒鐘1000條消息,持續時間長了內存和CPU佔用還是會有一些問題
之前寫過cluster模塊源碼分析、PM2原理等,感覺興趣的可以去公眾號翻一翻
Node.js的js部分源碼基本看得差不多了,今天寫一個createServer過程給大家,對於不怎麼熟悉Node.js源碼的朋友來說,可能是一個不錯的開始,源碼在gitHub上有,直接克隆即可,最近一直比較忙,公司和業餘的工作也是,所以原創比較少。
原生Node.js創建一個基本的服務:
var http = require('http');http.createServer(function (request, response) {// 發送 HTTP 頭部 // HTTP 狀態值: 200 : OK// 內容類型: text/plainresponse.writeHead(200, {'Content-Type': 'text/plain'});// 發送響應數據 "Hello World"response.end('Hello World\n');}).listen(8888);// 終端列印如下信息console.log('Server running at http://127.0.0.1:8888/');我們目前只分析Node.js源碼的js部分的
首先找到Node.js源碼的lib文件夾
然後找到http.js文件
發現createServer真正返回的是new Server,而 Server來自_http_server
於是找到同目錄下的_http_server.js文件,發現整個文件有800行的樣子,全局搜索Server找到函數
function Server(options, requestListener) {if (!(this instanceof Server)) return new Server(options, requestListener);if (typeof options === 'function') { requestListener = options; options = {}; } else if (options == null || typeof options === 'object') { options = { ...options }; } else {throw new ERR_INVALID_ARG_TYPE('options', 'object', options); }this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;this[kServerResponse] = options.ServerResponse || ServerResponse; net.Server.call(this, { allowHalfOpen: true });if (requestListener) {this.on('request', requestListener); }createServer函數解析:
參數控制有點像redux源碼裡的initState和reducer,根據傳入類型不同,做響應的處理
this.on('request', requestListener);}每次有請求,就會調用requestListener這個回調函數
至於IncomingMessage和ServerResponse,請求是流,響應也是流,請求是可讀流,響應是可寫流,當時寫那個靜態資源伺服器時候有提到過
那麼怎麼可以鏈式調用?有人可能會有疑惑。Node.js源碼遵循commonJs規範,大都掛載在prototype上,所以函數開頭有,就是確保可以鏈式調用
if (!(this instanceof Server)) return new Server(options, requestListener);上面已經將onrequest事件觸發回調函數講清楚了,那麼鏈式調用listen方法,監聽埠是怎麼回事呢?
傳統的鏈式調用,像JQ源碼是return this , 手動實現A+規範的Promise則是返回一個全新的Promise,然後Promise原型上有then方法,於是可以鏈式調用
怎麼實現.listen鏈式調用,重點在這行代碼:
net.Server.call(this, { allowHalfOpen: true });
allowHalfOpen實驗結論: 這裡TCP的知識不再做過度的講解(1)allowHalfOpen為true,一端發送FIN報文:進程結束了,那麼肯定會發送FIN報文;進程未結束,不會發送FIN報文(2)allowHalfOpen為false,一端發送FIN報文:進程結束了,肯定發送FIN報文;進程未結束,也會發送FIN報文;於是找到net.js文件模塊中的Server函數
function Server(options, connectionListener) { if (!(this instanceof Server)) return new Server(options, connectionListener);
EventEmitter.call(this);
if (typeof options === 'function') { connectionListener = options; options = {}; this.on('connection', connectionListener); } else if (options == null || typeof options === 'object') { options = { ...options };
if (typeof connectionListener === 'function') { this.on('connection', connectionListener); } } else { throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); }
this._connections = 0;
Object.defineProperty(this, 'connections', { get: deprecate(() => {
if (this._usingWorkers) { return null; } return this._connections; }, 'Server.connections property is deprecated. ' + 'Use Server.getConnections method instead.', 'DEP0020'), set: deprecate((val) => (this._connections = val), 'Server.connections property is deprecated.', 'DEP0020'), configurable: true, enumerable: false });
this[async_id_symbol] = -1; this._handle = null; this._usingWorkers = false; this._workers = []; this._unref = false;
this.allowHalfOpen = options.allowHalfOpen || false; this.pauseOnConnect = !!options.pauseOnConnect;}這裡巧妙的通過.call調用net模塊Server函數,保證了this指向一致
this._handle = null 這裡是因為Node.js考慮到多進程問題,所以會hack掉這個屬性,因為.listen方法最終會調用_handle中的方法,多個進程只會啟動一個真正進程監聽埠,然後負責分發給不同進程,這個後面會講
Node.js源碼的幾個特色:
遵循conmonjs規範,很多方法掛載到prototype上了
很多object.definepropoty數據劫持
this指向的修改,配合第一個進行鏈式調用
自帶自定義事件模塊,很多內置的函數都繼承或通過Object.setPrototypeOf去封裝了一些自定義事件
代碼模塊互相依賴比較多,一個.listen過程就很麻煩,初學代碼者很容易睡著
源碼學習,本就枯燥。沒什麼好說的了
我在net.js文件模塊中發現了一個原型上.listen的方法:
Server.prototype.listen = function(...args) { const normalized = normalizeArgs(args); var options = normalized[0]; const cb = normalized[1];
if (this._handle) { throw new ERR_SERVER_ALREADY_LISTEN(); }
if (cb !== null) { this.once('listening', cb); } const backlogFromArgs = // (handle, backlog) or (path, backlog) or (port, backlog) toNumber(args.length > 1 && args[1]) || toNumber(args.length > 2 && args[2]); // (port, host, backlog)
options = options._handle || options.handle || options; const flags = getFlags(options.ipv6Only); // (handle[, backlog][, cb]) where handle is an object with a handle if (options instanceof TCP) { this._handle = options; this[async_id_symbol] = this._handle.getAsyncId(); listenInCluster(this, null, -1, -1, backlogFromArgs); return this; } // (handle[, backlog][, cb]) where handle is an object with a fd if (typeof options.fd === 'number' && options.fd >= 0) { listenInCluster(this, null, null, null, backlogFromArgs, options.fd); return this; }
// ([port][, host][, backlog][, cb]) where port is omitted, // that is, listen(), listen(null), listen(cb), or listen(null, cb) // or (options[, cb]) where options.port is explicitly set as undefined or // null, bind to an arbitrary unused port if (args.length === 0 || typeof args[0] === 'function' || (typeof options.port === 'undefined' && 'port' in options) || options.port === null) { options.port = 0; } // ([port][, host][, backlog][, cb]) where port is specified // or (options[, cb]) where options.port is specified // or if options.port is normalized as 0 before var backlog; if (typeof options.port === 'number' || typeof options.port === 'string') { if (!isLegalPort(options.port)) { throw new ERR_SOCKET_BAD_PORT(options.port); } backlog = options.backlog || backlogFromArgs; // start TCP server listening on host:port if (options.host) { lookupAndListen(this, options.port | 0, options.host, backlog, options.exclusive, flags); } else { // Undefined host, listens on unspecified address // Default addressType 4 will be used to search for master server listenInCluster(this, null, options.port | 0, 4, backlog, undefined, options.exclusive); } return this; }
// (path[, backlog][, cb]) or (options[, cb]) // where path or options.path is a UNIX domain socket or Windows pipe if (options.path && isPipeName(options.path)) { var pipeName = this._pipeName = options.path; backlog = options.backlog || backlogFromArgs; listenInCluster(this, pipeName, -1, -1, backlog, undefined, options.exclusive);
if (!this._handle) { // Failed and an error shall be emitted in the next tick. // Therefore, we directly return. return this; }
let mode = 0; if (options.readableAll === true) mode |= PipeConstants.UV_READABLE; if (options.writableAll === true) mode |= PipeConstants.UV_WRITABLE; if (mode !== 0) { const err = this._handle.fchmod(mode); if (err) { this._handle.close(); this._handle = null; throw errnoException(err, 'uv_pipe_chmod'); } } return this; }
if (!(('port' in options) || ('path' in options))) { throw new ERR_INVALID_ARG_VALUE('options', options, 'must have the property "port" or "path"'); }
throw new ERR_INVALID_OPT_VALUE('options', inspect(options));};這個就是我們要找的listen方法,可是裡面很多ipv4和ipv6的處理,最重要的方法是listenInCluster
這個函數需要好好看一下,只有幾十行
function listenInCluster(server, address, port, addressType, backlog, fd, exclusive, flags) { exclusive = !!exclusive; if (cluster === undefined) cluster = require('cluster');
if (cluster.isMaster || exclusive) { // Will create a new handle // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method server._listen2(address, port, addressType, backlog, fd, flags); return; }
const serverQuery = { address: address, port: port, addressType: addressType, fd: fd, flags, };
// Get the master's server handle, and listen on it cluster._getServer(server, serverQuery, listenOnMasterHandle);
function listenOnMasterHandle(err, handle) { err = checkBindError(err, port, handle);
if (err) { var ex = exceptionWithHostPort(err, 'bind', address, port); return server.emit('error', ex); }
// Reuse master's server handle server._handle = handle; // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method server._listen2(address, port, addressType, backlog, fd, flags); }}如果是主進程,那麼就直接調用_.listen2方法了
Server.prototype._listen2 = setupListenHandle;
找到setupListenHandle函數
function setupListenHandle(address, port, addressType, backlog, fd, flags) { debug('setupListenHandle', address, port, addressType, backlog, fd);
// If there is not yet a handle, we need to create one and bind. // In the case of a server sent via IPC, we don't need to do this. if (this._handle) { debug('setupListenHandle: have a handle already'); } else { debug('setupListenHandle: create a handle');
var rval = null;
// Try to bind to the unspecified IPv6 address, see if IPv6 is available 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; } }
if (rval === null) rval = createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number') { var error = uvExceptionWithHostPort(rval, 'listen', address, port); process.nextTick(emitErrorNT, this, error); return; } this._handle = rval; }
this[async_id_symbol] = getNewAsyncId(this._handle); this._handle.onconnection = onconnection; this._handle[owner_symbol] = this;
// Use a backlog of 512 entries. We pass 511 to the listen() call because // the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1); // which will thus give us a backlog of 512 entries. const err = this._handle.listen(backlog || 511);
if (err) { var ex = uvExceptionWithHostPort(err, 'listen', address, port); this._handle.close(); this._handle = null; defaultTriggerAsyncIdScope(this[async_id_symbol], process.nextTick, emitErrorNT, this, ex); return; }
// Generate connection key, this should be unique to the connection this._connectionKey = addressType + ':' + address + ':' + port;
// Unref the handle if the server was unref'ed prior to listening if (this._unref) this.unref();
defaultTriggerAsyncIdScope(this[async_id_symbol], process.nextTick, emitListeningNT, this);}裡面的createServerHandle是重點
function createServerHandle(address, port, addressType, fd, flags) { var err = 0; // Assign handle in listen, and clean up if bind or listen fails var handle;
var isTCP = false; if (typeof fd === 'number' && fd >= 0) { try { handle = createHandle(fd, true); } catch (e) { // Not a fd we can listen on. This will trigger an error. debug('listen invalid fd=%d:', fd, e.message); return UV_EINVAL; }
err = handle.open(fd); if (err) return err;
assert(!address && !port); } else if (port === -1 && addressType === -1) { handle = new Pipe(PipeConstants.SERVER); if (process.platform === 'win32') { var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES); if (!Number.isNaN(instances)) { handle.setPendingInstances(instances); } } } else { handle = new TCP(TCPConstants.SERVER); isTCP = true; }
if (address || port || isTCP) { debug('bind to', address || 'any'); if (!address) { // Try binding to ipv6 first err = handle.bind6(DEFAULT_IPV6_ADDR, port, flags); if (err) { handle.close(); // Fallback to ipv4 return createServerHandle(DEFAULT_IPV4_ADDR, port); } } else if (addressType === 6) { err = handle.bind6(address, port, flags); } else { err = handle.bind(address, port); } }
if (err) { handle.close(); return err; }
return handle;}已經可以看到TCP了,離真正的綁定監聽埠,更近了一步
最終通過下面的方法綁定監聽埠
handle.bind6(address, port, flags); 或者 handle.bind(address, port);首選ipv6綁定,是因為ipv6可以接受到ipv4的套接字,而ipv4不可以接受ipv6的套接字,當然也有方法可以接收,就是麻煩了一點
上面的內容,請你認真看,因為下面會更複雜,設計到Node.js的多進程負載均衡原理
如果不是主進程,就調用cluster._getServer,找到cluster源碼
'use strict';
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';module.exports = require(`internal/cluster/${childOrMaster}`);找到_getServer函數源碼
// `obj` is a net#Server or a dgram#Socket object.cluster._getServer = function(obj, options, cb) { let address = options.address;
// Resolve unix socket paths to absolute paths if (options.port < 0 && typeof address === 'string' && process.platform !== 'win32') address = path.resolve(address);
const indexesKey = [address, options.port, options.addressType, options.fd ].join(':');
let index = indexes.get(indexesKey);
if (index === undefined) index = 0; else index++;
indexes.set(indexesKey, index);
const message = { act: 'queryServer', index, data: null, ...options };
message.address = address;
// Set custom data on handle (i.e. tls tickets key) if (obj._getServerData) message.data = obj._getServerData();
send(message, (reply, handle) => { if (typeof obj._setServerData === 'function') obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });
obj.once('listening', () => { cluster.worker.state = 'listening'; const address = obj.address(); message.act = 'listening'; message.port = (address && address.port) || options.port; send(message); });};我們之前傳入了三個參數給它,分別是
server,serverQuery,listenOnMasterHandle
這裡是比較複雜的,曾經我也在這裡迷茫過一段時間,但是想著還是看下去吧。堅持下,大家如果看到這裡看不下去了,先休息下,保存著。後面等心情平復了再靜下來接下去看
首先我們傳入了Server、serverQuery和cb(回調函數listenOnMasterHandle),整個cluster模塊的_getServer中最重要的就是:
if (obj._getServerData) message.data = obj._getServerData();
send(message, (reply, handle) => { if (typeof obj._setServerData === 'function') obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });首先我們會先獲取server上的data數據,然後調用send函數
function send(message, cb) { return sendHelper(process, message, null, cb);}send函數調用的是cluster模塊的utills文件內的函數,傳入了一個默認值process
function sendHelper(proc, message, handle, cb) { if (!proc.connected) return false;
message = { cmd: 'NODE_CLUSTER', ...message, seq };
if (typeof cb === 'function') callbacks.set(seq, cb);
seq += 1; return proc.send(message, handle);}這裡要看清楚,我們調用sendHelper傳入的第三個參數是null !!!
那麼主進程返回也是null
send(message, (reply, handle) => { if (typeof obj._setServerData === 'function') obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });所以我們會進入rr函數調用的這個判斷,這裡調用rr傳入的cb就是在net.js模塊定義的listenOnMasterHandle函數
Node.js的負載均衡算法是輪詢,官方給出的解釋是簡單粗暴效率高
上面的sendHelper函數就是做到了這點,每次+1
if (typeof cb === 'function') callbacks.set(seq, cb);
seq += 1;function rr(message, indexesKey, cb) { if (message.errno) return cb(message.errno, null);
var key = message.key;
function listen(backlog) { return 0; }
function close() { if (key === undefined) return;
send({ act: 'close', key }); handles.delete(key); indexes.delete(indexesKey); key = undefined; }
function getsockname(out) { if (key) Object.assign(out, message.sockname);
return 0; }
const handle = { close, listen, ref: noop, unref: noop };
if (message.sockname) { handle.getsockname = getsockname; }
assert(handles.has(key) === false); handles.set(key, handle); cb(0, handle);}此時的handle已經被重寫,listen方法調用會返回0,不會再佔用埠了。所以這樣Node.js多個進程也只是一個進程監聽埠而已
此時的cb還是net.js模塊的setupListenHandle即 - _listen2方法。
官方的注釋:
Faux handle. Mimics a TCPWrap with just enough fidelity to get away
仿句柄。以足夠的保真度來模擬TCPWrap覺得本文對你有幫助?請分享給更多人
關注「前端大全」加星標,提升前端技能
好文章,我在看❤️