导读: 当你用 Redis 客户端发一条命令,背后发生了什么?TCP 是字节流,没有消息边界------一次 recv 可能收到多条命令(粘包),也可能一条命令分三次到达(半包)。本文从零实现 MiniKV 的 RESP2 服务器:socket 生命周期管理、
send_all处理部分发送、pending buffer 解决粘包/半包、RESP2 协议五种类型编解码、命令分发。读完你就能写一个能跑的 KV 服务端。
系列回顾:从存储到网络
MiniKV 系列已经走过四篇。前四篇我们完成了存储核心:LRU 淘汰策略、TTL 过期机制、线程安全保证。但一个 KV 存储要真正可用,必须暴露网络接口------这就是本篇的主角:Server 与 Protocol 模块。
回顾一下架构分层:
┌─────────────────────────────┐
│ 客户端 (redis-cli) │
└──────────────┬──────────────┘
│ TCP 字节流
┌──────────────▼──────────────┐
│ Server (socket 管理) │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Protocol (RESP2 编解码) │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Storage (LRU/TTL/线程安全) │
└─────────────────────────────┘

网络层是存储层与外部世界的桥梁。今天要解决的三个核心问题:socket 生命周期管理、TCP 粘包/半包处理、RESP2 协议编解码。
一、Server 的 socket 生命周期
先看最基础的:一个 TCP 服务器怎么从创建到接受连接。server.cpp 的 run() 方法展示了完整流程:
cpp
listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0); // ① 创建
::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, ...); // ② 端口复用
::inet_pton(AF_INET, bind_address_.c_str(), &address.sin_addr); // ③ IP转换
::bind(listen_fd_, ..., sizeof(address)); // ④ 绑定
::listen(listen_fd_, 64); // ⑤ 监听(64 backlog)
::accept(listen_fd_, nullptr, nullptr); // ⑥ 接受连接
六个步骤,每一行都有讲究。
SO_REUSEADDR 是救命稻草 。没有它,服务端重启时会遇到 Address already in use------因为前一个进程的 socket 还处于 TIME_WAIT 状态。这个选项告诉内核:端口可以立即复用。
backlog=64 表示未 accept 的连接队列上限。如果并发连接超过 64,多余的会被内核拒绝。生产环境通常设更大,但 64 对 MiniKV 足够。
优雅停止 :stop() 方法调用 shutdown(SHUT_RDWR) 再 close()。shutdown 先切断读写通道,确保正在处理的数据完成,然后 close 释放资源。直接 close 会粗暴地断开所有连接。

二、send_all:处理部分发送
写网络代码最容易踩的坑:send() 一次没发完。
内核发送缓冲区是有限的。当数据量超过缓冲剩余空间,send() 只发送部分数据就返回。新手常犯的错误:只调用一次 send() 就认为发送完毕,导致对端收到残缺数据。
cpp
bool send_all(int fd, const std::string& data) {
std::size_t sent = 0;
while (sent < data.size()) {
const ssize_t count = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
if (count <= 0) return false;
sent += static_cast<std::size_t>(count);
}
return true;
}
核心逻辑:循环发送,直到全部数据发出。每次 send() 返回实际发送的字节数,更新偏移量继续发。
MSG_NOSIGNAL 是个容易被忽略的细节。当对端已关闭连接,send() 会触发 SIGPIPE 信号------默认行为是杀死进程 。加上 MSG_NOSIGNAL 后,send() 返回 -1,由代码处理错误,而不是进程直接崩溃。

三、pending buffer:TCP 粘包与半包
这是本篇的核心,也是面试高频考点。
TCP 是字节流协议 ,没有消息边界。发送方调两次 send() 发 "HELLO" 和 "WORLD",接收方可能一次 recv() 就收到 "HELLOWORLD"(粘包),也可能分三次收到 "HEL"、"LO"、"WORLD"(半包)。
MiniKV 的解法:pending buffer + 按 '\n' 切分。
cpp
std::string pending;
char buffer[4096];
while (running_.load()) {
const ssize_t count = ::recv(client_fd, buffer, sizeof(buffer), 0);
if (count <= 0) break;
pending.append(buffer, count); // 先累积
std::size_t newline = 0;
while ((newline = pending.find('\n')) != std::string::npos) {
std::string request = pending.substr(0, newline);
pending.erase(0, newline + 1);
if (!request.empty() && request.back() == '\r') request.pop_back();
if (!send_all(client_fd, execute(request))) return;
}
if (pending.size() > 64 * 1024) { // 防御恶意超大请求
send_all(client_fd, Protocol::error("request is too large"));
return;
}
}
处理逻辑分三步:
recv()的数据先 append 到 pending,不急着处理- 循环查找 '\n',找到就切出一条完整请求,执行并返回
- 剩余数据留在 pending,等下次
recv()再处理
这样粘包(一次收到多条)和半包(一条分多次)都被统一处理。粘包时 while 循环会连续切出多条;半包时数据不足,find 找不到 '\n',自然留到下次。
64KB 上限是防御机制。恶意客户端可以发无限长的数据耗尽内存。超过 64KB 直接返回错误并断开连接。

