📌 核心结论(TL;DR):
TCP 粘包本质 :TCP 是字节流,没有边界,必须依靠应用层自定义协议来划分边界。
协议设计 :通常采用
[长度]\r\n[数据体]\r\n的格式,接收方先读长度,再按长度读数据。序列化:网络不能直接传结构体,需要把数据转成 JSON 字符串(序列化),接收后再转回结构体(反序列化)。
守护进程(Daemon) :Linux 后台服务程序。核心步骤:
fork退出父进程、setsid创建新会话、重定向标准 I/O 到/dev/null。
一、 为什么 TCP 会"粘包"?

UDP 是面向数据报的,一次发一个包,有边界;而 TCP 是面向字节流的,它是一堆连续的字节。如果发送方连续发了两个数据包,接收方底层 read 时,可能一次性把两个包都读到了(粘包),也可能只读到了半个包(半包)。
二、 应用层自定义协议
解决粘包的最常见做法,就是双方约定一个格式。我们约定报文格式为:有效载荷长度\r\n有效载荷内容\r\n。
三、 代码实战:协议的编解码
(插入 Encode 和 Decode 代码)
cpp
// 编码:len\r\nx op y\r\n
std::string Encode(const std::string &message) {
std::string len = std::to_string(message.size());
return len + "\r\n" + message + "\r\n";
}
// 解码:判断数据完整性,并截取
bool Decode(std::string &package, std::string *message) {
auto pos = package.find("\r\n");
if (pos == std::string::npos) return false;
int messageLen = std::stoi(package.substr(0, pos));
int total = pos + 2 + messageLen + 2;
if (package.size() < total) return false; // 半包
*message = package.substr(pos + 2, messageLen);
package.erase(0, total);
return true;
}
四、 序列化与反序列化
我们引入了 jsoncpp 库,Request 结构体通过 Json::FastWriter 转成 JSON 字符串发出,接收方通过 Json::Reader 解析成对象,非常方便。
五、 守护进程:让程序在后台默默运行
(插入 Daemon 函数代码)
我们写的服务器程序如果直接跑,一旦关掉终端它就退出了。要变成真正的服务器,必须守护进程化:
-
fork后父进程退出(脱离终端)。 -
setsid()创建新会话。 -
将
0, 1, 2文件描述符重定向到/dev/null,防止输出报错影响主进程。
自我实战操作
手敲一个极简协议 和守护进程。
bash
cd ~/linux_study/network_test
mkdir protocol_test
cd protocol_test
实战 1:手写协议的编解码(解决粘包)
bash
vim protocol.h
(直接复制下面的代码,体会 \r\n 作为分隔符的妙用)
cpp
#pragma once
#include <iostream>
#include <string>
// 协议格式: "长度\r\n数据\r\n"
const std::string LineBreakSep = "\r\n";
// 编码
std::string Encode(const std::string &message) {
std::string len = std::to_string(message.size());
return len + LineBreakSep + message + LineBreakSep;
}
// 解码
bool Decode(std::string &package, std::string *message) {
// 1. 找第一个 \r\n,截取长度
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos) return false; // 连长度都没收全
std::string lens = package.substr(0, pos);
int messageLen = std::stoi(lens);
// 2. 计算总长度:长度字段长度 + 数据长度 + 两个 \r\n 的长度
int total = lens.size() + messageLen + 2 * LineBreakSep.size();
if (package.size() < total) {
return false; // 数据还没收全,半包
}
// 3. 截取有效数据,并删除已处理的部分
*message = package.substr(pos + LineBreakSep.size(), messageLen);
package.erase(0, total);
return true;
}
实战 2:守护进程
bash
vim daemon.c
(注意核心的 3 步)
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 1. 忽略子进程退出信号和管道破裂信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. fork并退出父进程 (脱离控制终端)
if (fork() > 0) exit(0);
// 3. 创建新会话 (摆脱终端的控制)
setsid();
// 4. 改变工作目录到根目录
chdir("/");
// 5. 重定向标准输入、输出、错误到 /dev/null
int fd = open("/dev/null", O_RDWR);
if (fd > 0) {
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
// 守护进程主体逻辑 (死循环)
while (1) {
// 后台默默干活,比如写日志、监听网络等
sleep(5);
}
return 0;
}
编译运行 gcc daemon.c -o daemon && ./daemon 。
你会发现程序运行后没有任何输出,直接回到了终端。敲 ps aux | grep daemon 就能看到它在后台一直跑着。

