Node.js 是如何跑起来的( 七 )


但这种架构模式下的性能瓶颈在于系统的进程数、线程数是有限的,开辟进程和线程的开销也是需要考虑的问题,系统资源消耗高 。
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// IP 和端口配置 ...// ...// 绑定 IP 和端口bind(serv_sock, ...);// 监听来自监听型套接字的请求listen(serv_sock, ...);int clnt_sock;int pid;while (1) {// 创建新的通信型套接字用于接收来自客户端的请求,此时会阻塞程序执行,直到有请求到来clnt_sock = accept(serv_sock, ...);pid = fork();if (pid < 0) {perror("fork failed");return -1;}if (fork() > 0) {// 父进程continue;} else {// 子进程// 接收客户端的数据,同步阻塞 I/O,等待数据就绪recvfrom(clnt_sock, ...);// 处理数据handle(data);}}?单进程单线程·事件驱动?除了通过多进程/多线程方式去应对并发量大的场景,基于 I/O 多路复用模型的单进程单线程·事件驱动架构也是较好的解决方案,同时由于是单线程,所以不会因开辟大量进场/线程所带来的资源开销以及同步互斥的问题 。
单线程不适合执行 CPU 密集型任务,因为如果任务一直占用 CPU 时间,则后续任务无法执行,因此针对大量 CPU 计算、引起进程阻塞的任务,可引入线程池技术去解决 。
目前 NodeJS 就是采用这种设计架构,所有 JS 代码跑在主线程中(单线程),基于 I/O 多路复用的模型去实现事件驱动的多读写请求的管理,配合线程池,将 CPU 密集型任务从主线程分离出来,以保证主线程的高效响应 。
解答

  • NodeJS 代码是如何跑起来的
  • 当我们执行 node server.js 时,NodeJS 首先会进行一系列的初始化操作,包括:
注册 C++ 系列的模块和 V8 的初始化操作
创建 environment 对象用于存放一些全局的公共变量
初始化模块加载器,以便在用户 JS 代码层调用原生 JS 模块以及原生 JS 模块调用 C++ 模块能够成功加载
初始化执行上下文,暴露 global 在全局上下文中,并设置一些全局变量和方法在 global 或 process 对象
初始化 libuv,创建一个默认的 event_loop 结构体用于管理后续各个阶段产生的任务
紧接着 NodeJS 执行用户 JS 代码,用户 JS 代码执行一些初始化的逻辑以及往事件循环注册任务,然后进程就进入事件循环的阶段 。
整个事件循环分为 7 个阶段,timer 处理定时器任务,pending 处理 poll io 阶段的成功或错误回调,idle、prepare、check 是自定义阶段,poll io 主要处理网络 I/O,文件 I/O等任务,close 处理关闭的回调任务,同时在各个事件阶段还会穿插微任务队列 。
以开篇的 TCP 服务为例,当创建 TCP 服务器调用原生 JS 的 net 模块的 server.listen 方法后,net 模块就会引用 C++ 的 TCP 模块实例化一个 TCP 服务器,内部调用了 Libuv 的 uv_tcp_init 方法,该方法封装了 C 中用于创建套接字的 socket 函数;接着就是调用 C++ 的 TCP 模块的 Bind 方法,该方法封装了 Libuv 的 uv_ip_addr 以及 uv_tcp_bind,分别用于设置 TCP 的 IP 地址和端口信息以及调用 C 中的 bind 方法用于绑定地址信息 。
然后 net 模块注册 onconnect 回调函数,该函数将在客户端请求到来后,在 Libuv 的 poll io 阶段执行,onconnect 函数调用了 C++ 的 ConnectionWrap::OnConnection 方法,内部调用了 Libuv 的 uv_accpet 去接收来自客户端的连接 。最后调用 TCP 实例的 listen 方法使得服务器进入被动监听状态,listen 使用了 C++ 的 TCPWrap::Listen 方法,该方法是对 uv_listen 的封装,最终调用的 C 的 listen 方法 。
当客户端请求通过网卡传递过来,对应的监听型 socket 发生状态变更,事件循环模块根据命中之前设置的可读事件,将 onconnection 回调插入 poll io 阶段的任务队列,当新一轮的事件循环到达 poll io 时执行回调,调用 accept 方法创建与客户端的通信型 socket,此时进入进程阻塞,经过三次握手后,建立与客户端的连接,将用户 JS 的回调插入 poll io 的任务队列,在新一轮的事件循环中进行数据的处理 。
Node.js 是如何跑起来的

文章插图
image.png
  • TCP 连接在 NodeJS 中是如何保持一直监听而进程不中断的
TCP 服务器在启动之后,就往 NodeJS 的事件循环系统插入 listen 的监听任务,该任务会一直阻塞监听(不超过 timeout)来自客户端的请求,当发生请求后,建立连接然后进行数据处理后,再会进入监听请求的阻塞状态,新一轮的事件循环发现 poll io 队列还有任务所以不会退出事件循环,从而驱动进程一直运行 。


推荐阅读