合肥站建设,wordpress交易,旅游预定型网站建设,杭州模板建站创建了一个 “重学TypeScript” 的微信群#xff0c;想加群的小伙伴#xff0c;加我微信 semlinker#xff0c;备注重学TS。在 “了不起的 Deno 入门教程”这篇文章中#xff0c;我们介绍了如何使用 Deno 搭建一个简单的 TCP echo server#xff0c;本文将使用… 创建了一个 “重学TypeScript” 的微信群想加群的小伙伴加我微信 semlinker备注重学TS。在 “了不起的 Deno 入门教程”这篇文章中我们介绍了如何使用 Deno 搭建一个简单的 TCP echo server本文将使用该示例来探究 TCP echo server 是怎么运行的前方高能请小伙伴们深吸一口气做好准备。 了不起的 Deno 入门教程本来计划重写 18 年写的 “深入学习 Node.js” 系列然而 Deno 它来了那就从 Deno 1.0.0 开始吧。“深入学习 Node.js” 仓库地址https://github.com/semlinker/node-deep有兴趣的小伙伴可以了解一下。一、搭建 TCP echo server好了废话不多说我们进入正题。首先我们先来回顾一下之前所写的 TCP echo server具体代码如下echo_server.tsconst listener Deno.listen({ port: 8080 });console.log(listening on 0.0.0.0:8080);for await (const conn of listener) { Deno.copy(conn, conn);}for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环包括 StringArrayArray-like 对象(比如 arguments 或者 NodeList)TypedArrayMap Set 和自定义的异步或者同步可迭代对象。for await...of 的语法如下for await (variable of iterable) { statement}接着我们使用以下命令来启动该 TCP echo server$ deno run --allow-net ./echo_server.ts这里需要注意的是在运行 ./echo_server.ts 时我们需要设置 --allow-net 标志以允许网络访问。不然会出现以下错误信息error: Uncaught PermissionDenied: network access to 0.0.0.0:8080, run again with the --allow-net flag为什么会这样呢这是因为 Deno 是一个 JavaScript/TypeScript 的运行时默认使用安全环境执行代码。当服务器成功运行之后我们使用 nc 命令来测试一下服务器的功能$ nc localhost 8080hell semlinkerhell semlinkernc 是 netcat 的简写有着网络界的瑞士军刀美誉。因为它短小精悍、功能实用被设计为一个简单、可靠的网络工具。nc 的作用1.实现任意 TCP/UDP 端口的侦听nc 可以作为 server 以 TCP 或 UDP 方式侦听指定端口2.端口的扫描nc 可以作为 Client 端发起 TCP 或 UDP 连接3.机器之间传输文件或机器之间网络测速。下面我们来分析一下从启动 TCP echo server 服务器开始到使用 nc 命令连接该服务器这期间发生了什么二、TCP echo server 运行流程分析2.1 启动 TCP echo server在命令行运行 deno run --allow-net ./echo_server.ts 命令后当前命令行会输出以下信息listening on 0.0.0.0:8080表示我们的 TCP echo server 已经开始监听本机的 8080 端口这里我们可以使用 netstat 命令来打印 Linux 中网络系统的状态信息[rootizuf6ghot555xyn666xm888 23178]# netstat -natpActive Internet connections (servers and established)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 23178/deno通过观察以上输出的网络信息我们发现当前 TCP echo server 处于 LISTEN 监听状态且当前进程的 PID 是 23178。在 Linux 中一切都是文件。在 Linux 的根目录下存在一个 /proc 目录/proc 文件系统是一种虚拟文件系统以文件系统目录和文件形式提供一个指向内核数据结构的接口通过它能够查看和改变各种系统属性。下面我们进入 23178 进程目录并使用 ls -l | grep ^d 命令查看当前目录下的子目录信息[rootizuf6ghot555xyn666xm888]# cd /proc/23178[rootizuf6ghot555xyn666xm888 23178]# ls -l | grep ^ddr-xr-xr-x 2 root root 0 May 17 13:17 attrdr-x------ 2 root root 0 May 17 13:16 fddr-x------ 2 root root 0 May 17 13:29 fdinfodr-x------ 2 root root 0 May 17 13:29 map_filesdr-xr-xr-x 5 root root 0 May 17 13:29 netdr-x--x--x 2 root root 0 May 17 13:16 nsdr-xr-xr-x 4 root root 0 May 17 13:16 task下面我们主要分析 /proc/pid/task 和 /proc/pid/fd 这两个目录2.1.1. /proc/pid/task 目录该目录包含的是进程中的每一个线程。每一个目录的名字是以线程 ID 命名的(tid)。在每一个 tid 下面的目录结构与 /proc/pid 下面的目录结构相同。对于所有线程共享的属性task/tid 子目录中的每个文件内容与 /proc/pid 目录中的相应文件内容相同。 比如所有线程中的 task/tid/cwd 文件和父目录中的 /proc/pid/cwd 文件内容相同因为所有的线程共享一个工作目录。对于每个线程的不同属性task/tid 下相应文件的值也不相同。对于我们的 Deno 进程( 23178 )我们使用 ls -al 命令查看 /proc/23178/task 目录的信息[rootizuf6ghot555xyn666xm888 task]# ls -altotal 0dr-xr-xr-x 4 root root 0 May 17 13:16 .dr-xr-xr-x 9 root root 0 May 17 13:15 ..dr-xr-xr-x 6 root root 0 May 17 13:16 23178dr-xr-xr-x 6 root root 0 May 17 13:16 23179接下来我们进入 /proc/23178/task 目录来开始分析 /proc/pid/fd 目录。2.1.2 /proc/pid/fd 目录该目录包含了当前进程打开的每一个文件。每一个条目都是一个文件描述符是一个符号链接指向的是实际打开的地址。其中 0 表示标准输入1 表示标准输出2 表示标准错误。在多线程程序中如果主程序退出了那么这个文件夹将不能被访问。文件描述符在形式上是一个非负整数。实际上它是一个索引值指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时内核向进程返回一个文件描述符。在程序设计中一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。每个 Unix 进程(除了可能的守护进程)应均有三个标准的 POSIX 文件描述符对应于三个标准流整数值名称unistd.h符号常量stdio.h文件流0Standard inputSTDIN_FILENOstdin1Standard outputSTDOUT_FILENOstdout2Standard errorSTDERR_FILENOstderr对于我们的 Deno 进程( 23178 )我们使用 ls -al 命令查看 /proc/23178/fd 目录的信息[rootizuf6ghot555xyn666xm888 fd]# ls -altotal 0dr-x------ 2 root root 0 May 17 13:16 .dr-xr-xr-x 9 root root 0 May 17 13:15 ..lrwx------ 1 root root 64 May 17 13:16 0 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 1 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 2 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 3 - anon_inode:[eventpoll]lr-x------ 1 root root 64 May 17 13:16 4 - pipe:[30180039]l-wx------ 1 root root 64 May 17 13:16 5 - pipe:[30180039]lrwx------ 1 root root 64 May 17 13:16 6 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 7 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 8 - socket:[30180040]观察以上输出结果我们发现除了 0-2 文件描述符之外我们的 Deno 进程( 23178 )还包含了其他的文件描述符。这里我们重点关注文件描述符 8根据输出结果可知它表示一个 Socket。那么这个 Socket 是什么时候创建的呢这个问题我们先记着后面我们会一起探究内部的创建过程。接下来我们来分析下一个流程即使用 nc 命令来连接我们的 TCP echo server。2.2 连接 TCP echo server接下来我们使用前面介绍的 nc 命令来连接我们的 TCP echo server[rootizuf6ghot555xyn666xm888 ~]# nc localhost 8080接着在键盘中输入 hello semlinker此时在当前命令行会自动回显 hello semlinker。这时我们先来使用 netstat 命令来查看当前的网络状态具体命令如下[rootizuf6ghot555xyn666xm888 fd]# netstat -natp | grep 8080tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 23178/deno tcp 0 0 127.0.0.1:55700 127.0.0.1:8080 ESTABLISHED 23274/nc tcp 0 0 127.0.0.1:8080 127.0.0.1:55700 ESTABLISHED 23178/deno 相信眼尖的小伙伴已经注意到 23274/nc 这一行通过这一行我们可以发现 nc 使用本机的 55700 端口与我们的 TCP echo server 建立了 TCP 连接因为当前的连接状态为 ESTABLISHED。这时让我们再次使用 ls -al 命令来查看 /proc/23178/fd 目录的信息该命令的执行结果如下[rootizuf6ghot555xyn666xm888 fd]# ls -altotal 0dr-x------ 2 root root 0 May 17 13:16 .dr-xr-xr-x 9 root root 0 May 17 13:15 ..lrwx------ 1 root root 64 May 17 13:16 0 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 1 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 2 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 3 - anon_inode:[eventpoll]lr-x------ 1 root root 64 May 17 13:16 4 - pipe:[30180039]l-wx------ 1 root root 64 May 17 13:16 5 - pipe:[30180039]lrwx------ 1 root root 64 May 17 13:16 6 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 7 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 8 - socket:[30180040]lrwx------ 1 root root 64 May 17 13:46 9 - socket:[30181765]对比前面的输出结果当使用 nc 命令与 TCP echo server 建立连接后 /proc/23178/fd 目录下增加了一个新的文件描述符即 9 - socket:[30181765]它也是用于表示一个 Socket。好了现在我们已经看到了现象那具体的内部流程是怎么样的呢为了分析内部的执行流程这时我们需要使用 Linux 提供的 strace 命令该命令常用来跟踪进程执行时的系统调用和所接收的信号。三、使用 strace 跟踪进程中的系统调用为了能够更好地理解后续的内容我们需要先介绍一些前置知识比如 Socket、Socket API、用户态和内核态等相关知识。3.1 文件描述符Linux 系统中把一切都看做是文件文件又可分为普通文件、目录文件、链接文件和设备文件。当进程打开现有文件或创建新文件时内核向进程返回一个文件描述符文件描述符就是内核为了高效管理已被打开的文件所创建的索引用来指向被打开的文件所有执行 I/O 操作的系统调用都会通过文件描述符。每一个文件描述符会与一个打开文件相对应同时不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表该表的值都是从 0 开始的所以在不同的进程中你会看到相同的文件描述符这种情况下相同文件描述符有可能指向同一个文件也有可能指向不同的文件。要理解文件描述符我们需要了解由内核维护的 3 个数据结构。进程级的文件描述符表系统级的打开文件描述符表文件系统的 i-node 表。下图展示了文件描述符、打开的文件句柄以及 i-node 之间的关系(图片来源于网络)图中两个进程拥有诸多打开的文件描述符。3.2 Socket网络上的两个程序通过一个双向的通信连接实现数据的交换这个连接的一端称为一个 socket(套接字)因此建立网络通信连接至少要一对端口号。socket 本质是对 TCP/IP 协议栈的封装它提供了一个针对 TCP 或者 UDP 编程的接口并不是另一种协议。通过 socket你可以使用 TCP/IP 协议。Socket 的英文原义是“孔”或“插座”。作为 BSD UNIX 的进程通信机制取后一种意思。通常也称作套接字用于描述IP地址和端口是一个通信链的句柄可以用来实现不同虚拟机或不同计算机之间的通信。在Internet 上的主机一般运行了多个服务软件同时提供几种服务。每种服务都打开一个Socket并绑定到一个端口上不同的端口对应于不同的服务。Socket 正如其英文原义那样像一个多孔插座。一台主机犹如布满各种插座的房间每个插座有一个编号有的插座提供 220 伏交流电 有的提供 110 伏交流电有的则提供有线电视节目。客户软件将插头插到不同编号的插座就可以得到不同的服务。—— 百度百科关于 Socket可以总结以下几点它可以实现底层通信几乎所有的应用层都是通过 socket 进行通信的。对 TCP/IP 协议进行封装便于应用层协议调用属于二者之间的中间抽象层。TCP/IP 协议族中传输层存在两种通用协议: TCP、UDP两种协议不同因为不同参数的 socket 实现过程也不一样。下图说明了面向连接的协议的套接字 API 的客户端/服务器关系。3.3 Socket API(1)socket() 函数用于创建套接字并配置套接字的各种属性返回描述符。int socket(int af, int type, int protocol);af 为地址族(Address Family)也就是 IP 地址类型常用的有 AF_INET 和 AF_INET6。AF 是 “Address Family” 的简写INET 是 “Inetnet” 的简写。AF_INET 表示 IPv4 地址AF_INET6 表示 IPv6 地址。type 为数据传输方式/套接字类型常用的有 SOCK_STREAM(流格式套接字) 和 SOCK_DGRAM(数据报套接字)。protocol 表示传输协议常用的有 IPPROTO_TCP 和 IPPTOTO_UDP分别表示 TCP 传输协议和 UDP 传输协议。使用方式int tcp_socket socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字int udp_socket socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字(2)bind() 函数用于将套接字与特定的 IP 地址和端口绑定起来只有这样流经该 IP 地址和端口的数据才能交给套接字处理。int bind(int sock, struct sockaddr *addr, socklen_t addrlen); sock 为 socket 文件描述符addr 为 sockaddr 结构体变量的指针addrlen 为 addr 变量的大小可由 sizeof() 计算得出。使用方式//创建套接字int listenfd socket(AF_INET, SOCK_STREAM, 0);//创建sockaddr_in结构体变量struct sockaddr_in serv_addr;memset(serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充serv_addr.sin_family AF_INET; //使用IPv4地址serv_addr.sin_addr.s_addr inet_addr(127.0.0.1); //具体的IP地址serv_addr.sin_port htons(8080); //端口//将套接字和IP、端口绑定bind(listenfd, (struct sockaddr*)serv_addr, sizeof(serv_addr));以上代码将创建的套接字与 IP 地址 127.0.0.1、端口 8080 进行绑定。(3)listen() 函数用于让套接字进入被动监听状态。所谓被动监听是指当没有客户端请求时套接字处于 “睡眠” 状态只有当接收到客户端请求时套接字才会被 “唤醒” 来响应请求。int listen(int sock, int backlog);sock 为需要进入监听状态的套接字backlog 为请求队列的最大长度。当套接字正在处理客户端请求时如果有新的请求进来套接字是没法处理的只能把它放进缓冲区待当前请求处理完毕后再从缓冲区中读取出来处理。如果不断有新的请求进来它们就按照先后顺序在缓冲区中排队直到缓冲区满。 这个缓冲区就称为请求队列(Request Queue)。当请求队列满时就不再接收新的请求对于 Linux客户端会收到 ECONNREFUSED 错误对于 Windows客户端会收到 WSAECONNREFUSED 错误。需要注意的是listen() 函数只是让套接字处于监听状态并没有接收请求。接收请求需要使用 accept() 函数。(4)accept() 函数当套接字处于监听状态时可以通过 accept() 函数来接收客户端请求。int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); 它的参数与 listen() 函数是一样的sock 为服务器端套接字addr 为 sockaddr_in 结构体变量addrlen 为参数 addr 的长度可由 sizeof() 求得。accept() 函数会返回一个新的套接字来和客户端通信addr 保存了客户端的 IP 地址和端口号而 sock 是服务器端的套接字大家注意区分。需要注意的是listen() 函数只是让套接字进入监听状态并没有真正接收客户端请求listen() 后面的代码会继续执行直到遇到 accept()。accept() 会阻塞程序执行直到有新的请求到来。 介绍完这几个核心的 Socket API我们来举一个 Server Socket 的示例从而让大家更好的理解这些函数具体是如何使用。simple_tcp_demo.c#include #include #include #include #include #include #define PORT 8080 int main(int argc, char const *argv[]) { int server_fd, new_socket, valread; struct sockaddr_in address; int opt 1; int addrlen sizeof(address); char buffer[1024] {0}; char *hello Hello from server; /* ① 创建监听套接字使用IPV4地址 */ if ((server_fd socket(AF_INET, SOCK_STREAM, 0)) 0) { perror(socket failed); exit(EXIT_FAILURE); } /* ② 设置socket相关配置 */ if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))) { perror(setsockopt); exit(EXIT_FAILURE); } /* AF_INET因特网使用的 IPv4 地址AF_INET6因特网使用功能的 IPv6 地址 */ address.sin_family AF_INET; /* INADDR_ANY就是指定地址为0.0.0.0的地址这个地址事实上表示不确定地址 或“所有地址”、“任意地址”。*/ address.sin_addr.s_addr INADDR_ANY; /* 网络端总是用Big endian而本机端却要视处理器体系而定比如x86就跟网络端的看法不同 使用的是Little endian。 htonsHost To Network Short它将本机端的字节序(endian)转换成了 网络端的字节序 */ address.sin_port htons( PORT ); /* ③ 绑定到本机地址端口为8080 */ if (bind(server_fd, (struct sockaddr *)address, sizeof(address))0) { perror(bind failed); exit(EXIT_FAILURE); } /* ④ 为了更好的理解 backlog 参数我们必须认识到内核为任何一个给定的监听套接口维护两个队列 - 未完成连接队列(incomplete connection queue)每个这样的 SYN 分节对应其中一项 已由某个客户发出并到达服务器而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口 处于 SYN_RCVD 状态。 - 已完成连接队列(completed connection queue)每个已完成 TCP 三次握手过程的客户 对应其中一项。这些套接口处于 ESTABLISHED 状态。*/ if (listen(server_fd, 3) 0) { perror(listen); exit(EXIT_FAILURE); } /* ⑤ accept()函数功能是从处于 established 状态的连接队列头部取出一个已经完成的连接 如果这个队列没有已经完成的连接accept()函数就会阻塞直到取出队列中已完成的用户连接为止。*/ /* 在实际开发过程中此处会使用 while(true) 或 for (;;) 循环处理用户请求*/ if ((new_socket accept(server_fd, (struct sockaddr *)address, (socklen_t*)addrlen))0) { perror(accept); exit(EXIT_FAILURE); } /* 读取客户端发送过来的数据 */ valread read( new_socket , buffer, 1024); printf(%s\n,buffer ); /* 返回数据给客户端 */ send(new_socket , hello , strlen(hello) , 0 ); printf(Hello message sent\n); return 0; } 对于上述 simple_tcp_demo.c 代码可以通过 gcc 进行编译并运行$ gcc simple_tcp_demo.c -o simple_tcp_demo ./simple_tcp_demo然后我们继续使用 nc 命令来连接该服务器$ nc localhost 8080hello denoHello from server% 如果一切正常的话在命令行终端可以看到以下输出结果$ tcp-server gcc simple_tcp_demo.c -o simple_tcp_demo ./simple_tcp_demohello denoHello message sent3.4 用户态和内核态Linux 操作系统的体系架构分为用户态和内核态(或者用户空间和内核空间)。内核从本质上看是一种软件 —— 控制计算机的硬件资源并提供上层应用程序运行的环境。 用户态即上层应用程序的活动空间应用程序的执行必须依托于内核提供的资源包括 CPU 资源、存储资源、I/O 资源等。为了使上层应用能够访问到这些资源内核必须为上层应用提供访问的接口即系统调用。系统调用时操作系统的最小功能单位。根据不同的应用场景不同的 Linux 发行版本提供的系统调用数量也不尽相同大致在 240-350 之间。这些系统调用组成了用户态跟内核态交互的基本接口。在实际的操作系统中为了屏蔽这些复杂的底层实现细节减轻开发者的负担操作系统为我们提供了库函数。它实现对系统调用的封装将简单的业务逻辑接口呈现给用户方便开发者调用。这里我们以 write() 函数为例来演示一下系统调用的过程(图片来源https://www.linuxbnb.net/home/adding-a-system-call-to-linux-arm-architecture/)除了系统调用外我们来简单介绍一下 Shell相信有的读者已经有写过 Shell 脚本。Shell 是一个特殊的应用程序俗称命令行本质上是一个命令解释器它下通系统调用上通各种应用通常充当着一种 “胶水” 的角色来连接各个小功能程序让不同程序能够以一个清晰的接口协同工作从而增强各个程序的功能。为了方便用户和系统交互一般情况下一个 Shell 对应一个终端终端是一个硬件设备呈现给用户的是一个图形化窗口。当然前面我们也提到过 Shell 是可编程的它拥有标准的 Shell 语法符合其语法的文本我们一般称它为 Shell 脚本。那么现在问题来了如何从用户态切换到内核态呢要实现状态切换可以通过以下三种方式系统调用其实系统调用本身就是中断但是软中断跟硬中断不同。异常如果当前进程运行在用户态如果这个时候发生了异常事件就会触发切换。外设中断当外设完成用户的请求时会向 CPU 发送中断信号。3.5 strace 命令strace 命令常用来跟踪进程执行时的系统调用和所接收的信号。在 Linux 世界进程不能直接访问硬件设备当进程需要访问硬件设备(比如读取磁盘文件接收网络数据等等)时必须由用户态模式切换至内核态模式通过系统调用访问硬件设备。strace 可以跟踪到一个进程产生的系统调用包括参数、返回值和执行消耗的时间。接下来我们将使用 strace 命令来跟踪 Deno TCP echo server 进程的系统调用流程。首先在命令行中输入以下命令[rootizuf6ghot555xyn666xm888 deno]# strace -ff -o ./echo_server deno run -A ./echo_server.ts-ff如果提供 -o filename则所有进程的跟踪结果输出到相应的 filename.pid 中pid 是各进程的进程号。-o filename将 strace 的输出写入文件 filename。当该命令成功运行之后在 /home/deno 当前目录下会生成以下两个文件-rw-r--r-- 1 root root 14173 May 17 13:16 echo_server.23178-rw-r--r-- 1 root root 137 May 17 13:15 echo_server.23179为了更直观的了解 23178 和 23179 这两个进程这里我们再通过 pstree -ap | grep deno 命令将 deno 相关的进程以树状图的形式展示出来[rootizuf6ghot555xyn666xm888 deno]# pstree -ap | grep deno | | -strace,23176 -ff -o ./echo_server deno run -A ./echo_server.ts | | -deno,23178 run -A ./echo_server.ts | | -{deno},23179 | |-grep,23285 --colorauto deno通过观察上述的进程树我们可以知道我们的 TCP echo server 进程对应的进程 ID 是 23178我们可以通过查看当前的网络状态来验证我们的猜测[rootizuf6ghot555xyn666xm888 deno]# netstat -natp | grep denotcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 23178/deno下面我们来打开 /home/deno/echo_server.23178 这个文件这个文件内容较多下面我们截取重要的部分echo-server-23178-listen从图中可知在 TCP echo server 启动的时候会调用 socket() 函数创建监听套接字之后会将该套接字与本机 0.0.0.0 地址和 8080 端口绑定起来只有这样流经该 IP 地址和端口的数据才能交给套接字处理。接着会继续调用 listen() 函数如 listen(8, 128) 让套接字进入被动监听状态。这时我们进入 /proc/23178/fd 目录使用 ls -al 查看当前目录的状态这里我们看到了预想的文件描述 —— 8 - socket:[30180040]。[rootizuf6ghot555xyn666xm888 fd]# ls -altotal 0dr-x------ 2 root root 0 May 17 13:16 .dr-xr-xr-x 9 root root 0 May 17 13:15 ..lrwx------ 1 root root 64 May 17 13:16 0 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 1 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 2 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 3 - anon_inode:[eventpoll]lr-x------ 1 root root 64 May 17 13:16 4 - pipe:[30180039]l-wx------ 1 root root 64 May 17 13:16 5 - pipe:[30180039]lrwx------ 1 root root 64 May 17 13:16 6 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 7 - /dev/pts/0lrwx------ 1 root root 64 May 17 13:16 8 - socket:[30180040]接下来我们使用 nc 命令来连接我们的 TCP echo server[rootizuf6ghot555xyn666xm888 deno]# nc localhost 8080前面我们已经知道当成功创建连接后/proc/23178/fd 目录下会增加一个新的文件描述符lrwx------ 1 root root 64 May 17 13:46 9 - socket:[30181765]前面我们已经介绍过了当套接字处于监听状态时可以通过 accept() 函数来接收客户端请求。此外accept() 函数会返回一个新的套接字来与客户端通信。下面我继续打开 /home/deno/echo_server.23178 这个文件这里我们找了与 accept 相关的内容echo-server-23178-accept由图可知文件描述符 9 所对应的 socket 套接字是在调用 nc 命令之后产生了当客户端与服务端建立连接后会返回一个新的套接字来与客户端通信。相信有的读者也有注意到图中除了 accept4 之外还出现了与 IO 多路复用相关的 epoll_ctl 和 epoll_wait 函数。epoll 是 Linux 内核的可扩展 I/O 事件通知机制。于 Linux 2.5.44 首度登场它设计目的旨在取代既有 POSIX select 与 poll 系统函数让需要大量操作文件描述符的程序得以发挥更优异的性能。epoll 实现的功能与 poll 类似都是监听多个文件描述符上的事件。epoll 与 FreeBSD 的 kqueue 类似底层都是由可配置的操作系统内核对象建构而成并以文件描述符(file descriptor)的形式呈现于用户空间。epoll 通过使用红黑树(RB-tree)搜索被监视的文件描述符(file descriptor)。关于 IO 多路复用与 epoll 相关的内容我们这里就不继续展开了后续有时间的话会专门写一下 IO 多路复用的文章介绍一下 select、poll 和 epoll 这些多路复用器的区别。这篇内容相对会比较难理解请小伙伴们多多包涵后续会来篇轻松一点的分析一下 Deno 标准库的相关实现。四、参考资源socket()函数用法详解Linux下/proc目录简介strace 跟踪进程中的系统调用怎样去理解Linux用户态和内核态Linux中的文件描述符与打开文件之间的关系▼往期精彩回顾▼ 在 TS 中如何减少重复代码在 TS 中如何减少重复代码 一文读懂 TS 中 Object, object, {} 类型之间的区别一文读懂 TS 中 Object, object, {} 类型之间的区别 遇到这些 TS 问题你会头晕么遇到这些 TS 问题你会头晕么聚焦全栈专注分享 Angular、TypeScript、Node.js 、Spring 技术栈等全栈干货。回复 0 进入重学TypeScript学习群回复 1 获取全栈修仙之路博客地址