不少同窗或多或少都使用过 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
文中用 ...
符合表示略去了部分和本文讨论内容关联性较低的,不影响主要逻辑的代码,如参数处理、属性赋值等。
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
模块的 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
实例后,一般须要调用 server.listen
方法启动服务,开始处理请求,如 Koa 的 app.listen
。listen
方法支持多种使用方式,下面咱们一一分析
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;
}
复制代码
server.listen([port[, host[, backlog]]][, callback])
第二种是咱们常见的监听端口,Node 容许咱们建立一个服务器,监听给定的 host 上的端口,host 能够是 IP 地址,或者域名连接,当 host 是域名连接时,Node 会先使用 dns.lookup
获取 IP 地址。最后,检验完端口合法后,一样是调用了 listenInCluster
方法,源码🔗。
[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
方法。
首先,咱们要对 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
进程的处理方法 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
,使用系统能力,开启监听服务。
若是当前进程不是 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
。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
(也是除 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
的处理模式为: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
进程处理。
最后,写文章不容易,若是你们喜欢的话,欢迎一键三联~