<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>I/O 多路复用 on JasperSao的博客</title><link>https://jasperstonnne.github.io/MyBlog/tags/i/o-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/</link><description>Recent content in I/O 多路复用 on JasperSao的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 30 Aug 2026 08:12:59 +0000</lastBuildDate><atom:link href="https://jasperstonnne.github.io/MyBlog/tags/i/o-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/index.xml" rel="self" type="application/rss+xml"/><item><title>网络 I/O 与 I/O 多路复用</title><link>https://jasperstonnne.github.io/MyBlog/p/network-io-and-io-multiplexing/</link><pubDate>Thu, 06 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/network-io-and-io-multiplexing/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/network-io-and-io-multiplexing/cover.svg" alt="Featured image of post 网络 I/O 与 I/O 多路复用" /&gt;&lt;h2 id="一网络-io-的使用场景"&gt;一、网络 I/O 的使用场景&#10;&lt;/h2&gt;&lt;p&gt;网络是后端服务开发中的重要环节。生活中大量功能都依赖客户端与服务端之间的网络通信。&lt;/p&gt;&#10;&lt;h3 id="1-微信发送文字语音和视频"&gt;1. 微信发送文字、语音和视频&#10;&lt;/h3&gt;&lt;p&gt;当用户在微信发送消息时，大致过程是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;发送方微信&#10; ↓&#10;通过网络将数据发送给微信服务器&#10; ↓&#10;服务器找到接收方&#10; ↓&#10;将数据转发给接收方微信&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;文字、语音、图片、视频在网络上传输时，最终都会被转换成字节数据。&lt;/p&gt;&#10;&lt;p&gt;网络 I/O 主要关心：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;数据从哪里读取&#10;数据向哪里发送&#10;读取了多少字节&#10;发送了多少字节&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;网络 I/O 本身并不关心这些字节具体表示文字、图片还是视频。&lt;/p&gt;&#10;&lt;h3 id="2-刷抖音"&gt;2. 刷抖音&#10;&lt;/h3&gt;&lt;p&gt;刷视频时：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;抖音 App 请求视频&#10; ↓&#10;服务器返回视频信息和资源地址&#10; ↓&#10;App 通过网络下载视频数据&#10; ↓&#10;客户端解码并播放&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通常视频会被切分成多个数据块，App 可以一边下载、一边播放。&lt;/p&gt;&#10;&lt;h3 id="3-使用-git-clone"&gt;3. 使用 &lt;code&gt;git clone&lt;/code&gt;&#10;&lt;/h3&gt;&lt;p&gt;执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;git clone 仓库地址&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;大致过程是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;本地 Git 客户端连接 GitHub&#10; ↓&#10;向 GitHub 请求仓库数据&#10; ↓&#10;GitHub 通过网络发送 Git 对象&#10; ↓&#10;本地接收、校验和解压&#10; ↓&#10;形成本地仓库&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代码能够到达本地，本质上也是客户端和 GitHub 服务端之间进行了网络 I/O。&lt;/p&gt;&#10;&lt;p&gt;这些场景都可以概括成：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端发起请求 → 服务端处理请求 → 服务端返回响应&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="二什么是网络-io"&gt;二、什么是网络 I/O&#10;&lt;/h2&gt;&lt;p&gt;网络 I/O 是建立在客户端和服务端之间的数据传输过程，类似二者之间连接的网络管道。例如十个客户端分别连接服务器，服务端通常会获得十个连接 fd：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端 A ←→ fd 4&#10;客户端 B ←→ fd 5&#10;客户端 C ←→ fd 6&#10;...&#10;客户端 J ←→ fd 13&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;随着客户端数量增加，fd 数量也会增加，于是就产生了一个重要问题：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;服务端如何同时管理大量 fd？&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;这正是后面需要学习 &lt;code&gt;select&lt;/code&gt;、&lt;code&gt;poll&lt;/code&gt; 和 &lt;code&gt;epoll&lt;/code&gt; 的原因。&lt;/p&gt;&#10;&lt;h2 id="三socket-与-fd"&gt;三、socket 与 fd&#10;&lt;/h2&gt;&lt;p&gt;在 Linux 中，Linux 通过 socket API 通信。创建 socket：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int sockfd = socket(AF_INET, SOCK_STREAM, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;参数的含义：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;AF_INET&lt;/code&gt;：IPv4&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;SOCK_STREAM&lt;/code&gt;：面向字节流，对应 TCP&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;0&lt;/code&gt;：使用该组合默认的 TCP 协议&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;fd 本质是进程中的非负整数（此处了解，记忆也没啥用）：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;0：标准输入&#10;1：标准输出&#10;2：标准错误&#10;3：监听 socket&#10;4：客户端连接 A&#10;5：客户端连接 B&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在当前服务端进程中，一个已连接 socket 的 fd 通常对应一条 TCP 连接。（存在 ACCEPT 的情况）&lt;/p&gt;&#10;&lt;h2 id="四tcp-服务端的简便理解"&gt;四、TCP 服务端的简便理解&#10;&lt;/h2&gt;&lt;p&gt;可以把 TCP 服务端理解成酒店：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;socket()&lt;/code&gt;：聘请迎宾员&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;bind()&lt;/code&gt;：安排迎宾员在哪个门口工作&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;listen()&lt;/code&gt;：正式开始迎宾&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;accept()&lt;/code&gt;：接待一位到达的客人&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;recv()&lt;/code&gt;：听客人提出需求&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;send()&lt;/code&gt;：将处理结果交给客人&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;close()&lt;/code&gt;：结束服务&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;说说两类重要 fd：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;1. 监听 fd&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;定义完 sockfd 经过 bind、listen 后，sockfd 就成为监听 fd。主要职责是监听新连接，不负责收发数据。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;2. 连接 fd&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;参考代码中的 &lt;code&gt;int clientfd = accept(sockfd, ...);&lt;/code&gt;。&lt;code&gt;accept()&lt;/code&gt; 成功后会返回一个新的 fd：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;sockfd：负责监听新客户端&#10;clientfd：负责和某个客户端通信&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关系如下：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; ┌→ clientfd 4 ←→ 客户端 A&#10;listenfd → accept() ─┼→ clientfd 5 ←→ 客户端 B&#10; └→ clientfd 6 ←→ 客户端 C&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;监听 fd 通常只有一个，但每建立一个客户端连接，服务端就会获得一个新的连接 fd。&lt;/p&gt;&#10;&lt;h2 id="五服务端代码执行流程"&gt;五、服务端代码执行流程&#10;&lt;/h2&gt;&lt;h3 id="1-创建-socket"&gt;1. 创建 socket&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int sockfd = socket(AF_INET, SOCK_STREAM, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;作用：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;在内核中创建 socket 对象。&lt;/li&gt;&#10;&lt;li&gt;返回一个 fd。&lt;/li&gt;&#10;&lt;li&gt;当前还没有绑定 IP 和端口。&lt;/li&gt;&#10;&lt;li&gt;当前也没有开始监听。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;更准确的变量名可以理解为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int listenfd;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为它后面会被设置为监听 socket。&lt;/p&gt;&#10;&lt;h3 id="2-准备服务器地址"&gt;2. 准备服务器地址&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;struct sockaddr_in servaddr;&#10;&#10;servaddr.sin_family = AF_INET;&#10;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);&#10;servaddr.sin_port = htons(2000);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="sin_family"&gt;&lt;code&gt;sin_family&lt;/code&gt;&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;servaddr.sin_family = AF_INET;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;表示使用 IPv4。&lt;/p&gt;&#10;&lt;h4 id="inaddr_any"&gt;&lt;code&gt;INADDR_ANY&lt;/code&gt;&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;INADDR_ANY&lt;/code&gt; 表示监听本机所有 IPv4 网络接口，对应：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;0.0.0.0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;假设服务器有多个 IP：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;127.0.0.1&#10;192.168.1.10&#10;10.0.0.10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;绑定 &lt;code&gt;0.0.0.0&lt;/code&gt;，表示这些本机地址上的 2000 端口都可以接收连接。&lt;/p&gt;&#10;&lt;h4 id="htons"&gt;&lt;code&gt;htons()&lt;/code&gt;&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;servaddr.sin_port = htons(2000);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;htons&lt;/code&gt; 表示：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;host to network short&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;它把主机字节序的 16 位端口号转换成网络字节序。&lt;/p&gt;&#10;&lt;p&gt;同理：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;htonl&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;表示把 32 位数据从主机字节序转换成网络字节序。&lt;/p&gt;&#10;&lt;p&gt;端口范围是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;0～65535&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通常：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;0&lt;/code&gt;：让操作系统自动分配端口。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;1～1023&lt;/code&gt;：传统意义上的特权端口。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;1024～65535&lt;/code&gt;：普通程序通常可以使用。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;但 1024 以上的端口也可能已经被其他程序占用。&lt;/p&gt;&#10;&lt;h3 id="3-绑定-ip-和端口"&gt;3. 绑定 IP 和端口&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;bind(sockfd,&#10; (struct sockaddr *)&amp;amp;servaddr,&#10; sizeof(struct sockaddr));&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;bind()&lt;/code&gt; 将 socket 与本地 IP、端口建立联系：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;sockfd ←→ 0.0.0.0:2000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以理解为：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;给迎宾人员安排具体的工作地点。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;如果端口已经被其他服务绑定，可能出现：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Address already in use&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;你的代码通过下面的方式查看错误：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;printf(&amp;#34;bind error:%s\n&amp;#34;, strerror(errno));&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;不过如果 &lt;code&gt;bind()&lt;/code&gt; 失败，程序理论上不应该继续执行 &lt;code&gt;listen()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="4-进入监听状态"&gt;4. 进入监听状态&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;listen(sockfd, 10);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行成功后，socket 从普通 socket 变成监听 socket。&lt;/p&gt;&#10;&lt;p&gt;此时可以使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ss -lntp | grep &amp;#39;:2000&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;查看监听状态。&lt;/p&gt;&#10;&lt;p&gt;可能看到：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;LISTEN 0 10 0.0.0.0:2000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h3 id="5-接收客户端连接"&gt;5. 接收客户端连接&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int clientfd = accept(&#10; sockfd,&#10; (struct sockaddr *)&amp;amp;clientaddr,&#10; &amp;amp;len&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果当前没有已经建立完成的客户端连接，阻塞模式下的 &lt;code&gt;accept()&lt;/code&gt; 会暂停程序：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;程序阻塞在 accept()&#10; ↓&#10;等待客户端连接&#10; ↓&#10;客户端完成 TCP 三次握手&#10; ↓&#10;accept() 返回 clientfd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;accept()&lt;/code&gt; 不会把原来的 &lt;code&gt;sockfd&lt;/code&gt; 变成客户端连接。&lt;/p&gt;&#10;&lt;p&gt;它会返回一个全新的 &lt;code&gt;clientfd&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h2 id="六代码中的三个服务端版本代码文末附"&gt;六、代码中的三个服务端版本（代码文末附）&#10;&lt;/h2&gt;&lt;h3 id="第一阶段只处理一个客户端的一次数据"&gt;第一阶段：只处理一个客户端的一次数据&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#if 0&#10; int clientfd = accept(...);&#10;&#10; char buffer[1024] = {0};&#10; int count = recv(clientfd, buffer, 1024, 0);&#10;&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10;&#10; count = send(clientfd, buffer, count, 0);&#10;#endif&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行过程：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;accept 一个客户端&#10; ↓&#10;recv 一次&#10; ↓&#10;send 一次&#10; ↓&#10;后面不再 accept&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个版本只能：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;接收一个客户端。&lt;/li&gt;&#10;&lt;li&gt;读取一次数据。&lt;/li&gt;&#10;&lt;li&gt;返回一次数据。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果客户端想再次发送，服务端没有循环继续读取。&lt;/p&gt;&#10;&lt;h3 id="第二阶段循环接收客户端但串行处理"&gt;第二阶段：循环接收客户端，但串行处理&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#elif 0&#10;while (1) {&#10; int clientfd = accept(...);&#10;&#10; char buffer[1024] = {0};&#10; int count = recv(clientfd, buffer, 1024, 0);&#10;&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10;&#10; count = send(clientfd, buffer, count, 0);&#10;}&#10;#endif&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里增加了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;while (1)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此服务端可以不断接收新客户端。&lt;/p&gt;&#10;&lt;p&gt;但是它仍然是串行处理：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;accept 客户端 A&#10; ↓&#10;recv 等待 A 发送数据&#10; ↓&#10;send 返回给 A&#10; ↓&#10;重新 accept 客户端 B&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关键问题在这里：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recv(clientfd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;默认情况下，&lt;code&gt;recv()&lt;/code&gt; 是阻塞的。&lt;/p&gt;&#10;&lt;p&gt;假设客户端 A 已经连接，但一直没有发送数据：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;服务端阻塞在 A 的 recv()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;此时即使客户端 B 已经发起连接，应用程序也暂时无法回到 &lt;code&gt;accept()&lt;/code&gt; 处理 B。&lt;/p&gt;&#10;&lt;h3 id="第三阶段一个客户端对应一个线程"&gt;第三阶段：一个客户端对应一个线程&#10;&lt;/h3&gt;&lt;p&gt;当前实际编译的是 &lt;code&gt;#else&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#else&#10;while (1) {&#10; int clientfd = accept(...);&#10;&#10; pthread_t thid;&#10; pthread_create(&#10; &amp;amp;thid,&#10; NULL,&#10; client_thread,&#10; &amp;amp;clientfd&#10; );&#10;}&#10;#endif&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每接收一个客户端，创建一个线程。&lt;/p&gt;&#10;&lt;p&gt;线程函数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;void *client_thread(void *arg)&#10;{&#10; int clientfd = *(int *)arg;&#10;&#10; while (1) {&#10; char buffer[1024] = {0};&#10;&#10; int count = recv(&#10; clientfd,&#10; buffer,&#10; 1024,&#10; 0&#10; );&#10;&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10;&#10; count = send(&#10; clientfd,&#10; buffer,&#10; count,&#10; 0&#10; );&#10; }&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;运行关系变成：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;主线程&#10; │&#10; ├─ accept 客户端 A&#10; │ └─ 创建线程 1 → recv(A)&#10; │&#10; ├─ accept 客户端 B&#10; │ └─ 创建线程 2 → recv(B)&#10; │&#10; └─ accept 客户端 C&#10; └─ 创建线程 3 → recv(C)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;主线程只负责：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;不断执行 accept()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;工作线程负责：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recv() → 处理数据 → send()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样即使线程 1 阻塞在客户端 A 的 &lt;code&gt;recv()&lt;/code&gt;，主线程仍然可以继续 &lt;code&gt;accept()&lt;/code&gt; 客户端 B。&lt;/p&gt;&#10;&lt;p&gt;准确来说，这是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个连接对应一个线程。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;不是严格的”一次请求对应一个线程”，因为一个连接线程内部可以通过 &lt;code&gt;while&lt;/code&gt; 处理多次请求。&lt;/p&gt;&#10;&lt;h2 id="七对于阶段三一个连接一个线程的问题"&gt;七、对于阶段三一个连接一个线程的问题&#10;&lt;/h2&gt;&lt;p&gt;这种模型简单直观，但连接数量增大后会产生问题。&lt;/p&gt;&#10;&lt;h3 id="1-线程需要内存"&gt;1. 线程需要内存&#10;&lt;/h3&gt;&lt;p&gt;每创建一个线程，都需要线程栈及相关管理资源。连接越多，线程越多，百万并发的时候&amp;hellip;&lt;/p&gt;&#10;&lt;h3 id="2-上下文切换成本"&gt;2. 上下文切换成本&#10;&lt;/h3&gt;&lt;p&gt;CPU 核心数量有限。&lt;/p&gt;&#10;&lt;p&gt;假设服务器只有 8 个 CPU 核心，却创建了 5000 个线程，操作系统必须不停地：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;保存线程 A 的状态&#10;切换到线程 B&#10;保存线程 B 的状态&#10;切换到线程 C&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;线程太多时，大量 CPU 时间可能消耗在线程调度上。&lt;/p&gt;&#10;&lt;h3 id="3-大部分线程可能只是在等待"&gt;3. 大部分线程可能只是在等待&#10;&lt;/h3&gt;&lt;p&gt;很多网络连接并不是一直都有数据。&lt;/p&gt;&#10;&lt;p&gt;例如即时通信中的长连接：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端保持在线&#10;但大部分时间没有发送消息&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对应线程会长期阻塞在：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recv(clientfd, ...);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果一个空闲连接占用一个线程，就会浪费大量线程资源。&lt;/p&gt;&#10;&lt;h2 id="代码"&gt;代码&#10;&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt; // printf&#10;#include &amp;lt;string.h&amp;gt; // memset&#10;#include &amp;lt;sys/socket.h&amp;gt;&#10;#include &amp;lt;arpa/inet.h&amp;gt; // htonl, htons&#10;#include &amp;lt;errno.h&amp;gt; // errno&#10;#include &amp;lt;netinet/in.h&amp;gt; // struct sockaddr_in&#10;#include &amp;lt;unistd.h&amp;gt; // close&#10;#include &amp;lt;pthread.h&amp;gt;&#10;&#10;void *client_thread(void *arg) {&#10; int clientfd = *(int *)arg;&#10; while (1) {&#10; char buffer[1024] = {0};&#10; int count = recv(clientfd, buffer, 1024, 0); // 全部阻塞在 recv 上&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10; count = send(clientfd, buffer, count, 0);&#10; printf(&amp;#34;SEND:%s\n&amp;#34;, count);&#10; }&#10;}&#10;&#10;int main() {&#10; int sockfd = socket(AF_INET, SOCK_STREAM, 0); // create socket 聘请迎宾者&#10; // 绑定本地端口&#10; struct sockaddr_in servaddr;&#10; servaddr.sin_family = AF_INET;&#10; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0&#10; servaddr.sin_port = htons(2000); // 0-1023 系统默认，1024 之后都可以用，不能与其他的冲突&#10; // 在哪个门迎宾，安排的位置&#10; if (-1 == bind(sockfd, (struct sockaddr *)&amp;amp;servaddr, sizeof(struct sockaddr))) {&#10; printf(&amp;#34;bind error:%s\n&amp;#34;, strerror(errno));&#10; }&#10; // 登记绑定&#10; listen(sockfd, 10); // 正式上榜&#10; printf(&amp;#34;listen finished\n&amp;#34;);&#10; struct sockaddr_in clientaddr;&#10; socklen_t len = sizeof(clientaddr);&#10;#if 0&#10; printf(&amp;#34;accept\n&amp;#34;);&#10; int clientfd = accept(sockfd, (struct sockaddr *)&amp;amp;clientaddr, &amp;amp;len); // 建立链接&#10; printf(&amp;#34;accept finished\n&amp;#34;);&#10; char buffer[1024] = {0};&#10; int count = recv(clientfd, buffer, 1024, 0);&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10; count = send(clientfd, buffer, count, 0);&#10; printf(&amp;#34;SEND:%s\n&amp;#34;, count);&#10;#elif 0&#10; while (1) {&#10; printf(&amp;#34;accept\n&amp;#34;);&#10; int clientfd = accept(sockfd, (struct sockaddr *)&amp;amp;clientaddr, &amp;amp;len); // 建立链接&#10; printf(&amp;#34;accept finished\n&amp;#34;);&#10; char buffer[1024] = {0};&#10; int count = recv(clientfd, buffer, 1024, 0); // 全部阻塞在 recv 上&#10; printf(&amp;#34;RECV:%s\n&amp;#34;, buffer);&#10; count = send(clientfd, buffer, count, 0);&#10; printf(&amp;#34;SEND:%s\n&amp;#34;, count);&#10; } // 多个 io 如何接收数据&#10;#else&#10; while (1) {&#10; printf(&amp;#34;accept\n&amp;#34;);&#10; int clientfd = accept(sockfd, (struct sockaddr *)&amp;amp;clientaddr, &amp;amp;len); // 建立链接&#10; printf(&amp;#34;accept finished\n&amp;#34;);&#10; pthread_t thid;&#10; pthread_create(&amp;amp;thid, NULL, client_thread, &amp;amp;clientfd);&#10; }&#10;#endif&#10; getchar();&#10; printf(&amp;#34;exit\n&amp;#34;);&#10; return 0;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;</description></item></channel></rss>