手写一个 C++ TCP 服务器实现自定义协议(顺便解决粘包问题)
在之前的网络编程实践中,我们直观地感受了 UDP 和 TCP 套接字的使用,成功完成了客户端与服务端的通信。但这里有一个关键细节常被忽略:UDP 基于数据报传输,发送即完整;而 TCP 基于字节流传输,如何保证读取上来的数据是一个完整的报文?
TCP 的字节流特性与粘包风险
TCP 是面向连接的协议,主要负责控制何时发送、发送多少以及出错如何处理。我们在应用层调用 read/write 系统调用时,数据只是从用户空间拷贝到了内核空间的 TCP 缓冲区。至于何时真正发送给对方,完全由 TCP 协议栈决定。
这就导致接收端面临不确定性。比如发送方发了一个长报文,接收方可能只收到一部分就返回给用户;或者一次 read() 读到了多个报文。这就像你通过微信道歉,本想发'我不后悔我爱你',结果对方只收到了'我不后悔',误会由此产生。
为了避免这种差异,我们必须制定应用层协议,确保数据的边界清晰。
自定义应用层协议设计
为什么需要序列化?
想象一个聊天群,我发送'哈哈哈',实际传输的不止这三个字,还有昵称、头像、时间戳等元数据。看似简单的字符串,背后其实是结构化数据的序列化和反序列化过程。
为了演示这一点,我们构建一个简单的网络版计算器。
协议格式
由于 TCP 没有消息边界,我们需要自定义协议头来标识长度。采用如下格式:
len\n
content\n
例如发送 10 + 20,编码后变为:
7\n10 + 20\n
这种 长度 + 换行 + 内容 + 换行 的结构,让接收方能准确知道要读多少个字节。
核心代码实现
协议封装:Encode 与 Decode
这是解决粘包的关键。我们需要将业务数据打包成符合协议的字符串,并在接收端解析。
编码函数 Encode:
std::string Encode(std::string &content) {
std::string s;
size_t len = content.size();
s += std::to_string(len);
s += "\n";
s += content;
s += "\n";
return s;
}
解码函数 Decode:
这个函数负责从缓冲区中提取完整报文。它做了三件事:判断头部是否完整、判断数据量是否足够、解析并移除已处理部分。
bool Decode(std::string &s, std::string *content) {
size_t left_pos = s.find("\n");
if (left_pos == std::string::npos) return ;
std::string content_len = s.(, left_pos);
len = std::(content_len);
(s.() < content_len.() + len + ) ;
*content = s.(left_pos + , len);
s.(, content_len.() + len + );
;
}


