<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>高并发 on JasperSao的博客</title><link>https://jasperstonnne.github.io/MyBlog/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/</link><description>Recent content in 高并发 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/%E9%AB%98%E5%B9%B6%E5%8F%91/index.xml" rel="self" type="application/rss+xml"/><item><title>Reactor 与 epoll：百万并发网络服务器</title><link>https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/</link><pubDate>Sat, 08 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/cover.svg" alt="Featured image of post Reactor 与 epoll：百万并发网络服务器" /&gt;&lt;h2 id="1-本节课要解决什么问题"&gt;1. 本节课要解决什么问题&#10;&lt;/h2&gt;&lt;p&gt;本节课不是直接实现完整 WebServer，而是先搭建一个能够承载大量网络连接的底层 I/O 框架。在这个框架之上，后续才能继续实现 HTTP 解析、路由、业务处理等功能。&lt;/p&gt;&#10;&lt;p&gt;核心目标有两个：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;使用 Linux &lt;code&gt;epoll&lt;/code&gt; 管理大量 socket。&lt;/li&gt;&#10;&lt;li&gt;使用 Reactor 思想，把不同 I/O 事件分发给不同回调函数。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;当前代码实现的是一个简单的多端口 Echo Server：客户端发来什么，服务端就回写什么。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-reactor-的核心思想"&gt;2. Reactor 的核心思想&#10;&lt;/h2&gt;&lt;p&gt;Reactor 可以概括为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O 对象（fd） → 发生事件（event） → 执行回调（callback）&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本例中的映射关系如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;I/O 对象&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;事件&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;回调函数&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;listenfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLIN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;有新连接到达&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;accept_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;clientfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLIN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;客户端数据可读&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;recv_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;clientfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLOUT&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;socket 当前可写&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;send_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;因此，主循环不需要判断“这个 fd 到底要做什么业务”，而只需要：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;调用 &lt;code&gt;epoll_wait()&lt;/code&gt; 等待事件。&lt;/li&gt;&#10;&lt;li&gt;从事件中取得 fd。&lt;/li&gt;&#10;&lt;li&gt;根据事件类型，调用该 fd 已经注册好的回调。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这就是“事件驱动”：没有事件时线程阻塞等待，有事件时才处理对应连接。&lt;/p&gt;&#10;&lt;h3 id="21-reactor-的两个重点"&gt;2.1 Reactor 的两个重点&#10;&lt;/h3&gt;&lt;ol&gt;&#10;&lt;li&gt;建立 &lt;code&gt;event&lt;/code&gt; 与 &lt;code&gt;callback&lt;/code&gt; 的对应关系。&lt;/li&gt;&#10;&lt;li&gt;为每一个 I/O 保存独立的连接状态。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;第二点非常重要。多个连接的数据不能共用一组读写缓冲区，否则连接之间会互相覆盖。因此每个 fd 都需要自己的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;读缓冲区 &lt;code&gt;rbuffer&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;当前读取长度 &lt;code&gt;rlength&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;写缓冲区 &lt;code&gt;wbuffer&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;待发送长度 &lt;code&gt;wlength&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;读事件回调&lt;/li&gt;&#10;&lt;li&gt;写事件回调&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-epoll-在-reactor-中的职责"&gt;3. &lt;code&gt;epoll&lt;/code&gt; 在 Reactor 中的职责&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 是 Linux 提供的 I/O 多路复用机制。一个线程可以通过一个 &lt;code&gt;epoll&lt;/code&gt; 实例监听大量 fd，而不必给每条 TCP 连接创建一个线程。&lt;/p&gt;&#10;&lt;p&gt;三个最重要的接口是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxevents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其作用分别为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_create1()&lt;/code&gt;：创建一个 epoll 实例。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_ctl()&lt;/code&gt;：添加、修改或删除所关注的 fd 和事件。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_wait()&lt;/code&gt;：等待已经注册的 fd 产生事件。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当前代码使用的是默认的 LT（Level Triggered，水平触发）模式，因为没有设置 &lt;code&gt;EPOLLET&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;LT 模式下，只要 fd 仍处于可读或可写状态，&lt;code&gt;epoll_wait()&lt;/code&gt; 就会继续报告它。若以后改成 ET（边沿触发），则 socket 必须设置成非阻塞，并且 &lt;code&gt;accept()&lt;/code&gt;、&lt;code&gt;recv()&lt;/code&gt; 等操作通常要循环执行，直到返回 &lt;code&gt;EAGAIN&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-代码中的连接对象"&gt;4. 代码中的连接对象&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;send_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;recv_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;accept_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="41-为什么需要-struct-conn"&gt;4.1 为什么需要 &lt;code&gt;struct conn&lt;/code&gt;&#10;&lt;/h3&gt;&lt;p&gt;TCP 是字节流协议，一次 &lt;code&gt;recv()&lt;/code&gt; 不保证收到一个完整业务消息：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&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;/ul&gt;&#10;&lt;p&gt;所以 Reactor 不仅要保存回调，还必须保存每个连接的处理进度。严格来说，后续还应增加：&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;li&gt;连接状态和超时信息；&lt;/li&gt;&#10;&lt;li&gt;动态扩容或链式缓冲区。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="42-为什么-accept_callback-与-recv_callback-使用-union"&gt;4.2 为什么 &lt;code&gt;accept_callback&lt;/code&gt; 与 &lt;code&gt;recv_callback&lt;/code&gt; 使用 &lt;code&gt;union&lt;/code&gt;&#10;&lt;/h3&gt;&lt;p&gt;监听 fd 的读事件表示“有连接可接受”，其回调是 &lt;code&gt;accept_cb()&lt;/code&gt;；客户端 fd 的读事件表示“有数据可接收”，其回调是 &lt;code&gt;recv_cb()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同一个 fd 在这里不会同时既是监听 fd 又是客户端 fd，因此两个函数指针可以共用同一块存储空间。&lt;code&gt;union&lt;/code&gt; 表达了二者“二选一”的关系。&lt;/p&gt;&#10;&lt;p&gt;不过，代码在赋值时使用了：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recv_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept_cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;语义上更清晰的写法是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept_cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用时也应根据 fd 的角色使用对应成员，或者直接将二者合并成一个名称更通用的 &lt;code&gt;read_callback&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="43-为什么可以用-fd-作为数组下标"&gt;4.3 为什么可以用 fd 作为数组下标&#10;&lt;/h3&gt;&lt;p&gt;当前代码使用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONNECTION_SIZE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并直接通过 &lt;code&gt;conn_list[fd]&lt;/code&gt; 找到连接。这种方法查找复杂度为 &lt;code&gt;O(1)&lt;/code&gt;，非常直接。&lt;/p&gt;&#10;&lt;p&gt;但它有两个条件：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;必须保证 &lt;code&gt;0 &amp;lt;= fd &amp;lt; CONNECTION_SIZE&lt;/code&gt;，否则会越界。&lt;/li&gt;&#10;&lt;li&gt;数组内存非常大。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;每个连接对象包含两个 1024 字节缓冲区，再加上长度、fd 和函数指针。100 多万个元素大约需要 2 GiB 以上的地址空间和可观的实际内存。因此“支持 100 万个 fd”不等于“采用这种结构后机器就一定能稳定承载 100 万连接”。生产实现通常会根据实际连接动态分配，或者按块分配连接对象与缓冲区。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="5-程序完整执行流程"&gt;5. 程序完整执行流程&#10;&lt;/h2&gt;&lt;h3 id="51-初始化-epoll"&gt;5.1 初始化 epoll&#10;&lt;/h3&gt;&lt;p&gt;代码中：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;参数在现代 Linux 中只要大于 0 即可，并不表示只能监听一个 fd。更推荐：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOLL_CLOEXEC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;创建失败时必须检查返回值。&lt;/p&gt;&#10;&lt;h3 id="52-创建-20-个监听-socket"&gt;5.2 创建 20 个监听 socket&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_PORTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sockfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;程序监听 &lt;code&gt;2000~2019&lt;/code&gt; 共 20 个端口。每个监听 fd 都注册 &lt;code&gt;EPOLLIN&lt;/code&gt;，并对应 &lt;code&gt;accept_cb()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;监听 fd 上的 &lt;code&gt;EPOLLIN&lt;/code&gt; 不是普通业务数据可读，而是表示全连接队列中存在已经完成握手、可以由 &lt;code&gt;accept()&lt;/code&gt; 取出的连接。&lt;/p&gt;&#10;&lt;h3 id="53-接受新连接"&gt;5.3 接受新连接&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;accept_cb()&lt;/code&gt; 的逻辑是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;调用 &lt;code&gt;accept()&lt;/code&gt;，获得 &lt;code&gt;clientfd&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;检查 &lt;code&gt;clientfd &amp;lt; 0&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;初始化 &lt;code&gt;conn_list[clientfd]&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;给客户端 fd 注册 &lt;code&gt;EPOLLIN&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这时事件映射从：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenfd + EPOLLIN → accept_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;变成新连接上的：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd + EPOLLIN → recv_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="54-接收数据"&gt;5.4 接收数据&#10;&lt;/h3&gt;&lt;p&gt;当客户端 fd 可读时执行 &lt;code&gt;recv_cb()&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前 Echo 逻辑把收到的数据复制到写缓冲区：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wlength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wlength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;随后把关注事件修改为 &lt;code&gt;EPOLLOUT&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd + EPOLLOUT → send_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="55-发送数据"&gt;5.5 发送数据&#10;&lt;/h3&gt;&lt;p&gt;当 fd 可写时执行 &lt;code&gt;send_cb()&lt;/code&gt;。数据发送结束后，再把关注事件切换回 &lt;code&gt;EPOLLIN&lt;/code&gt;，等待客户端下一次发送。&lt;/p&gt;&#10;&lt;p&gt;完整状态流转如下：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;监听 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept_cb：建立 clientfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;客户端 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recv_cb：接收并准备响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;客户端 EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;send_cb：发送响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;重新监听客户端 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="6-set_event-与事件注册"&gt;6. &lt;code&gt;set_event()&lt;/code&gt; 与事件注册&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;set_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前约定：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;flag == 1&lt;/code&gt;：使用 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;flag == 0&lt;/code&gt;：使用 &lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;功能上可行，但 &lt;code&gt;flag&lt;/code&gt; 的含义不直观。更清晰的设计是直接传入 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;、&lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt;，或者分别封装成 &lt;code&gt;add_event()&lt;/code&gt; 和 &lt;code&gt;mod_event()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同时必须检查 &lt;code&gt;epoll_ctl()&lt;/code&gt; 的返回值。例如：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;epoll_ctl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;set_event()&lt;/code&gt; 和 &lt;code&gt;event_rigister()&lt;/code&gt; 声明为返回 &lt;code&gt;int&lt;/code&gt;，但成功路径没有 &lt;code&gt;return&lt;/code&gt;，这属于未定义行为。应补充 &lt;code&gt;return 0&lt;/code&gt;。另外，&lt;code&gt;rigister&lt;/code&gt; 应改为 &lt;code&gt;register&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="7-主事件循环"&gt;7. 主事件循环&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;EPOLLOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;send_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;timeout == -1&lt;/code&gt; 表示没有事件时一直阻塞，避免空转消耗 CPU。&lt;/p&gt;&#10;&lt;h3 id="两个独立的-if-还是-if--else-if"&gt;两个独立的 &lt;code&gt;if&lt;/code&gt; 还是 &lt;code&gt;if ... else if&lt;/code&gt;？&#10;&lt;/h3&gt;&lt;p&gt;两个独立 &lt;code&gt;if&lt;/code&gt; 理论上允许同一次返回同时处理读、写事件；&lt;code&gt;else if&lt;/code&gt; 每轮只处理其中一种。&lt;/p&gt;&#10;&lt;p&gt;但不能简单认为两个 &lt;code&gt;if&lt;/code&gt; 一定更好。若读回调发现连接已经断开并关闭了 fd，随后继续执行写回调就可能访问已经失效或被复用的 fd。稳妥做法是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;先处理 &lt;code&gt;EPOLLERR&lt;/code&gt;、&lt;code&gt;EPOLLHUP&lt;/code&gt;、&lt;code&gt;EPOLLRDHUP&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;回调返回关闭状态后，不再处理该 fd 的其他事件。&lt;/li&gt;&#10;&lt;li&gt;再依据连接状态处理读写事件。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="8-课堂代码补充"&gt;8. 课堂代码补充&#10;&lt;/h2&gt;&lt;p&gt;这份代码主要是为了演示 Reactor 的整体流程，所以有些细节课堂上只是带了一下。自己再写一遍时，可以顺手补上下面这些地方。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 &lt;code&gt;send()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;课上代码里有一句：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里最后的 &lt;code&gt;count&lt;/code&gt; 还没有赋值，应该写成 &lt;code&gt;conn_list[fd].wlength&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;另外，一次 &lt;code&gt;send()&lt;/code&gt; 不一定能把 &lt;code&gt;wlength&lt;/code&gt; 个字节全部发完。可以在连接对象里再记一个 &lt;code&gt;woffset&lt;/code&gt;，每次发送后往后移动。数据还没发完就继续等 &lt;code&gt;EPOLLOUT&lt;/code&gt;，全部发完后再切回 &lt;code&gt;EPOLLIN&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 &lt;code&gt;recv()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;记住三个返回值：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;大于 0：本次收到的字节数；&lt;/li&gt;&#10;&lt;li&gt;等于 0：对端关闭连接；&lt;/li&gt;&#10;&lt;li&gt;小于 0：要结合 &lt;code&gt;errno&lt;/code&gt; 判断，非阻塞情况下可能只是暂时没有数据。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;不能让 &lt;code&gt;-1&lt;/code&gt; 继续参与后面的 &lt;code&gt;memcpy()&lt;/code&gt;，否则长度转换后可能越界。&lt;code&gt;EINTR&lt;/code&gt; 可以重试，&lt;code&gt;EAGAIN/EWOULDBLOCK&lt;/code&gt; 表示这次先读到这里，其他情况再按真正的错误处理。&lt;/p&gt;&#10;&lt;p&gt;还有一点：&lt;code&gt;recv()&lt;/code&gt; 收到的是一段字节，不会自动在末尾补 &lt;code&gt;\0&lt;/code&gt;。要用 &lt;code&gt;%s&lt;/code&gt; 打印时需要自己留位置并补结束符；如果处理二进制数据，就一直按长度来处理。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于非阻塞&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Reactor 一般要和非阻塞 socket 配合。监听 fd 和客户端 fd 都可以设置成 &lt;code&gt;O_NONBLOCK&lt;/code&gt;，避免某一次 &lt;code&gt;accept()&lt;/code&gt;、&lt;code&gt;recv()&lt;/code&gt; 或 &lt;code&gt;send()&lt;/code&gt; 卡住整个事件循环。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F_SETFL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F_GETFL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;O_NONBLOCK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果以后从 LT 改成 ET，&lt;code&gt;accept()&lt;/code&gt; 和 &lt;code&gt;recv()&lt;/code&gt; 通常都要放在循环里，一直处理到返回 &lt;code&gt;EAGAIN/EWOULDBLOCK&lt;/code&gt;。当前是 LT，一次只 &lt;code&gt;accept()&lt;/code&gt; 一个连接也能继续触发，不过连接集中到来时，循环取完会更快一些。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 fd 和连接状态&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;当前写法直接用 fd 作为 &lt;code&gt;conn_list&lt;/code&gt; 的下标，使用前要判断：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTION_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;连接关闭时也不只是 &lt;code&gt;close(fd)&lt;/code&gt;，对应的读写长度、偏移量和回调最好一起清空。因为 fd 的数字会被复用，下一个新连接有可能再次拿到同一个编号。&lt;/p&gt;&#10;&lt;p&gt;fd 也不能当作累计连接数。想每建立 1000 个连接打印一次，应该另外维护一个 &lt;code&gt;accepted_count&lt;/code&gt;，不能用 &lt;code&gt;clientfd % 1000&lt;/code&gt; 来判断。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;其他随手记下的点&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;socket()&lt;/code&gt;、&lt;code&gt;bind()&lt;/code&gt;、&lt;code&gt;listen()&lt;/code&gt;、&lt;code&gt;epoll_ctl()&lt;/code&gt;、&lt;code&gt;epoll_wait()&lt;/code&gt; 等系统调用都要看返回值；&lt;/li&gt;&#10;&lt;li&gt;监听 socket 通常会设置 &lt;code&gt;SO_REUSEADDR&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;listen(sockfd, 10)&lt;/code&gt; 中的 &lt;code&gt;10&lt;/code&gt; 是 backlog，不是服务器最多只能保持 10 条连接；&lt;/li&gt;&#10;&lt;li&gt;主循环除了 &lt;code&gt;EPOLLIN&lt;/code&gt;、&lt;code&gt;EPOLLOUT&lt;/code&gt;，还要留意 &lt;code&gt;EPOLLERR&lt;/code&gt;、&lt;code&gt;EPOLLHUP&lt;/code&gt; 和 &lt;code&gt;EPOLLRDHUP&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;对端关闭后继续发送可能触发 &lt;code&gt;SIGPIPE&lt;/code&gt;，可以忽略该信号，或在 &lt;code&gt;send()&lt;/code&gt; 时使用 &lt;code&gt;MSG_NOSIGNAL&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;TIME_SUB_MS(current, begin)&lt;/code&gt; 当前记录的是两次打印之间的间隔，不是从程序启动开始的总耗时。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-大包拆包与粘包"&gt;9. 大包、拆包与粘包&#10;&lt;/h2&gt;&lt;p&gt;代码中的读缓冲区只有 1024 字节。若客户端发送 2048 字节，一次 &lt;code&gt;recv()&lt;/code&gt; 可能只收到其中一部分；即使缓冲区足够大，TCP 也不保证一次接收对应一次发送。&lt;/p&gt;&#10;&lt;p&gt;因此不能把：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一次 send = 一个业务消息 = 一次 recv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当成可靠规律。&lt;/p&gt;&#10;&lt;p&gt;业务协议需要定义消息边界，常见方法有：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;固定长度消息。&lt;/li&gt;&#10;&lt;li&gt;特殊分隔符，例如文本协议中的换行。&lt;/li&gt;&#10;&lt;li&gt;消息头携带正文长度，例如“4 字节长度 + 消息体”。&lt;/li&gt;&#10;&lt;li&gt;HTTP 中使用请求行、首部、&lt;code&gt;Content-Length&lt;/code&gt; 或分块传输规则。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Reactor 收到数据后应先追加到该连接自己的缓冲区，再交给协议解析器：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recv → 追加到连接缓冲区 → 判断是否形成完整消息&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 未完整：保留并等待下次 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 已完整：取出消息并处理，剩余数据继续保留&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果固定数组空间不足，可以使用动态缓冲区、环形缓冲区或链式缓冲区。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="10-为什么会出现-too-many-open-files"&gt;10. 为什么会出现 &lt;code&gt;Too many open files&lt;/code&gt;&#10;&lt;/h2&gt;&lt;p&gt;报错对应 &lt;code&gt;errno == EMFILE&lt;/code&gt;（常见值为 24），表示当前进程已经达到可打开 fd 的上限。&lt;/p&gt;&#10;&lt;p&gt;一条 TCP 连接在客户端和服务端各自都占用一个 fd。百万连接测试至少要同时考虑：&lt;/p&gt;&#10;&lt;h3 id="101-进程级限制"&gt;10.1 进程级限制&#10;&lt;/h3&gt;&lt;p&gt;查看当前 shell 的限制：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -a&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;临时提高当前 shell 的 soft limit：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -n &lt;span class="m"&gt;1048576&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;程序必须从修改后的 shell 中启动，才能继承该限制。还要确保 hard limit 足够高。&lt;/p&gt;&#10;&lt;h3 id="102-系统级限制"&gt;10.2 系统级限制&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/fs/file-max&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/fs/file-nr&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;fs.file-max&lt;/code&gt; 是系统范围的文件句柄限制，&lt;code&gt;ulimit -n&lt;/code&gt; 是单进程/会话限制，两者不是同一个概念。持久化设置还取决于系统的 PAM、systemd unit 和发行版配置，不能只改一个参数就假定生效。&lt;/p&gt;&#10;&lt;h3 id="103-程序自身限制"&gt;10.3 程序自身限制&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;CONNECTION_SIZE&lt;/code&gt; 是否覆盖可能出现的 fd 值；&lt;/li&gt;&#10;&lt;li&gt;进程虚拟内存和实际物理内存是否足够；&lt;/li&gt;&#10;&lt;li&gt;每连接缓冲区是否过大；&lt;/li&gt;&#10;&lt;li&gt;是否存在 fd 泄漏；&lt;/li&gt;&#10;&lt;li&gt;epoll 实例和监听 socket 自己也会占 fd。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="11-为什么单个目标端口大约只能建立数万连接"&gt;11. 为什么单个目标端口大约只能建立数万连接&#10;&lt;/h2&gt;&lt;p&gt;一条 TCP 连接由五元组唯一标识：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;源 IP、源端口、目的 IP、目的端口、传输层协议&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在单台压测客户端连接单台服务器的单个端口时：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;源 IP 固定；&lt;/li&gt;&#10;&lt;li&gt;目的 IP 固定；&lt;/li&gt;&#10;&lt;li&gt;目的端口固定；&lt;/li&gt;&#10;&lt;li&gt;协议固定为 TCP；&lt;/li&gt;&#10;&lt;li&gt;主要只能变化源端口。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当可用临时源端口耗尽时，客户端可能报：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cannot assign requested address&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="111-一个需要校正的细节"&gt;11.1 一个需要校正的细节&#10;&lt;/h3&gt;&lt;p&gt;客户端临时端口范围并非固定为 &lt;code&gt;1024~65535&lt;/code&gt;。Linux 实际自动分配范围应通过下面的命令查看：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sysctl net.ipv4.ip_local_port_range&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不同系统、不同配置的范围可能不同，部分端口还可能被占用、保留，或暂时处于 &lt;code&gt;TIME_WAIT&lt;/code&gt; 等状态。因此可建立数量通常小于理论上的 65535。&lt;/p&gt;&#10;&lt;h3 id="112-为什么增加到-20-个服务端端口有效"&gt;11.2 为什么增加到 20 个服务端端口有效&#10;&lt;/h3&gt;&lt;p&gt;将目标端口从一个增加到 &lt;code&gt;2000~2019&lt;/code&gt;，改变了五元组中的“目的端口”。只要客户端把连接分散到这些端口，同一个源端口就可以与不同目的端口组成不同连接，理论连接空间随之扩大。&lt;/p&gt;&#10;&lt;p&gt;其他扩展方式还包括：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;使用多个客户端源 IP；&lt;/li&gt;&#10;&lt;li&gt;使用多台压测机；&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;不能仅用单台客户端的结果直接代表服务端的最大承载能力，因为压测端本身可能先耗尽端口、fd、CPU、内存或 conntrack 容量。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="12-nf_conntrack-应该怎样理解"&gt;12. &lt;code&gt;nf_conntrack&lt;/code&gt; 应该怎样理解&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;nf_conntrack&lt;/code&gt; 是 Linux Netfilter 的连接跟踪机制，防火墙、NAT 等功能会用到它。它不是 &lt;code&gt;epoll&lt;/code&gt; 工作所必需的组成部分。&lt;/p&gt;&#10;&lt;p&gt;如果测试路径启用了连接跟踪，并且连接数达到表容量，才需要观察：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sysctl net.netfilter.nf_conntrack_max&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/net/netfilter/nf_conntrack_count&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;旧资料中的 &lt;code&gt;ip_conntrack&lt;/code&gt; 是较早的名称；现代内核通常使用 &lt;code&gt;nf_conntrack&lt;/code&gt;。只有确实需要且模块尚未加载时，才考虑：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo modprobe nf_conntrack&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不应看到连接问题就盲目加载模块或增大参数。应先确认机器是否启用了防火墙/NAT、模块是否存在、表是否真的已满。在服务端还是客户端调整，取决于哪台机器实际承担连接跟踪；中间的 NAT/防火墙设备也可能成为瓶颈。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="13-百万连接断开时为什么-cpu-会突然升高"&gt;13. 百万连接断开时为什么 CPU 会突然升高&#10;&lt;/h2&gt;&lt;p&gt;如果一次性终止百万连接的一端，另一端会在短时间内集中处理大量 TCP 状态变化与关闭事件，例如 FIN/RST、epoll 唤醒、连接对象清理、定时器更新等。&lt;/p&gt;&#10;&lt;p&gt;这会形成“惊群式的业务处理洪峰”或资源回收洪峰，表现为 CPU 短时暴增。它不一定表示程序死循环，而可能是系统在集中处理积压事件。&lt;/p&gt;&#10;&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;li&gt;使用高效定时器结构，如时间轮；&lt;/li&gt;&#10;&lt;li&gt;避免每连接大量内存分配和释放；&lt;/li&gt;&#10;&lt;li&gt;做好背压和过载保护；&lt;/li&gt;&#10;&lt;li&gt;使用多 Reactor、多线程或多进程分摊事件处理；&lt;/li&gt;&#10;&lt;li&gt;用火焰图、&lt;code&gt;perf&lt;/code&gt;、系统指标确认 CPU 真正消耗在哪里。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;优化目标不是保证 CPU 永不升高，而是让峰值可控、持续时间缩短，并保证服务仍能继续响应。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="14-百万并发不等于百万-qps"&gt;14. “百万并发”不等于“百万 QPS”&#10;&lt;/h2&gt;&lt;p&gt;性能测试至少要区分四个维度：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;指标&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;并发连接数&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;同一时刻保持的连接数量&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;QPS/吞吐量&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;单位时间完成的请求或消息数量&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;延迟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一次请求从发出到收到响应所需时间，通常关注 P50/P95/P99&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;测试用例&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;报文大小、收发频率、连接是否长连、业务是否计算或访问存储&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;保持 100 万条空闲 TCP 连接，和让 100 万条连接持续高速收发数据，是完全不同的负载。前者更考验 fd、内存和内核连接管理；后者还会迅速消耗 CPU、网络带宽、内存带宽以及业务依赖资源。&lt;/p&gt;&#10;&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;所有相关内核参数与 fd 限制；&lt;/li&gt;&#10;&lt;li&gt;连接建立速率；&lt;/li&gt;&#10;&lt;li&gt;活跃连接与空闲连接比例；&lt;/li&gt;&#10;&lt;li&gt;请求/响应大小和频率；&lt;/li&gt;&#10;&lt;li&gt;QPS、吞吐量、错误率和延迟分位数；&lt;/li&gt;&#10;&lt;li&gt;CPU、内存、网络、fd、conntrack 使用率；&lt;/li&gt;&#10;&lt;li&gt;测试持续时间和连接关闭方式。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="15-本节课一句话总结"&gt;15. 本节课一句话总结&#10;&lt;/h2&gt;&lt;p&gt;Reactor 的本质不是“用了 epoll 就很快”，而是把每个 fd 的状态独立保存起来，由事件循环将 &lt;code&gt;I/O + event&lt;/code&gt; 精确分发给相应 callback；百万连接则是在这个正确模型之上，再共同解决 fd、内存、端口空间、内核网络栈和压测方法等系统性限制。&lt;/p&gt;&#10;&lt;hr&gt;&#10;</description></item></channel></rss>