<?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/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/</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/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/index.xml" rel="self" type="application/rss+xml"/><item><title>KV 存储项目：客户端测试、压力测试与多引擎扩展</title><link>https://jasperstonnne.github.io/MyBlog/p/kv-store-testing-multi-engine/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/kv-store-testing-multi-engine/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/kv-store-testing-multi-engine/cover.svg" alt="Featured image of post KV 存储项目：客户端测试、压力测试与多引擎扩展" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;阅读目标：沿着“建立可重复的测试—用压力测试暴露状态问题—在现有协议后接入第二种 KV 引擎”这条主线，回顾项目从数组存储继续向多引擎结构演进的过程。&lt;/p&gt;&#10;&lt;p&gt;本文承接 &lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/kv-store-network-layer/" &gt;KV 存储项目网络层：统一接入 Reactor、Proactor 与协程&lt;/a&gt; 和 &lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/kv-store-protocol-array/" &gt;KV 存储项目：协议层与数组存储&lt;/a&gt;。网络层的 Reactor、Proactor、ntyco，以及 &lt;code&gt;kvs_protocol()&lt;/code&gt;、token 切分、数组五种操作等内容，前文已经完整讲过，这里只保留理解本阶段所需的连接点。&lt;/p&gt;&#10;&lt;p&gt;本文以学习笔记和当前代码为准，重点解释实现过程、测试现象与架构关系，不展开红黑树算法本身。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="1-这一阶段解决什么问题"&gt;1. 这一阶段解决什么问题&#10;&lt;/h2&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;客户端发送文本命令&#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;网络层接收数据&#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;kvs_protocol() 切分 token&#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;kvs_filter_protocol() 识别命令&#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;数组引擎执行 SET / GET / DEL / MOD / EXIST&#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;协议层生成 OK / EXIST / NO EXIST / value&#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;网络层把响应发回客户端&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&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;协议层能否在数组之外接入新的存储引擎？&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;Makefile 统一构建&#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;TCP 客户端实现自动化功能测试&#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;重复执行测试并粗测 QPS&#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;通过高次数测试定位数组状态问题&#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;保留原有数组引擎，接入红黑树引擎&#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;用同一组语义测试第二种引擎&#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="2-用-makefile-固定构建过程"&gt;2. 用 Makefile 固定构建过程&#10;&lt;/h2&gt;&lt;p&gt;随着源文件增加，手工重复输入完整的 &lt;code&gt;gcc&lt;/code&gt; 命令会越来越不方便。最初可以直接把编译过程写进 &lt;code&gt;all&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;gcc -o kvstore kvstore.c reactor.c proactor.c kvs_array.c ntyco.c &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;-I ./NtyCo/core/ -L ./NtyCo/ -luring -lntyco&#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="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;rm -rf kvstore&#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;部分&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;gcc&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;编译并链接 C 程序&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;target&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;kvstore&lt;/code&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;sources&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;kvstore.c&lt;/code&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;flags&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-I&lt;/code&gt;、&lt;code&gt;-L&lt;/code&gt;、&lt;code&gt;-l&lt;/code&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;把这些部分提取成变量后，Makefile 中各项职责更清楚：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gcc&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; kvstore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SRCS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; kvstore.c reactor.c proactor.c kvs_array.c ntyco.c kvs_rbtree.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;INC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -I ./NtyCo/core/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LIBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -L ./NtyCo/ -luring -lntyco&#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="nf"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;$(&lt;/span&gt;CC&lt;span class="k"&gt;)&lt;/span&gt; -o &lt;span class="k"&gt;$(&lt;/span&gt;TARGET&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;SRCS&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;INC&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;LIBS&lt;span class="k"&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="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;rm -rf kvstore&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前阶段加入红黑树后，构建层面的变化很小：在 &lt;code&gt;SRCS&lt;/code&gt; 中加入 &lt;code&gt;kvs_rbtree.c&lt;/code&gt;。也就是说，存储引擎虽然增加了，但整个项目仍由同一个 target 统一构建。&lt;/p&gt;&#10;&lt;h3 id="21-ntyco-是项目的外部依赖"&gt;2.1 NtyCo 是项目的外部依赖&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;INC&lt;/code&gt; 和 &lt;code&gt;LIBS&lt;/code&gt; 都引用了 &lt;code&gt;./NtyCo/&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;INC → ./NtyCo/core/ 中的头文件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LIBS → ./NtyCo/ 中的 libntyco&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，Makefile 描述的是“依赖已经存在时怎样编译”。如果本地没有 &lt;code&gt;NtyCo&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;准备第三方依赖&#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;编译当前项目&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Makefile 的价值不只在于少输入一条命令，更重要的是把当前项目由哪些文件、头文件路径和库组成固定下来。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-客户端测试用例把人工验证变成固定流程"&gt;3. 客户端测试用例：把人工验证变成固定流程&#10;&lt;/h2&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;1. 建立 TCP 连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 发送一条协议命令&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 接收服务端响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 将实际响应与预期响应比较&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 一致则 PASS，不一致则 FAILED&#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;函数&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;connect_tcpserver()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;创建 socket，并连接指定 IP 和端口&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;send_msg()&lt;/code&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;&lt;code&gt;recv_msg()&lt;/code&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;&lt;code&gt;testcase()&lt;/code&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;&lt;code&gt;array_testcase()&lt;/code&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;&lt;code&gt;rbtree_testcase()&lt;/code&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;代码中的注释“一个函数只做一件事”在这里得到了体现：网络连接、一次断言和一组业务场景分别由不同函数承担。&lt;/p&gt;&#10;&lt;h3 id="31-一个测试用例包含什么"&gt;3.1 一个测试用例包含什么&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;testcase()&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testcase&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;connfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;casename&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;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;&#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;connfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;已连接的 TCP socket&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;与 KV 服务端的连接&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;msg&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;SET Dad Jasper&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;pattern&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;OK\r\n&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;casename&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;SET-Dad&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;测试执行过程可以压缩为：&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;msg ──send──&amp;gt; KV 服务端&#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; └── response ──recv──&amp;gt; result&#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; strcmp(result, pattern)&#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; 相同 不同&#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; PASS FAILED&#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;TCP 收发 + 协议解析 + 引擎操作 + 响应生成&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="4-一组完整场景不只是五条命令"&gt;4. 一组完整场景不只是“五条命令”&#10;&lt;/h2&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;SET / GET / DEL / MOD / EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但“覆盖五个命令”不等于每个命令只执行一次。当前 &lt;code&gt;array_testcase()&lt;/code&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 style="text-align: right"&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;&#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 style="text-align: right"&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;SET Dad Jasper&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&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 style="text-align: right"&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Jasper&lt;/code&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 style="text-align: right"&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;MOD Dad Sao&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&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 style="text-align: right"&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Sao&lt;/code&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 style="text-align: right"&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST&lt;/code&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 style="text-align: right"&gt;6&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;DEL Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&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 style="text-align: right"&gt;7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;NO EXIST&lt;/code&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 style="text-align: right"&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;MOD Dad Jasper&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;NO EXIST&lt;/code&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 style="text-align: right"&gt;9&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST Dad&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;NO EXIST&lt;/code&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;这九步不是彼此独立的命令，而是一条状态变化链：&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;不存在&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ SET&#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;Dad = Jasper&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ MOD&#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;Dad = Sao&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ DEL&#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;不存在&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;测试在每个关键状态后使用 &lt;code&gt;GET&lt;/code&gt; 或 &lt;code&gt;EXIST&lt;/code&gt; 进行确认。这样不仅验证命令返回了什么，还验证前一条命令是否真正改变了存储状态。&lt;/p&gt;&#10;&lt;h3 id="41-测试覆盖的两个方向"&gt;4.1 测试覆盖的两个方向&#10;&lt;/h3&gt;&lt;p&gt;当前场景同时覆盖：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;正向结果：写入成功、读取成功、修改成功、存在、删除成功；&lt;/li&gt;&#10;&lt;li&gt;反向结果：删除后读取、修改和判断存在均返回 &lt;code&gt;NO EXIST&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;“尽量覆盖项目所有结果，目标达到 90% 以上”可以理解为：不仅让每个函数运行一次，还要让函数中的主要结果分支都被执行到。对于协议层而言，要关注的结果包括：&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;OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NO EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;具体 value&#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="5-从功能测试扩展到压力测试"&gt;5. 从功能测试扩展到压力测试&#10;&lt;/h2&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;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;同一组操作重复很多次以后，结果是否仍然正确？大约能处理多少请求？&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;array_testcase_10w()&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="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="mi"&gt;10000&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;count&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="cm"&gt;/* 每轮执行 9 条请求 */&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;/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;10000 轮 × 9 条/轮 = 90000 条请求&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;计时使用 &lt;code&gt;gettimeofday()&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;tv_begin：循环开始时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tv_end：循环结束时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;time_used：两者相差的毫秒数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;宏 &lt;code&gt;TIME_SUB_MS&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="cp"&gt;#define TIME_SUB_MS(tv1, tv2) \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; ((tv1.tv_sec - tv2.tv_sec) * 1000 + \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; (tv1.tv_usec - tv2.tv_usec) / 1000)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;QPS 的基本计算关系是：&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;QPS = 请求总数 ÷ 耗时秒数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; = 请求总数 × 1000 ÷ 耗时毫秒数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的“请求总数”应按实际循环次数和每轮命令数计算。数组测试中是 &lt;code&gt;90000&lt;/code&gt;；红黑树测试同样是 &lt;code&gt;10000 × 9 = 90000&lt;/code&gt;。阅读输出时先统一这个口径，两个引擎的数据才可以放在一起比较。&lt;/p&gt;&#10;&lt;h3 id="51-三次粗测说明了什么"&gt;5.1 三次粗测说明了什么&#10;&lt;/h3&gt;&lt;p&gt;笔记记录了 9000 次请求附近的三次结果：&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 style="text-align: right"&gt;耗时&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;粗测 QPS&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;客户端输出每次 PASS，服务端也打印&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;477 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;18867&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;屏蔽客户端 PASS 输出&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;415 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;21686&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 style="text-align: right"&gt;285 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;31578&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&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;测试总耗时&#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; + 协议处理耗时&#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; + 客户端打印耗时&#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;code&gt;printf()&lt;/code&gt; 位于高频路径中，它也属于被计时的工作。逐步屏蔽打印后，QPS 上升，说明这次粗测测到的是整个端到端过程，而不只是 KV 引擎本身。&lt;/p&gt;&#10;&lt;h3 id="52-当前-qps-的准确含义"&gt;5.2 当前 QPS 的准确含义&#10;&lt;/h3&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;send 一条请求&#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 一条响应&#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;比较结果&#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;发送下一条请求&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;hr&gt;&#10;&lt;h2 id="6-为什么小次数正常高次数却失败"&gt;6. 为什么小次数正常，高次数却失败&#10;&lt;/h2&gt;&lt;p&gt;压力测试增加到 90000 次请求后，出现了：&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;FAILED-&amp;gt; SET-Dad,ERROR!=OK&#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;SET Dad Jasper&#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;DEL Dad&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一轮结束时，&lt;code&gt;Dad&lt;/code&gt; 已经删除，所以下一轮的 &lt;code&gt;SET Dad Jasper&lt;/code&gt; 应该仍然能够成功。实际数据量始终只在 0 和 1 之间变化：&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;SET 后：1 个元素&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DEL 后：0 个元素&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET 后：1 个元素&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DEL 后：0 个元素&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但是数组实例使用 &lt;code&gt;total&lt;/code&gt; 记录有效元素数量。如果插入成功执行 &lt;code&gt;total++&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;实际元素数量：0 → 1 → 0 → 1 → 0 ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;total： 0 → 1 → 1 → 2 → 2 ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;循环次数少时，&lt;code&gt;total&lt;/code&gt; 还没有触及 &lt;code&gt;KVS_ARRAY_SIZE&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;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;KVS_ARRAY_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;SET&lt;/code&gt; 返回错误。与此同时，查找范围由 &lt;code&gt;total&lt;/code&gt; 决定，&lt;code&gt;total&lt;/code&gt; 不断增大也会使遍历范围越来越长，所以压力测试还能观察到性能逐渐变化。&lt;/p&gt;&#10;&lt;h3 id="61-total-的核心约定"&gt;6.1 &lt;code&gt;total&lt;/code&gt; 的核心约定&#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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table[0] 到 table[total - 1]：有效元素&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table[total] 以及后面的位置：空闲空间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;total：当前有效元素数量&#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;插入成功 → total++&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;删除成功 → total--&#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;删除前：[Dad, Mom, Son] total = 3&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;删除 Mom 后出现空位：[Dad, 空, Son]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把最后一个元素移到空位：[Dad, Son, 空]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;最终：total = 2&#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="7-压力测试进一步暴露的几个语义问题"&gt;7. 压力测试进一步暴露的几个语义问题&#10;&lt;/h2&gt;&lt;p&gt;高次数运行不仅检查容量，还会不断经过删除、查询不存在和修改不存在等分支，因此能把返回值、内存和状态维护问题一起暴露出来。&lt;/p&gt;&#10;&lt;h3 id="71-删除-value-时的内存顺序"&gt;7.1 删除 value 时的内存顺序&#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="nf"&gt;kvs_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;inst-&amp;gt;table[i].value = NULL&#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;kvs_free(NULL)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原来保存在 &lt;code&gt;value&lt;/code&gt; 中的地址因此没有被交给 &lt;code&gt;kvs_free()&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;先把原地址交给 kvs_free&#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;再把保存该地址的指针设为 NULL&#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="nf"&gt;kvs_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&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;key&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;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&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="nf"&gt;kvs_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&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;value&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;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;table&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&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="72-循环下标与业务状态不是同一种含义"&gt;7.2 循环下标与业务状态不是同一种含义&#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="kt"&gt;int&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;&#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="kt"&gt;int&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;inst&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&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="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;for&lt;/code&gt; 中的 &lt;code&gt;i&lt;/code&gt; 属于内层作用域，会遮蔽外层的 &lt;code&gt;i&lt;/code&gt;。循环结束后，外层 &lt;code&gt;i&lt;/code&gt; 仍是初始值 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;更关键的是，即使只保留一个 &lt;code&gt;i&lt;/code&gt;，&lt;code&gt;return i&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;i：遍历到了哪个数组下标&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回值：本次 KV 操作是什么结果&#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;i = 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;total = 0&#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;return i 得到 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;0&lt;/code&gt; 只是下标初始值，并不能说明修改或删除成功。&lt;/p&gt;&#10;&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 style="text-align: right"&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 style="text-align: right"&gt;&lt;code&gt;&amp;lt; 0&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;参数或执行错误，生成 &lt;code&gt;ERROR&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 style="text-align: right"&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;操作成功，或 &lt;code&gt;EXIST&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 style="text-align: right"&gt;&lt;code&gt;&amp;gt; 0&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SET 时表示已存在；DEL/MOD/EXIST 时表示不存在&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="73-两种失败现象来自同一套返回值约定"&gt;7.3 两种失败现象来自同一套返回值约定&#10;&lt;/h3&gt;&lt;p&gt;删除成功后如果返回 &lt;code&gt;1&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;DEL 返回 1 → NO EXIST&#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;FAILED-&amp;gt; DEL-Dad,NO EXIST!=OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;删除以后再执行 &lt;code&gt;MOD&lt;/code&gt;，如果数组为空、循环未执行，最后又通过 &lt;code&gt;return i&lt;/code&gt; 返回初始值 &lt;code&gt;0&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;MOD 返回 0 → OK&#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;FAILED-&amp;gt; MOD-Dad,OK!=NO EXIST&#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;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;引擎内部状态&#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;固定返回值&#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;kvs_filter_protocol()&#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;客户端可见响应&#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;testcase() 与预期比较&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;测试失败信息最终帮助定位到的，不一定是 TCP 层，也可能是更深处的状态维护或返回值语义。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="8-从单一数组引擎扩展为多引擎"&gt;8. 从单一数组引擎扩展为多引擎&#10;&lt;/h2&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;SET / GET / DEL / MOD / EXIST&#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; global_array&#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="cp"&gt;#define ENABLE_ARRAY 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define ENABLE_RBTREE 1&#10;&lt;/span&gt;&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;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; kvs_protocol&#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; kvs_filter_protocol&#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; 根据命令选择对应的引擎&#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; SET/GET/DEL/MOD/EXIST RSET/RGET/RDEL/RMOD/REXIST&#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; global_array global_rbtree&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里没有改变底层网络。无论使用 Reactor、Proactor 还是 ntyco，网络层仍然只调用同一个 &lt;code&gt;kvs_protocol()&lt;/code&gt;。增加的是协议层后面的存储选择。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-为什么给红黑树命令增加-r-前缀"&gt;9. 为什么给红黑树命令增加 &lt;code&gt;R&lt;/code&gt; 前缀&#10;&lt;/h2&gt;&lt;p&gt;一条普通的 &lt;code&gt;SET Dad Jasper&lt;/code&gt; 只表达了“设置键值”，没有说明数据应该存进数组还是红黑树。&lt;/p&gt;&#10;&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;&#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;SET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RSET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;新增 key-value&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;GET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RGET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;查询 value&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;DEL&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RDEL&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;删除 key-value&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;MOD&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RMOD&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;修改 value&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;EXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;REXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;判断 key 是否存在&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&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="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&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="s"&gt;&amp;#34;SET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;DEL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;MOD&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;EXIST&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="s"&gt;&amp;#34;RSET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;RGET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;RDEL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;RMOD&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;REXIST&amp;#34;&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;kvs_filter_protocol()&lt;/code&gt; 找到命令编号后，通过 &lt;code&gt;switch&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;RSET&#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;KVS_CMD_RSET&#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;kvs_rbtree_set(&amp;amp;global_rbtree, key, value)&#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;按照返回值生成 OK / EXIST / ERROR&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，&lt;code&gt;R&lt;/code&gt; 前缀在当前阶段承担的是一个简单的路由信息：让协议层知道应该调用哪种引擎。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="10-每种数据结构都遵守5--2接口"&gt;10. 每种数据结构都遵守“5 + 2”接口&#10;&lt;/h2&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;create&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;destory&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;get&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;del&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mod&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exist&#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;2 个生命周期操作 + 5 个 KV 操作 = 5 + 2&#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="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_rbtree_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&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;void&lt;/span&gt; &lt;span class="nf"&gt;kvs_rbtree_destory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&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;kvs_rbtree_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&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;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;kvs_rbtree_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&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;kvs_rbtree_del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&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;kvs_rbtree_mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&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;kvs_rbtree_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_rbtree_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&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-text" data-lang="text"&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;调用哪个 KV 操作&#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;这正是本次“加入红黑树”最值得回顾的部分：不是从零推导红黑树算法，而是把已有数据结构包装成项目已经形成的 KV 引擎接口。&lt;/p&gt;&#10;&lt;p&gt;同样的思路也可以用于后续哈希引擎：哈希表内部怎样定位 key 属于数据结构实现；接入项目时，仍围绕生命周期和五种 KV 语义组织接口。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="11-红黑树在项目中的最小理解范围"&gt;11. 红黑树在项目中的最小理解范围&#10;&lt;/h2&gt;&lt;p&gt;为了理解本项目中的接入，只需要掌握下面几层关系。&lt;/p&gt;&#10;&lt;h3 id="111-树实例和节点"&gt;11.1 树实例和节点&#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;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;_rbtree&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;rbtree_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;root&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;rbtree_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;nil&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;rbtree&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;root&lt;/code&gt; 指向根节点；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;nil&lt;/code&gt; 是哨兵节点，用于表示空孩子和空树边界；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;nil&lt;/code&gt; 初始化为黑色；&lt;/li&gt;&#10;&lt;li&gt;创建完成时，&lt;code&gt;root&lt;/code&gt; 指向 &lt;code&gt;nil&lt;/code&gt;，表示树中还没有 KV 节点。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;节点中与 KV 项目直接相关的是：&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;key → 用于比较和查找&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;value → 与 key 对应的数据&#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="112-五种-kv-操作如何映射到底层树操作"&gt;11.2 五种 KV 操作如何映射到底层树操作&#10;&lt;/h3&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;KV 接口&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;kvs_rbtree_set()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;为 key/value 分配空间，构造节点并插入树&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;kvs_rbtree_get()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;按 key 搜索节点，返回节点的 value&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;kvs_rbtree_del()&lt;/code&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;&lt;code&gt;kvs_rbtree_mod()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;搜索节点，替换已有 value&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;kvs_rbtree_exist()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;搜索节点，通过是否到达 &lt;code&gt;nil&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;这些包装函数把“树节点操作”转换成与数组引擎一致的“KV 操作”。&lt;/p&gt;&#10;&lt;h3 id="113-生命周期怎样进入程序主流程"&gt;11.3 生命周期怎样进入程序主流程&#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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;init_kvengine()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ kvs_array_create(&amp;amp;global_array)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ kvs_rbtree_create(&amp;amp;global_rbtree)&#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;dest_kvengine()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ kvs_array_destory(&amp;amp;global_array)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ kvs_rbtree_destory(&amp;amp;global_rbtree)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;红黑树销毁时需要逐步删除树中的节点，最后处理树实例的哨兵节点。具体采用最小节点、最大节点或其他顺序，是树内部的销毁策略；对项目主线而言，重要的是 &lt;code&gt;destory&lt;/code&gt; 与 &lt;code&gt;create&lt;/code&gt; 共同构成完整生命周期。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="12-用同一套语义验证红黑树引擎"&gt;12. 用同一套语义验证红黑树引擎&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rbtree_testcase()&lt;/code&gt; 与 &lt;code&gt;array_testcase()&lt;/code&gt; 的业务顺序保持一致，只是命令增加了 &lt;code&gt;R&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;数组： SET → GET → MOD → GET → EXIST → DEL → GET → MOD → EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;红黑树：RSET → RGET → RMOD → RGET → REXIST → RDEL → RGET → RMOD → REXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这使测试具有两个层次：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;单独验证每个引擎是否满足五种 KV 操作；&lt;/li&gt;&#10;&lt;li&gt;验证两种引擎在相同业务场景下是否表现出相同语义。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&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;&#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;不存在时 SET&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&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;SET 后 GET&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;对应 value&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;对应 value&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;已存在时 MOD&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&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;已存在时 EXIST&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST&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;已存在时 DEL&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OK&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;DEL 后 GET/MOD/EXIST&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;NO EXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;NO EXIST&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;只要对外行为一致，客户端就不需要了解底层究竟使用数组还是红黑树。当前协议用命令前缀显式选择引擎，但测试验证的是二者共同的 KV 语义。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="13-数组与红黑树在当前项目中的角色"&gt;13. 数组与红黑树在当前项目中的角色&#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;&#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;&#9;&#9;&lt;td&gt;五种 &lt;code&gt;R&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;global_array&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;global_rbtree&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;create/destory&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;create/destory&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;KV 接口&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;set/get/del/mod/exist&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;set/get/del/mod/exist&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;/td&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;查找方式&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;在有效区间中遍历&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;按 key 在树中搜索&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&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;协议命令&#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;统一的 KV 语义&#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;不同引擎各自实现&#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;需求是保存、查询、删除、修改和判断 key；&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;hr&gt;&#10;&lt;h2 id="14-关于多进程多线程压力测试的课后思考"&gt;14. 关于多进程、多线程压力测试的课后思考&#10;&lt;/h2&gt;&lt;p&gt;当前单连接测试中，每条请求都要等待上一条响应，无法同时给服务端制造多个在途请求。要观察并发场景，可以让多个执行单元各自建立连接并运行同一组测试。&lt;/p&gt;&#10;&lt;h3 id="141-多进程思路"&gt;14.1 多进程思路&#10;&lt;/h3&gt;&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;父进程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ fork → 子进程 1 → 建立连接 → 重复测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ fork → 子进程 2 → 建立连接 → 重复测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ fork → 子进程 N → 建立连接 → 重复测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个子进程拥有自己的客户端 socket 和测试循环。父进程等待所有子进程结束，再汇总：&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;总请求数 = 进程数 × 每个进程的请求数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;整体 QPS = 总请求数 × 1000 ÷ 整体耗时毫秒数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="142-多线程思路"&gt;14.2 多线程思路&#10;&lt;/h3&gt;&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;一个客户端进程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ 线程 1 → 独立连接 → 重复测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ 线程 2 → 独立连接 → 重复测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ 线程 N → 独立连接 → 重复测试&#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="143-并发数增加后观察什么"&gt;14.3 并发数增加后观察什么&#10;&lt;/h3&gt;&lt;p&gt;并发测试不只是期待 QPS 一定上升，还要同时观察：&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;p&gt;因此，多进程或多线程的意义是改变负载模型：从“一个客户端串行发请求”变成“多个客户端同时发请求”。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="15-这一阶段形成的完整项目画面"&gt;15. 这一阶段形成的完整项目画面&#10;&lt;/h2&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;测试客户端&#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; └─ 压力用例：重复场景、计时、计算 QPS&#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; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TCP 网络&#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; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;网络层：Reactor / Proactor / ntyco&#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; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kvs_protocol()：切分 token&#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; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kvs_filter_protocol()：识别命令并选择引擎&#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; ▼ ▼&#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; 5 + 2 接口 5 + 2 接口&#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; └────┬────┘&#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; 统一返回值与文本响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Makefile 则从构建角度把这些模块放到一起：&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;kvstore.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;reactor.c / proactor.c / ntyco.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kvs_array.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kvs_rbtree.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NtyCo 与 liburing&#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; kvstore&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="16-最后的复习提纲"&gt;16. 最后的复习提纲&#10;&lt;/h2&gt;&lt;h3 id="161-测试主线"&gt;16.1 测试主线&#10;&lt;/h3&gt;&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;connect&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → send command&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → recv response&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → compare pattern&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → PASS / FAILED&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="162-压力测试主线"&gt;16.2 压力测试主线&#10;&lt;/h3&gt;&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;固定业务场景&#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; → 统计实际请求数&#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; → 计算 QPS&#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;h3 id="163-数组问题定位主线"&gt;16.3 数组问题定位主线&#10;&lt;/h3&gt;&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;少量测试正常、增加次数后 SET 返回 ERROR&#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; → 检查 total 的增减&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 发现实际元素已删除但 total 仍增长&#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;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;释放内存：先 free 原地址，再把指针置 NULL&#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;函数返回值：表示业务结果&#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;h3 id="164-多引擎扩展主线"&gt;16.4 多引擎扩展主线&#10;&lt;/h3&gt;&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;保留数组成型代码&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 用 ENABLE_ARRAY / ENABLE_RBTREE 控制编译&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 为红黑树提供同样的 5 + 2 接口&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 增加 RSET/RGET/RDEL/RMOD/REXIST&#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; → 用同一套业务场景验证两种引擎&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="165-本阶段最重要的认识"&gt;16.5 本阶段最重要的认识&#10;&lt;/h3&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;测试用例不是项目完成后的附属代码，而是项目继续迭代时用来确认行为的固定标准。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;小次数测试验证基本功能，高次数测试还会验证状态能否长期保持一致。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;code&gt;total&lt;/code&gt;、节点数量和指针生命周期都属于数据结构必须维护的内部状态。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;数组、红黑树以及后续哈希表可以采用不同内部实现，但都可以通过相同的 KV 语义接入协议层。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;本阶段加入红黑树的重点，是把一种已有数据结构组织成项目需要的存储引擎，而不是从零实现红黑树算法。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>io_uring（二）：QPS 实测，真的比 epoll 强吗？</title><link>https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/</link><pubDate>Thu, 20 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/cover.svg" alt="Featured image of post io_uring（二）：QPS 实测，真的比 epoll 强吗？" /&gt;&lt;h2 id="一学习目标"&gt;一、学习目标&#10;&lt;/h2&gt;&lt;p&gt;这部分学习主要围绕两个目标展开：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;编写一个 TCP 客户端测试工具，对比 epoll 和 io_uring 服务端的性能。&lt;/li&gt;&#10;&lt;li&gt;整理网络相关面试题，重点理解 TCP、UDP、建链、断链、并发及分包粘包问题。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二qps-测试工具需求"&gt;二、QPS 测试工具需求&#10;&lt;/h2&gt;&lt;p&gt;客户端需要支持以下参数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-n request_num 请求总数&#10;-t thread_num 线程数量&#10;-c connection_num 连接数量&#10;-s server_ip 服务端 IP&#10;-p server_port 服务端端口&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用示例：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;./test_qps_tcpclient \&#10; -s 172.16.145.129 \&#10; -p 2048 \&#10; -t 50 \&#10; -c 100 \&#10; -n 10000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试采用 Echo 模型：&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;客户端比较发送和接收的数据。&lt;/li&gt;&#10;&lt;li&gt;数据一致则请求成功，否则失败。&lt;/li&gt;&#10;&lt;li&gt;最后统计总耗时和 QPS。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;QPS 的计算方式为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;QPS = 完成的请求数 ÷ 耗时（秒）&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代码中使用毫秒计时，因此写成：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;qps = request_num * 1000 / time_used_ms;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h2 id="三测试数据的构造"&gt;三、测试数据的构造&#10;&lt;/h2&gt;&lt;p&gt;测试字符串为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#define TEST_MESSAGE \&#10;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz\r\n&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这段字符串的长度正好是 64 字节：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;26 个大写字母&#10;+ 10 个数字&#10;+ 26 个小写字母&#10;+ \r\n 两个字符&#10;= 64 字节&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通过重复复制，可以构造不同大小的请求：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; 2; i++) {&#10; strcpy(&#10; wbuffer + i * strlen(TEST_MESSAGE),&#10; TEST_MESSAGE&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&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;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;64 字节&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;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 字节&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;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 字节&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;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 字节&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;16&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;测试大包时，需要关闭服务端和客户端的数据打印，否则终端输出本身会严重影响测试结果。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四64-bytes-测试"&gt;四、64 bytes 测试&#10;&lt;/h2&gt;&lt;p&gt;测试 100 万次、每次发送 64 bytes 时，结果如下：&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;&#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;&#9;&#9;&lt;th&gt;QPS&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;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;epoll&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5132 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194855&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;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;io_uring&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4400 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227272&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;在 64 bytes 测试中：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;epoll QPS：194855&#10;io_uring QPS：227272&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;io_uring 的 QPS 比 epoll 高约 16.6%。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五不同数据长度下的测试"&gt;五、不同数据长度下的测试&#10;&lt;/h2&gt;&lt;h3 id="1-io_uring-服务端"&gt;1. io_uring 服务端&#10;&lt;/h3&gt;&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;&#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;QPS&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;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4337 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;230574&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;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4268 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;234301&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;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4307 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;232180&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;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4390 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227790&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;从结果来看，在 128～1024 bytes 范围内，io_uring 的 QPS 比较稳定，基本维持在 22 万～23 万。&lt;/p&gt;&#10;&lt;p&gt;1024 bytes 测试时，笔记中怀疑可能出现“粘包”。&lt;/p&gt;&#10;&lt;p&gt;更准确地说，TCP 本身是字节流，没有一条 &lt;code&gt;send()&lt;/code&gt; 必须对应一条 &lt;code&gt;recv()&lt;/code&gt; 的保证。一次发送的 1024 bytes，可能需要多次 &lt;code&gt;recv()&lt;/code&gt; 才能完整读取，因此客户端必须实现循环接收。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="2-epoll-服务端阻塞-io"&gt;2. epoll 服务端——阻塞 I/O&#10;&lt;/h3&gt;&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;&#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;QPS&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;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;74935 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;13344&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;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;147749 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6768&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;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;296207 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3376&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;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;721163 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1386&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;这组结果明显异常：数据长度每增加一倍，耗时几乎也增加一倍，QPS 则接近减半。&lt;/p&gt;&#10;&lt;p&gt;问题在于 epoll 服务端使用了阻塞 socket。&lt;/p&gt;&#10;&lt;p&gt;epoll 的作用是通知程序某个 fd 当前可读或者可写。但即使 fd 已经可读，也不代表接下来所有的 &lt;code&gt;recv()&lt;/code&gt; 都不会阻塞。&lt;/p&gt;&#10;&lt;p&gt;尤其是在循环读取时，如果已经把当前到达的数据读完，又调用了一次阻塞式 &lt;code&gt;recv()&lt;/code&gt;，整个事件循环就可能停在某一个连接上，导致其他连接无法及时处理。&lt;/p&gt;&#10;&lt;p&gt;所以 epoll 一般需要与非阻塞 socket 配合：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;epoll 通知 fd 可读&#10; ↓&#10;循环调用 recv&#10; ↓&#10;读取当前已经到达的数据&#10; ↓&#10;recv 返回 EAGAIN&#10; ↓&#10;处理下一个就绪事件&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h3 id="3-epoll-服务端非阻塞-io"&gt;3. epoll 服务端——非阻塞 I/O&#10;&lt;/h3&gt;&lt;p&gt;将 socket 设置成非阻塞后重新测试：&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;&#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;QPS&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;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5198 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;192381&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;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5144 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194401&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;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5141 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194514&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;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5216 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;191717&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;修改后，epoll 的 QPS 恢复到约 19 万，并且不再随数据长度增加而大幅下降。&lt;/p&gt;&#10;&lt;p&gt;这说明前一组数据反映的主要不是 epoll 本身的性能，而是“epoll 配合阻塞 I/O”造成的异常。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六epoll-与-io_uring-的完整对比"&gt;六、epoll 与 io_uring 的完整对比&#10;&lt;/h2&gt;&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;io_uring QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;epoll QPS&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;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227272&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194855&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;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;230574&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;192381&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;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;234301&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194401&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;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;232180&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194514&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;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227790&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;191717&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;从测试结果来看：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;64 bytes 时，io_uring 比 epoll 高约 16.6%；&lt;/li&gt;&#10;&lt;li&gt;128～1024 bytes 时，io_uring 大约比 epoll 高 19%～21%；&lt;/li&gt;&#10;&lt;li&gt;所有测试包大小下，io_uring 的 QPS 都高于 epoll；&lt;/li&gt;&#10;&lt;li&gt;两种模型在改正 epoll 的阻塞 I/O 问题后，QPS 都没有随着数据大小增加而发生明显下降。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;二者的处理思路不同。&lt;/p&gt;&#10;&lt;h3 id="epoll"&gt;epoll&#10;&lt;/h3&gt;&lt;p&gt;epoll 是就绪通知模型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;先等待 fd 可读或可写&#10;→ 得到就绪事件&#10;→ 应用程序再调用 recv/send&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;也就是说，epoll 负责告诉应用程序“现在可以尝试进行 I/O”，真正的数据读写仍然由应用程序发起。&lt;/p&gt;&#10;&lt;h3 id="io_uring"&gt;io_uring&#10;&lt;/h3&gt;&lt;p&gt;io_uring 更接近完成通知模型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;应用程序先提交 recv/send 请求&#10;→ 内核执行 I/O&#10;→ I/O 完成后通知应用程序&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;应用程序可以提前向内核提交 I/O 请求，然后通过完成队列取得结果。&lt;/p&gt;&#10;&lt;p&gt;根据本次测试，可以得到以下结论：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;在当前服务端实现、当前客户端模型和当前测试环境下，io_uring 的 QPS 全面高于非阻塞 epoll，整体高约 17%～21%。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;这个结论应当限定在本次测试条件内，不能仅凭这一轮测试认为所有业务中 io_uring 都一定优于 epoll。&lt;/p&gt;&#10;&lt;hr&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;客户端和服务端之间通常会存在心跳包。&lt;/p&gt;&#10;&lt;p&gt;心跳包只用于检测连接是否仍然存活，因此数据量通常很小，可能只有几个字节。&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;I/O 事件通知和调度开销；&lt;/li&gt;&#10;&lt;li&gt;服务端是否能够及时发现断开的连接。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="2-视频和图片传输"&gt;2. 视频和图片传输&#10;&lt;/h3&gt;&lt;p&gt;视频或者较大的图片需要被拆分成多个数据块。&lt;/p&gt;&#10;&lt;p&gt;应用层可以将每一个业务包组织成约 1 KB，再通过协议头标识数据长度、类型和序号。&lt;/p&gt;&#10;&lt;p&gt;这类场景除了 QPS，还需要关注：&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;p&gt;因此，也可以结合不同数据大小下的测试结果，理解 epoll 和 io_uring 的差别。&lt;/p&gt;&#10;&lt;h3 id="3-百万并发测试"&gt;3. 百万并发测试&#10;&lt;/h3&gt;&lt;p&gt;后续还可以使用 io_uring 和 epoll 测试百万并发，包括：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;建立相同数量连接所需要的时间；&lt;/li&gt;&#10;&lt;li&gt;相同并发连接数下的 QPS；&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;/ol&gt;&#10;&lt;p&gt;断开连接涉及 TCP 协议栈中的状态转换，但应用层仍然需要正确执行 &lt;code&gt;close()&lt;/code&gt;、移除连接状态，并释放自己管理的连接资源。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八qps-客户端代码详解"&gt;八、QPS 客户端代码详解&#10;&lt;/h2&gt;&lt;h3 id="1-测试上下文"&gt;1. 测试上下文&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;struct test_context_s {&#10; char serverip[16];&#10; int port;&#10; int threadnum;&#10; int connection;&#10; int requestion;&#10;&#10;#if 1&#10; int failed;&#10;#endif&#10;};&#10;&#10;typedef struct test_context_s test_context_t;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个结构保存测试参数：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;serverip&lt;/code&gt;：服务端 IPv4 地址；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;port&lt;/code&gt;：服务端端口；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;threadnum&lt;/code&gt;：测试线程数；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;connection&lt;/code&gt;：期望建立的连接数量；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;requestion&lt;/code&gt;：请求总数；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;：失败请求数。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;code&gt;requestion&lt;/code&gt; 想表达的应该是请求数量，也就是 &lt;code&gt;request_count&lt;/code&gt; 或者 &lt;code&gt;requests&lt;/code&gt;，只是命名不太准确，不影响程序运行。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;serverip&lt;/code&gt; 的长度是 16，刚好可以保存最长的 IPv4 字符串和字符串结尾的 &lt;code&gt;\0&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;255.255.255.255\0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;不过，代码使用的是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;strcpy(ctx.serverip, optarg);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果传入的参数超过数组长度，就会发生越界写入。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="2-建立-tcp-连接"&gt;2. 建立 TCP 连接&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int connect_tcpserver(&#10; const char *ip,&#10; unsigned short port&#10;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;函数首先创建 TCP socket：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int connfd = 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;connfd&lt;/code&gt; 是连接对应的文件描述符。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;然后构造服务端地址：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct sockaddr_in tcpserver_addr;&#10;&#10;memset(&#10; &amp;amp;tcpserver_addr,&#10; 0,&#10; sizeof(struct sockaddr_in)&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;清零后设置地址类型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tcpserver_addr.sin_family = AF_INET;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;设置服务端 IP：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tcpserver_addr.sin_addr.s_addr = inet_addr(ip);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;inet_addr()&lt;/code&gt; 将类似下面的 IPv4 字符串：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;172.16.145.129&#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;tcpserver_addr.sin_port = htons(port);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;htons()&lt;/code&gt; 将端口从主机字节序转换成网络字节序。&lt;/p&gt;&#10;&lt;p&gt;最后调用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int ret = connect(&#10; connfd,&#10; (struct sockaddr *)&amp;amp;tcpserver_addr,&#10; sizeof(struct sockaddr_in)&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果连接成功，返回 &lt;code&gt;connfd&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;return connfd;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果连接失败，返回 &lt;code&gt;-1&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (ret) {&#10; perror(&amp;#34;connect\n&amp;#34;);&#10; return -1;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当前代码没有检查 &lt;code&gt;socket()&lt;/code&gt; 是否创建失败。&lt;/p&gt;&#10;&lt;p&gt;另外，如果 &lt;code&gt;connect()&lt;/code&gt; 失败，已经创建的 &lt;code&gt;connfd&lt;/code&gt; 没有关闭，会造成文件描述符泄漏。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="3-时间计算"&gt;3. 时间计算&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#define TIME_SUB_MS(tv1, tv2) \&#10; ((tv1.tv_sec - tv2.tv_sec) * 1000 + \&#10; (tv1.tv_usec - tv2.tv_usec) / 1000)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个宏用来计算两个 &lt;code&gt;timeval&lt;/code&gt; 之间相差的毫秒数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;秒差 × 1000 + 微秒差 ÷ 1000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;主函数在线程创建前记录开始时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_begin;&#10;gettimeofday(&amp;amp;tv_begin, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程结束后记录结束时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_end;&#10;gettimeofday(&amp;amp;tv_end, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后计算：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int time_used = TIME_SUB_MS(tv_end, tv_begin);&#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;建立 TCP 连接的时间；&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;所以当前 QPS 是一轮完整压测的整体 QPS，并不是纯粹的数据收发 QPS。&lt;/p&gt;&#10;&lt;p&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;#define TEST_MESSAGE \&#10;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz\r\n&amp;#34;&#10;&#10;#define RBUFFER_LENGTH 2048&#10;#define WBUFFER_LENGTH 2048&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;TEST_MESSAGE&lt;/code&gt; 是 64 bytes。&lt;/p&gt;&#10;&lt;p&gt;读缓冲区和写缓冲区都设置为 2048 bytes。&lt;/p&gt;&#10;&lt;p&gt;在当前代码中，&lt;code&gt;WBUFFER_LENGTH&lt;/code&gt; 宏并没有被使用，因为写缓冲区直接写成了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;char wbuffer[2048] = {0};&#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 send_recv_tcppkt(int fd)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个函数完成一次请求和响应。&lt;/p&gt;&#10;&lt;h4 id="构造发送数据"&gt;构造发送数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;char wbuffer[2048] = {0};&#10;int i = 0;&#10;&#10;for (i = 0; i &amp;lt; 2; i++) {&#10; strcpy(&#10; wbuffer + i * strlen(TEST_MESSAGE),&#10; TEST_MESSAGE&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于 &lt;code&gt;TEST_MESSAGE&lt;/code&gt; 为 64 bytes，循环两次后，&lt;code&gt;wbuffer&lt;/code&gt; 中保存了 128 bytes 数据。&lt;/p&gt;&#10;&lt;p&gt;第一次复制的位置是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;wbuffer + 0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第二次复制的位置是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;wbuffer + 64&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，两段 64 bytes 的数据首尾相连，组成 128 bytes 的请求。&lt;/p&gt;&#10;&lt;h4 id="发送数据"&gt;发送数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int res = send(&#10; fd,&#10; wbuffer,&#10; strlen(wbuffer),&#10; 0&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果发送失败：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (res &amp;lt; 0) {&#10; exit(1);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里的主要问题是，一次 &lt;code&gt;send()&lt;/code&gt; 不保证发送完全部数据。&lt;/p&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;send(fd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;返回值可能是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;1024：全部发送完成&#10;512：只发送了 512 bytes&#10;-1：发送失败&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以不能只判断 &lt;code&gt;res &amp;lt; 0&lt;/code&gt;，还需要判断返回值是否等于预期发送长度。&lt;/p&gt;&#10;&lt;p&gt;正确的逻辑应当是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;记录总共需要发送的长度&#10; ↓&#10;循环调用 send&#10; ↓&#10;每次根据返回值移动缓冲区指针&#10; ↓&#10;直到所有字节发送完成&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="接收数据"&gt;接收数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;char rbuffer[RBUFFER_LENGTH] = {0};&#10;&#10;res = recv(&#10; fd,&#10; rbuffer,&#10; RBUFFER_LENGTH,&#10; 0&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果返回值小于或者等于 0：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (res &amp;lt;= 0) {&#10; exit(1);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;recv()&lt;/code&gt; 的返回值含义是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;大于 0：本次实际读取的字节数&#10;等于 0：对端正常关闭连接&#10;小于 0：读取失败&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和 &lt;code&gt;send()&lt;/code&gt; 一样，一次 &lt;code&gt;recv()&lt;/code&gt; 也不保证读取到完整的响应。&lt;/p&gt;&#10;&lt;p&gt;服务端即使完整返回了 1024 bytes，客户端也可能出现：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;第一次 recv：读取 300 bytes&#10;第二次 recv：读取 500 bytes&#10;第三次 recv：读取 224 bytes&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，需要循环接收，直到读取到预期长度。&lt;/p&gt;&#10;&lt;p&gt;这正是笔记中“很有必要重新封装一个 &lt;code&gt;recv&lt;/code&gt; 和 &lt;code&gt;send&lt;/code&gt;”的原因。&lt;/p&gt;&#10;&lt;h4 id="比较数据"&gt;比较数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;if (strcmp(rbuffer, wbuffer) != 0) {&#10; printf(&#10; &amp;#34;failed: &amp;#39;%s&amp;#39;!=&amp;#39;%s&amp;#39;\n&amp;#34;,&#10; rbuffer,&#10; wbuffer&#10; );&#10; return -1;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里通过 &lt;code&gt;strcmp()&lt;/code&gt; 比较接收数据和发送数据。&lt;/p&gt;&#10;&lt;p&gt;但网络数据本质上是一段带长度的二进制数据，使用 &lt;code&gt;strcmp()&lt;/code&gt; 的前提是两个缓冲区都以 &lt;code&gt;\0&lt;/code&gt; 结尾。&lt;/p&gt;&#10;&lt;p&gt;更稳定的比较方式是根据实际数据长度使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;memcmp(rbuffer, wbuffer, expected_length);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果一次 &lt;code&gt;recv()&lt;/code&gt; 只读到部分数据，那么当前 &lt;code&gt;strcmp()&lt;/code&gt; 会直接认为请求失败，即使剩余数据稍后还能继续收到。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="6-工作线程"&gt;6. 工作线程&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;static void *test_qps_entry(void *arg)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;线程首先取得测试上下文：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;test_context_t *pctx =&#10; (test_context_t *)arg;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;原始笔记中出现了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;(test\_context\_t *)arg&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果实际代码中确实包含反斜杠，需要将反斜杠删除。它也可能只是复制代码时产生的 Markdown 转义。&lt;/p&gt;&#10;&lt;h4 id="计算每个线程的连接数"&gt;计算每个线程的连接数&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int conn_num =&#10; pctx-&amp;gt;connection / pctx-&amp;gt;threadnum;&#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;连接数：100&#10;线程数：50&#10;每个线程应当管理：2 个连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是，&lt;code&gt;conn_num&lt;/code&gt; 后面没有被使用。&lt;/p&gt;&#10;&lt;p&gt;实际代码只建立了一次连接：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int connfd = connect_tcpserver(&#10; pctx-&amp;gt;serverip,&#10; pctx-&amp;gt;port&#10;);&#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;-t 50 -c 100&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实际建立的不是 100 个连接，而是 50 个连接。&lt;/p&gt;&#10;&lt;p&gt;当前代码中的 &lt;code&gt;-c&lt;/code&gt; 参数实际上没有生效。&lt;/p&gt;&#10;&lt;h4 id="计算每个线程的请求数"&gt;计算每个线程的请求数&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int count =&#10; pctx-&amp;gt;requestion / pctx-&amp;gt;threadnum;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;请求总数：10000&#10;线程数：50&#10;每个线程：200 次请求&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每个线程执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;while (i++ &amp;lt; count) {&#10; res = send_recv_tcppkt(connfd);&#10;&#10; if (res != 0) {&#10; printf(&amp;#34;send_recv_tcppkt failed\n&amp;#34;);&#10; pctx-&amp;gt;failed++;&#10; continue;&#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;阻塞等待响应&#10; ↓&#10;收到完整响应&#10; ↓&#10;再发送下一次请求&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以每个连接同一时间最多只有一个未完成请求。&lt;/p&gt;&#10;&lt;p&gt;整个客户端能够同时处理的请求数量，实际上接近线程数量，而不是 &lt;code&gt;-c&lt;/code&gt; 指定的连接数量。&lt;/p&gt;&#10;&lt;h4 id="请求数量不能整除线程数"&gt;请求数量不能整除线程数&#10;&lt;/h4&gt;&lt;p&gt;如果：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-n 10003&#10;-t 50&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;则：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;count = 10003 / 50;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整数除法结果为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;200&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实际发送请求数为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;200 × 50 = 10000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;剩余 3 次请求不会发送。&lt;/p&gt;&#10;&lt;p&gt;但是，程序最后仍然按照 10003 计算成功数量和 QPS，所以统计结果会不准确。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="7-失败请求统计"&gt;7. 失败请求统计&#10;&lt;/h3&gt;&lt;p&gt;失败时执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pctx-&amp;gt;failed++;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程共享同一个 &lt;code&gt;failed&lt;/code&gt;，但这里没有加锁，也没有使用原子变量。&lt;/p&gt;&#10;&lt;p&gt;多个线程可能同时执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pctx-&amp;gt;failed++;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如，两个线程同时读到：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;failed = 10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;两个线程都计算得到 11，再分别写回，最终结果可能仍然是 11，而不是正确的 12。&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;/ul&gt;&#10;&lt;p&gt;笔记中提到“数据没有那么重要，只需要知道有失败的可能性”，这种处理可以用于临时测试，但最终版本如果需要准确统计失败数，仍然需要使用锁或者原子操作。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="8-命令行参数解析"&gt;8. 命令行参数解析&#10;&lt;/h3&gt;&lt;p&gt;主函数通过 &lt;code&gt;getopt()&lt;/code&gt; 解析参数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;while (&#10; (opt = getopt(&#10; argc,&#10; argv,&#10; &amp;#34;s:p:t:c:n:?&amp;#34;&#10; )) != -1&#10;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;参数含义如下：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-s：服务端 IP&#10;-p：服务端端口&#10;-t：线程数量&#10;-c：连接数量&#10;-n：请求数量&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;s&amp;#39;:&#10; strcpy(ctx.serverip, optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取 IP。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;p&amp;#39;:&#10; ctx.port = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取端口。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;t&amp;#39;:&#10; ctx.threadnum = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取线程数量。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;c&amp;#39;:&#10; ctx.connection = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取连接数量。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;n&amp;#39;:&#10; ctx.requestion = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取请求总数。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;getopt()&lt;/code&gt; 使用了一些全局状态，因此笔记中记录它不是线程安全函数。不过当前代码只在主线程、创建工作线程之前调用，所以不会产生并发调用问题。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="9-创建工作线程"&gt;9. 创建工作线程&#10;&lt;/h3&gt;&lt;p&gt;程序首先根据线程数量申请线程 ID 数组：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pthread_t *ptid =&#10; malloc(&#10; ctx.threadnum * sizeof(pthread_t)&#10; );&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后记录开始时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_begin;&#10;gettimeofday(&amp;amp;tv_begin, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;创建工作线程：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; ctx.threadnum; i++) {&#10; pthread_create(&#10; &amp;amp;ptid[i],&#10; NULL,&#10; test_qps_entry,&#10; &amp;amp;ctx&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程都接收同一个 &lt;code&gt;ctx&lt;/code&gt; 地址：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;amp;ctx&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;IP、端口、线程数、连接数和请求数只读；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed&lt;/code&gt; 会被所有线程共同修改。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;创建完成后，主线程等待所有工作线程退出：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; ctx.threadnum; i++) {&#10; pthread_join(ptid[i], NULL);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;只有全部线程执行完毕，主线程才会继续计算总耗时。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="10-统计-qps"&gt;10. 统计 QPS&#10;&lt;/h3&gt;&lt;p&gt;所有线程结束后，记录结束时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_end;&#10;gettimeofday(&amp;amp;tv_end, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;计算总耗时：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int time_used =&#10; TIME_SUB_MS(tv_end, tv_begin);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后输出：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;printf(&#10; &amp;#34;success: %d,failed:%d,&amp;#34;&#10; &amp;#34;time_used:%d,qps:%d\n&amp;#34;,&#10; ctx.requestion - ctx.failed,&#10; ctx.failed,&#10; time_used,&#10; ctx.requestion * 1000 / time_used&#10;);&#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;li&gt;QPS。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当前 QPS 使用的是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ctx.requestion * 1000 / time_used&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里使用了配置的请求数，而不是实际完成的请求数。&lt;/p&gt;&#10;&lt;p&gt;如果请求数不能整除线程数，或者中间出现失败，QPS 就不能准确表示真正成功完成的请求数量。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="11-资源释放"&gt;11. 资源释放&#10;&lt;/h3&gt;&lt;p&gt;主函数结束前释放线程数组：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;free(ptid);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但工作线程建立的 TCP 连接没有关闭：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;close(connfd);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在线程退出前，应当关闭自己创建的连接。&lt;/p&gt;&#10;&lt;p&gt;虽然进程退出时，操作系统最终会回收文件描述符，但如果后续让一个进程连续执行多轮测试，不关闭连接就会逐渐产生资源泄漏。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九当前客户端的实际测试模型"&gt;九、当前客户端的实际测试模型&#10;&lt;/h2&gt;&lt;p&gt;虽然命令行参数中包含 &lt;code&gt;-c&lt;/code&gt;，但当前代码的实际模型是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;主线程&#10; ├─ 工作线程 1&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; ├─ 工作线程 2&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; ├─ 工作线程 3&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; └─ 其他工作线程&#10; └─ 每个线程一个 TCP 连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;实际连接数 = 线程数&#10;每个连接同时只有一个请求&#10;-c 参数没有生效&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当前工具比较的是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;多线程、每个线程使用一个阻塞 TCP 连接、请求与响应严格交替时，epoll 和 io_uring 服务端的处理能力。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;它还不能测试固定线程数管理大量连接的能力，也不能直接用于百万并发测试。&lt;/p&gt;&#10;&lt;p&gt;如果需要让 &lt;code&gt;-c&lt;/code&gt; 生效，每个线程就要创建：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;connection / threadnum&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;个连接，并在这些连接之间分配请求。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十当前代码需要优先处理的问题"&gt;十、当前代码需要优先处理的问题&#10;&lt;/h2&gt;&lt;p&gt;按照对测试准确性的影响排序：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;-c&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;recv()&lt;/code&gt; 没有循环处理短读。&lt;/li&gt;&#10;&lt;li&gt;请求数不能整除线程数时，实际请求数量少于 &lt;code&gt;-n&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed++&lt;/code&gt; 存在多线程数据竞争。&lt;/li&gt;&#10;&lt;li&gt;工作线程结束后没有执行 &lt;code&gt;close(connfd)&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;QPS 使用配置的请求数计算，而不是实际完成数。&lt;/li&gt;&#10;&lt;li&gt;没有检查 &lt;code&gt;socket()&lt;/code&gt;、&lt;code&gt;malloc()&lt;/code&gt; 和 &lt;code&gt;pthread_create()&lt;/code&gt; 的返回值。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;connect()&lt;/code&gt; 失败后没有关闭已经创建的 socket。&lt;/li&gt;&#10;&lt;li&gt;工作线程中的 &lt;code&gt;exit(1)&lt;/code&gt; 会让一个线程的网络错误终止整个压测进程。&lt;/li&gt;&#10;&lt;li&gt;如果源代码确实包含 &lt;code&gt;test\_context\_t&lt;/code&gt;，需要删除反斜杠。&lt;/li&gt;&#10;&lt;li&gt;如果源代码第一行确实是 &lt;code&gt;include&amp;lt;stdio.h&amp;gt;&lt;/code&gt;，需要补上 &lt;code&gt;#&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十一网络面试问题整理"&gt;十一、网络面试问题整理&#10;&lt;/h2&gt;&lt;p&gt;原始笔记目前记录了四个核心问题，没有足够内容整理成十个，因此这里只整理已有内容，不额外补题。&lt;/p&gt;&#10;&lt;h3 id="1-tcp-三次握手与四次挥手"&gt;1. TCP 三次握手与四次挥手&#10;&lt;/h3&gt;&lt;h4 id="三次握手"&gt;三次握手&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端 服务端&#10;&#10;SYN&#10; ------------------------&amp;gt;&#10;&#10; SYN + ACK&#10; &amp;lt;------------------------&#10;&#10;ACK&#10; ------------------------&amp;gt;&#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;li&gt;客户端能够接收；&lt;/li&gt;&#10;&lt;li&gt;双方同步初始序列号。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;之所以不能只使用两次握手，是因为服务端发送 &lt;code&gt;SYN + ACK&lt;/code&gt; 后，还需要知道客户端是否真正收到了自己的响应。&lt;/p&gt;&#10;&lt;p&gt;第三次 ACK 让服务端确认：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端已经收到服务端的 SYN&#10;客户端也能够正常接收数据&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&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;确认客户端的 SYN&#10;+&#10;发送自己的 SYN&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，服务端可以通过一个 &lt;code&gt;SYN + ACK&lt;/code&gt; 同时完成这两个操作，最终形成三次握手。&lt;/p&gt;&#10;&lt;p&gt;断开连接时：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;收到 FIN 后必须先回复 ACK；&lt;/li&gt;&#10;&lt;li&gt;但是本端可能还有数据没有发送完；&lt;/li&gt;&#10;&lt;li&gt;等剩余数据发送完成后，才能发送自己的 FIN。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;因此，ACK 和 FIN 通常不能立即合并，形成四次挥手。&lt;/p&gt;&#10;&lt;h4 id="四次挥手"&gt;四次挥手&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;主动关闭方 被动关闭方&#10;&#10;FIN&#10; ------------------------&amp;gt;&#10;&#10; ACK&#10; &amp;lt;------------------------&#10;&#10; FIN&#10; &amp;lt;------------------------&#10;&#10;ACK&#10; ------------------------&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;TCP 是全双工连接，两个方向需要分别关闭。&lt;/p&gt;&#10;&lt;p&gt;一方发送 FIN，只表示自己不再发送数据，并不表示对方也已经没有数据可发。&lt;/p&gt;&#10;&lt;p&gt;如果对方收到 FIN 时，刚好也没有剩余数据需要发送，那么 ACK 和 FIN 也可能合并。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="3-udp-的并发如何实现"&gt;3. UDP 的并发如何实现&#10;&lt;/h3&gt;&lt;p&gt;UDP 没有 TCP 中的：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;listen&#10;accept&#10;connection fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;客户端通过：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;sendto()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;向服务端指定的 IP 和端口发送数据。&lt;/p&gt;&#10;&lt;p&gt;服务端通过：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recvfrom()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收数据，同时得到客户端的 IP 和端口。&lt;/p&gt;&#10;&lt;p&gt;假设有 100 万个客户端同时向同一个 UDP 端口发送数据，内核会按照数据报把数据放入 socket 的接收队列。&lt;/p&gt;&#10;&lt;p&gt;服务端每次调用 &lt;code&gt;recvfrom()&lt;/code&gt; 时，可以获得：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;一条 UDP 数据报；&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;p&gt;应用层可以使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端 IP + 客户端端口&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;标识一个逻辑会话。&lt;/p&gt;&#10;&lt;p&gt;笔记中提出了一种模拟 TCP 的管理方式：&lt;/p&gt;&#10;&lt;ol&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;li&gt;后续通过会话信息管理双方的数据；&lt;/li&gt;&#10;&lt;li&gt;如果需要分散压力，可以使用多个服务端端口或者多个 UDP socket。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;2000 个端口&#10;每个端口管理约 1000 个逻辑连接&#10;总计约 200 万个逻辑连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是，不一定需要为每个 UDP 客户端单独分配一个端口。&lt;/p&gt;&#10;&lt;p&gt;UDP 的并发数量也不是直接受 65535 个端口限制。多个客户端可以通过不同的来源 IP 和来源端口访问同一个服务端端口。&lt;/p&gt;&#10;&lt;p&gt;这一部分需要继续整理成一篇独立的技术笔记：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;UDP 的并发实现与应用层会话管理。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="4-tcp-和-udp-有哪些区别"&gt;4. TCP 和 UDP 有哪些区别&#10;&lt;/h3&gt;&lt;h4 id="连接方式"&gt;连接方式&#10;&lt;/h4&gt;&lt;p&gt;TCP 是面向连接的协议。&lt;/p&gt;&#10;&lt;p&gt;通信前需要建立连接：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端连接 fd ←→ 服务端连接 fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;服务端通过 &lt;code&gt;accept()&lt;/code&gt; 为每个客户端创建连接 fd，因此每条 TCP 连接都有独立的连接状态。&lt;/p&gt;&#10;&lt;p&gt;UDP 是无连接协议。&lt;/p&gt;&#10;&lt;p&gt;客户端不需要先建立 TCP 意义上的连接，可以直接通过 &lt;code&gt;sendto()&lt;/code&gt; 发包，服务端通过 &lt;code&gt;recvfrom()&lt;/code&gt; 接收。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h4 id="数据形式"&gt;数据形式&#10;&lt;/h4&gt;&lt;p&gt;TCP 是字节流协议。&lt;/p&gt;&#10;&lt;p&gt;它传输的是连续字节流，本身不保留应用层的消息边界。&lt;/p&gt;&#10;&lt;p&gt;UDP 是数据报协议。&lt;/p&gt;&#10;&lt;p&gt;每次 &lt;code&gt;sendto()&lt;/code&gt; 发送一条 UDP 数据报，接收端通过 &lt;code&gt;recvfrom()&lt;/code&gt; 接收一条数据报。UDP 会保留数据报边界，但如果接收缓冲区太小，超出的部分可能被截断。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h4 id="可靠性和顺序"&gt;可靠性和顺序&#10;&lt;/h4&gt;&lt;p&gt;TCP 提供：&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;/ul&gt;&#10;&lt;p&gt;先发送的数据，在连接正常的情况下，会先交付给接收方应用程序。&lt;/p&gt;&#10;&lt;p&gt;UDP 本身不保证：&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;/ul&gt;&#10;&lt;p&gt;如果业务需要这些能力，就需要在 UDP 的应用层加入：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;包 ID；&lt;/li&gt;&#10;&lt;li&gt;数据序号；&lt;/li&gt;&#10;&lt;li&gt;ACK 确认；&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;p&gt;例如，把一份大数据拆成多个 UDP 包时，可以在用户空间定义：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;消息 ID&#10;分片序号&#10;总分片数量&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端根据 ID 和序号判断当前收到的是第几个包，并将多个数据包重新组装。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="5-tcp-的分包与粘包"&gt;5. TCP 的分包与粘包&#10;&lt;/h3&gt;&lt;p&gt;TCP 是字节流。&lt;/p&gt;&#10;&lt;p&gt;应用层调用一次：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;send(fd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;并不代表接收端一定通过一次：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recv(fd, buffer, 1024, 0);&#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;一次 send → 多次 recv&#10;多次 send → 一次 recv&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以所谓的 TCP 分包和粘包，本质上是应用层没有明确消息边界。&lt;/p&gt;&#10;&lt;p&gt;常见解决方法有两种。&lt;/p&gt;&#10;&lt;h4 id="方法一长度字段"&gt;方法一：长度字段&#10;&lt;/h4&gt;&lt;p&gt;在应用数据的头部记录消息体长度：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;| 固定长度的消息头 | 消息体 |&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如使用前两个或者前四个字节表示长度：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;| 2 bytes 长度 | 实际数据 |&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端先读取固定长度的消息头：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;先循环读取 2 bytes&#10; ↓&#10;解析出消息体长度&#10; ↓&#10;再循环 recv&#10; ↓&#10;读取完整消息体&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于 TCP 保证字节流有序，因此接收端可以按照这个顺序解析数据。&lt;/p&gt;&#10;&lt;h4 id="方法二分隔符"&gt;方法二：分隔符&#10;&lt;/h4&gt;&lt;p&gt;每条消息后面增加一个特殊分隔符，例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;\r\n\r\n&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端不断读取数据并放入缓冲区，然后查找分隔符：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;收到数据&#10; ↓&#10;加入接收缓冲区&#10; ↓&#10;查找 \r\n\r\n&#10; ↓&#10;找到后切出一条完整消息&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果消息正文中也可能出现相同分隔符，就需要进行转义，或者使用长度字段。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="6-tcp-和-udp-的并发区别"&gt;6. TCP 和 UDP 的并发区别&#10;&lt;/h3&gt;&lt;p&gt;TCP 服务端通常采用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;listen socket&#10; ↓&#10;accept&#10; ↓&#10;每个客户端对应一个连接 fd&#10; ↓&#10;使用 epoll 管理大量连接 fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;TCP 协议栈已经维护了每条连接的状态，因此应用层可以通过不同 fd 区分客户端。&lt;/p&gt;&#10;&lt;p&gt;UDP 没有为每个客户端创建独立的连接 fd。&lt;/p&gt;&#10;&lt;p&gt;服务端一般根据：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;来源 IP&#10;来源端口&#10;应用层会话 ID&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;管理客户端状态。&lt;/p&gt;&#10;&lt;p&gt;因此，不是说 UDP 不能做高并发，而是 UDP 需要在应用层建立自己的会话管理方式。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="7-udp-的使用场景"&gt;7. UDP 的使用场景&#10;&lt;/h3&gt;&lt;p&gt;UDP 更适合以下场景：&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;DNS 等一次请求、一次响应的场景；&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;p&gt;如果一个旧的位置数据由于重传很晚才到达，这个数据可能已经失去意义。因此，这类实时状态通常更偏向使用 UDP。&lt;/p&gt;&#10;&lt;p&gt;但是，“对战游戏没有使用 TCP”这个说法过于绝对。&lt;/p&gt;&#10;&lt;p&gt;更合适的理解是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;游戏中强调实时性的位置、动作等消息更倾向使用 UDP；登录、充值、聊天等要求可靠的数据仍然可能使用 TCP。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="8-tcp-的使用场景"&gt;8. TCP 的使用场景&#10;&lt;/h3&gt;&lt;p&gt;TCP 更适合：&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;HTTP/1.1、HTTP/2 等基于 TCP 的协议；&lt;/li&gt;&#10;&lt;li&gt;长连接通信。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;文件下载需要保证最终文件完整，因此通常需要可靠传输。&lt;/p&gt;&#10;&lt;p&gt;UDP 本身没有拥塞控制和可靠重传。如果使用 UDP 传输文件，应用层就需要自己实现：&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;p&gt;TCP 已经提供了这些能力，因此使用 TCP 开发更加直接。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="9-短通信与长连接"&gt;9. 短通信与长连接&#10;&lt;/h3&gt;&lt;p&gt;DNS 是典型的短请求、短响应场景：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;发送一次查询&#10; ↓&#10;服务端返回查询结果&#10; ↓&#10;本次通信结束&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;UDP 不需要建立连接，适合这种简单的请求响应。&lt;/p&gt;&#10;&lt;p&gt;“UDP 做短连接”更准确的说法是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;UDP 没有 TCP 意义上的连接，适合短暂的一次性请求响应。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;有些业务也会选择 TCP 完成短请求，因为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;TCP 相关框架成熟；&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;p&gt;TCP 则天然适合需要长期保持通信状态的长连接，但 TCP 并不是只能做长连接。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十二阶段总结"&gt;十二、阶段总结&#10;&lt;/h2&gt;&lt;p&gt;从传统阻塞 I/O、epoll 再到 io_uring，容易产生一种混沌感。&lt;/p&gt;&#10;&lt;p&gt;当前最需要捋清的是三个层次之间的关系：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;TCP&#10;提供字节流、连接、可靠传输等协议能力&#10; ↓&#10;epoll / io_uring&#10;解决应用程序如何高效处理 I/O&#10; ↓&#10;业务协议&#10;解决消息边界、心跳、会话和错误处理&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;本轮实验可以得到三个明确认识：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;epoll 应当与非阻塞 I/O 配合，否则单个连接可能阻塞整个事件循环。&lt;/li&gt;&#10;&lt;li&gt;当前测试中，io_uring 的 QPS 全面高于非阻塞 epoll，整体优势约为 17%～21%。&lt;/li&gt;&#10;&lt;li&gt;客户端的连接模型、收发方式和计数准确性，会直接影响测试结果。当前 &lt;code&gt;-c&lt;/code&gt; 参数没有生效，循环收发也尚未实现。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十三面试复盘"&gt;十三、面试复盘&#10;&lt;/h2&gt;&lt;p&gt;面试需要不断复盘。&lt;/p&gt;&#10;&lt;p&gt;简历中能够被问到的内容是有限的，每次面试后都应该记录问题，并重新整理问题背后的知识。&lt;/p&gt;&#10;&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;30 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5%&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;45 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30%&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;60 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;80%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&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;pre tabindex="0"&gt;&lt;code&gt;Offer 数量 / 邀面数量&#10;→ 反映技术能力和面试表现&#10;&lt;/code&gt;&lt;/pre&gt;</description></item></channel></rss>