我可以想象100个请求到达单个Node.js服务器的情况。它们中的每一个都需要一些DB交互,这些交互是通过一些本地异步代码实现的——使用任务队列或至少是微任务队列(例如,DB驱动程序接口是承诺的(。
当请求处理程序停止同步时,Node.js如何返回响应?来自api/web客户端的连接发生了什么?这100个来自描述的请求来自哪里?
此功能在操作系统级别可用,称为异步I/O或非阻塞I/O(Windows也称之为重叠I/O(。
在最低级别上,在C(C#/Swift(中,操作系统提供了一个API来跟踪请求和响应。根据您使用的操作系统,有各种API可用,Node.js使用libuv在编译时自动选择最佳可用的API,但为了理解异步API的工作原理,让我们看看所有平台都可用的API:select()
系统调用。
select()
函数如下所示:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, time *timeout);
fd_set
数据结构是一组文件描述符/列表,您有兴趣监视I/O活动。记住,在POSIX中,套接字也是文件描述符。您使用此API的方式如下:
// Pseudocode:
// Say you just sent a request to a mysql database and also sent a http
// request to google maps. You are waiting for data to come from both.
// Instead of calling `read()` which would block the thread you add
// the sockets to the read set:
add mysql_socket to readfds
add maps_socket to readfds
// Now you have nothing else to do so you are free to wait for network
// I/O. Great, call select:
select(2, &readfds, NULL, NULL, NULL);
// Select is a blocking call. Yes, non-blocking I/O involves calling a
// blocking function. Yes it sounds ironic but the main difference is
// that we are not blocking waiting for each individual I/O activity,
// we are waiting for ALL of them
// At some point select returns. This is where we check which request
// matches the response:
check readfds if mysql_socket is set {
then call mysql_handler_callback()
}
check readfds if maps_socket is set {
then call maps_handler_callback()
}
go to beginning of loop
因此,基本上,您的问题的答案是,我们检查数据结构,哪个套接字/文件刚刚触发了I/O活动,并执行适当的代码。
毫无疑问,您可以很容易地发现如何概括这种代码模式:您可以将所有挂起的异步请求和回调保留在列表或数组中,并在select()
之前和之后循环使用,而不是手动设置和检查文件描述符。事实上,Node.js(以及javascript(就是这么做的。正是这个回调/文件描述符列表有时被称为事件队列——它本身不是一个队列,只是等待执行的东西的集合。
select()
函数的末尾还有一个超时参数,可用于实现setTimeout()
和setInterval()
,并在浏览器中处理GUI事件,以便我们可以在等待I/O时运行代码。因为请记住,select
是阻塞的——只有当select返回时,我们才能运行其他代码。通过仔细管理定时器,我们可以计算出适当的值作为超时值传递给select
。
fd_set
数据结构实际上并不是一个链表。在较旧的实现中,它是一个位字段。只要符合API,更现代的实现可以改进比特字段。但这部分解释了为什么有这么多竞争异步API,如poll
、epoll
、kqueue
等。它们是为了克服select
的限制而创建的。不同的API以不同的方式跟踪文件描述符,有些使用链表,有些使用哈希表,有些则考虑到可伸缩性(能够监听数万个套接字(,有些则注意到速度,大多数API都试图比其他API做得更好。无论他们使用什么,最终用于存储请求的只是一个跟踪文件描述符的数据结构。