四、RESP2 协议:五种类型
RESP2(REdis Serialization Protocol 2)是 Redis 使用的协议。MiniKV 完整实现了五种类型:
| 类型 | 编码 | 示例 |
|---|---|---|
| Simple String | + 开头 |
+OK\r\n |
| Error | - 开头 |
-ERR msg\r\n |
| Integer | : 开头 |
:1\r\n |
| Bulk String | $长度\r\n数据\r\n |
$5\r\nhello\r\n |
| Nil | $-1\r\n |
键不存在 |
| Array | *数量\r\n+各元素 |
*2\r\n$3\r\nfoo\r\n... |
Bulk String 是重点。它包含长度前缀,接收方先读长度,再读对应字节数------这天然解决了粘包问题。但 MiniKV 的协议设计更简单:每条命令以 '\n' 结尾,用行分隔符切分。
Nil 与 Bulk String 的区别 :GET 不存在的键返回 $-1\r\n(Nil),而不是空字符串 $0\r\n\r\n。客户端能区分「键不存在」和「键值为空」两种语义。

五、命令分发:execute 方法
协议解析完成后,进入命令分发。execute 方法根据命令类型路由到不同处理逻辑:
| 命令 | 响应 |
|---|---|
| PING | +PONG\r\n |
| SET k v | +OK\r\n |
| GET k | $len\r\nv 或 $-1 |
| DEL/EXISTS/EXPIRE/TTL | :N |
| KEYS | *N\r\n... |
| STATS | 数组 |
| 未知命令 | -ERR |
参数数量校验 是隐藏的坑。SET 需要 2 个参数,GET 需要 1 个。参数数不对要返回 -ERR wrong number of arguments,而不是崩溃或静默失败。
大小写不敏感 :SET 和 set 等价。解析时统一转小写再比较,这是协议测试里明确要求的。
六、测试验证
protocol_test.cpp 覆盖三个核心场景:
- 大小写不敏感 :
SET key value和set key value返回相同结果 - 空输入拒绝:空字符串不崩溃,返回错误
- RESP2 编码正确性:每种类型的编码字节序列与规范完全一致
测试是网络层的安全网。粘包处理、边界条件、异常输入------这些逻辑单靠手工测试很难覆盖全。
小结
本篇完成了网络层:socket 生命周期、send_all 保证完整发送、pending buffer 解决粘包/半包、RESP2 五种类型编解码、命令分发。
核心收获:TCP 是字节流,消息边界需要自己定义。MiniKV 用 '\n' 做分隔符,pending buffer 累积数据,按行切分------简单但有效。
- socket 生命周期:socket → SO_REUSEADDR → bind → listen(64) → accept,stop() 用 shutdown+close 优雅退出
- send_all:循环发送直到全部数据发出,MSG_NOSIGNAL 防 SIGPIPE 杀进程
- pending buffer:recv 累积 → 按 '\n' 切分完整请求 → 剩余留待下次,统一解决粘包与半包
- RESP2 五种类型:Simple String/Error/Integer/Bulk String/Nil/Array 的编码格式
- 命令分发:参数数量校验 + 大小写不敏感
下一篇预告
《60 行测试框架的魔法:C++ 静态注册模式》 ------网络层讲完,进入测试体系。MiniKV 的测试框架只有约 60 行,却实现了"零手动注册"的自动测试收集:TEST(name) 宏 + Registrar 静态构造 + 全局 registry。这篇拆解这个精巧的静态注册模式,以及它背后的 C++ 初始化顺序原理。
参考文献与引用
- Redis 协议规范(RESP2) :redis.io/docs/latest/develop/reference/protocol-spec------五种类型的编码格式权威定义
- Beej's Guide to Network Programming :beej.us/guide/bgnet------socket 生命周期、send/recv 语义的经典参考
📥 下载完整源码 :如需整个工程的源码,请在下面的链接下载:
觉得有用?点个关注,持续获取优质内容。欢迎留言讨论。