很多同学或多或少都使用过 Node 创建 HTTP Server 处理 HTTP 请求,可能是简易的博客,或者已经是负载千万级请求的大型服务。但是我们可能并没有深入了解过 Node 创建 HTTP Server 的过程,希望借这篇文章,让大家对 Node 更加了解。
先上流程图,帮助大家更容易的理解源码

初探
我们先看一个简单的创建 HTTP Server 的例子,基本的过程可以分为两步
- 使用
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 的具体内部过程。
在此之前,为了更好的理解代码,我们需要了解一些基本的概念:
文件描述符(File descriptor)是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的,该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
句柄(handle)是 Windows 操作系统用来标识被应用程序所建立或使用的对象的整数。其本质相当于带有引用计数的智能指针。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,可以使用句柄。Unix 系统的文件描述符基本上也属于句柄。
本文中的 handle 可以理解为相关对象的引用。
文中用 ... 符合表示略去了部分和本文讨论内容关联性较低的,不影响主要逻辑的代码,如参数处理、属性赋值等。
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.listen。listen 方法支持多种使用方式,下面我们一一分析
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._listen2,server._listen2 是 setupListenHandle 的别名。
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.cc 和 pipe_wrap 模块创建 PIPE 和 TCP 服务。PIPE 和 TCP 对象都拥有 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 判断当前进程是否子进程,分别使用 child 或 master 文件的导出变量,相应的处理方法也会有所不同
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进程是第一次接收到监听此端口/fd 的worker,则起一个内部 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 接收到到消息后,会根据不同的条件(平台、协议等)分别创建 RoundRobinHandle 和 SharedHandle ,即 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 进程处理。
最后,写文章不容易,如果大家喜欢的话,欢迎一键三联~