TCP问题详解
2026/8/30 14:58:30 网站建设 项目流程

TCP粘包拆包原因拆解:

1. 发送数据太大,装不下 → 拆包

你可以把 TCP 发送缓冲区 理解成一个小快递箱子。

你一次性塞了超大一堆数据

箱子装不下

系统只能强行分成好几箱分批发货

👉 对应:你发的数据 > 发送缓冲区大小 = 拆包

2. 发送数据太小,系统攒一攒再发 → 粘包

还是快递箱子:

你每次只发一点点小数据

箱子还空很大,直接发太浪费快递费(浪费网络资源)

操作系统很聪明:先憋着不发,等你多发几次,装满箱子一起发

结果:你发的 3 条消息,被合成 1 条发到对面。👉 这就是粘包

3. 网络硬件有最大限制(MSS/MTU)→ 拆包

马路有宽度限制,货车载货长度不能超标:

网络里每一个「数据包」也有法定最大长度

你发的数据包太长,超过网络规定上限

必须切碎成小段,才能正常在路上传输

简单对应:

MTU:整条路最大允许的货车总长

MSS:货车里能装的纯货物最大长度

数据超标 → 强制切碎 → 拆包

4. 接收方读数据太慢 → 粘包

换接收端视角:

对方发过来的数据,会先放到电脑的临时缓存区(仓库)

你的程序如果偷懒、读数据很慢

新的数据不断进来,全都堆在仓库里挤在一起

等你终于来读的时候,一次性读出好几条混在一起的数据

👉 前面的没拿走,后面的堆上来 = 粘包

TCP 粘包/拆包 三种最简单解决方案:

方案一:固定长度

原理:

规定每条消息固定多少字节,比如统一每条 1024 字节。

优缺点:

✅ 逻辑超简单,代码好写

❌ 浪费流量,短消息也要占满固定长度

适用情景:

消息永远很短(比如验证码、指令、状态)

长度永远固定(比如 4 字节数字、8 字节 ID)

绝对不能用于:

聊天消息

长文本

文件

图片

动态长度数据

方案二:首尾分隔符

原理

在每条消息的末尾加特殊分隔符,比如 \n、#、&&要求:分隔符不能出现在正常业务数据里。

例子

两条消息:今天吃饭#明天上课#

哪怕 TCP 粘包拼成:今天吃饭#明天上课#

接收端不断读数据,只要读到 #,就截取前面当成一条完整消息。

优缺点

✅ 实现简单,文本协议(聊天、HTTP)常用

❌ 二进制数据(文件、图片)不适合,容易和分隔符冲突

为什么二进制数据不可以呢?

二进制文件是纯机器编码,没有 “冷门” 一说,

任何单字节、多字节组合,都有概率天然出现在文件内容里。

方案三:消息头 + 数据长度

原理:

每条数据分为两部分:

消息头(固定字节):存「后面数据的长度」

消息体:真实要发送的数据

流程:

发送

先计算消息体有多少字节

把「长度」写到数据包开头

长度 + 真实数据 一起发出去

接收

第一步:先读取固定长度的消息头,解析出「本次消息有多长」

第二步:根据长度,精准读取对应字节的消息体

读完一条,再循环取下一条

例子

要发数据:hello world假设用 4 字节存长度:

数据长度:11

发送内容:[长度11] + hello world

接收:先读 4 字节 →

string recvMsg(int fd){ //1读取长度头 int netlen=0; int ret=recv(fd,&netlen,HEADER_LEN,0); if(ret<=0) return ""; //2转回本机字节序 int datalen=ntohl(ret); //3按需读取完整数据 char buf[1024]={0}; recv(fd,buf,datalen,0); return string(buf); }

知道要收 11 个字节 → 精准只读 11 字节,完美拆分。

优缺点

✅ 不浪费流量、二进制 / 文本都能用、工业级标准方案

❌ 比前两种多一点点代码,但是最通用

代码实现简单socket功能

程序1:attempt_client.cpp

程序2:attempt_server.cpp

g++ attempt_server.cpp -o attempt_server

g++ attempt_client.cpp -o attempt_client

先跑服务端

./attempt_server

再开一个窗口跑客户端

./attempt_client

attempt_client.cpp

#include <bits/stdc++.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; const int HEADER_LEN=4; void sendMsg(int fd,string& msg){ //1获取数据长度 int len=msg.size(); //2先发固定的4个字节长度(网络字节序) int netLen=htonl(len); send(fd, &netLen, HEADER_LEN, 0); //3发数据 send(fd,msg.c_str(),len,0); return; } string recvMsg(int fd){ //1读取长度头 int netlen=0; int ret=recv(fd,&netlen,HEADER_LEN,0); if(ret<=0) return ""; //2转回本机字节序 int datalen=ntohl(ret); //3按需读取完整数据 char buf[1024]={0}; recv(fd,buf,datalen,0); return string(buf); } int main(){ //1创建socket int clientFd=socket(AF_INET,SOCK_STREAM,0); //2连接服务端 127.0.0.1:8888 sockaddr_in serAddr; // ① 创建一个“地址信息”变量 serAddr.sin_family = AF_INET; // ② 用IPv4地址(固定写法) serAddr.sin_port = htons(8888); // ③ 端口号:8888 serAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ④ IP:本机 connect(clientFd, (sockaddr*)&serAddr, sizeof(serAddr)); // ⑤ 发起连接 cout<<"连接服务端成功"<<endl; string input; while(1){ cout<<"请输入信息"<<endl; getline(cin,input); sendMsg(clientFd,input);//发 string res=recvMsg(clientFd); cout<<"服务端回复"<<res<<endl; } close(clientFd); return 0; }

attempt_server.cpp

#include <bits/stdc++.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; const int HEADER_LEN=4; void sendMsg(int fd,const string& msg){ //1获取数据长度 int len=msg.size(); //2先发固定的4个字节长度(网络字节序) int netLen=htonl(len); send(fd,&netLen,HEADER_LEN,0); //3发数据 send(fd,msg.c_str(),len,0); return ; } string recvMsg(int fd){ //1读取长度头 int netlen=0; int ret=recv(fd,&netlen,HEADER_LEN,0); if(ret<=0) return ""; //2转回本机字节序 int datalen=ntohl(ret); //3按需读取完整数据 char buf[1024]={0}; recv(fd,buf,datalen,0); return string(buf); } int main(){ //1创建socket int listenFd=socket(AF_INET,SOCK_STREAM,0); // sockaddr_in serAddr; serAddr.sin_family = AF_INET; serAddr.sin_port = htons(8888); serAddr.sin_addr.s_addr = INADDR_ANY; //3 bind绑定 bind(listenFd,(sockaddr*)&serAddr,sizeof(serAddr)); //4 listen监听 listen(listenFd, 5); // 最多排队5个连接 cout << "服务端启动,监听8888...\n"; //5 accept等待客户端连接 int connFd=accept(listenFd,NULL,NULL); while(1){ string res=recvMsg(connFd); cout<<"收到客户端消息:"<<res<<endl; sendMsg(connFd, "已收到:" + res); } close(connFd); close(listenFd); return 0; }

函数解释

htonl()

在attempt_client.cpp中,发送长度时,用到htonl().其目的是,转为网络字节序.

那么,为什么进行这一操作?

因为不同电脑的 “字节存储顺序” 不一样,不转成统一的网络字节序,接收方读到的长度会变成乱数,直接解析失败。

不同电脑字节存储顺序不同,如何理解?

简单说:多字节数字在内存里,是从左往右存,还是从右往左存?

有两种:

小端序(主机字节序)

大端序(网络字节序)

socket()

1 作用:

创建一个"网络插座",让程序有收发网络数据功能

2 返回值:

成功:返回文件描述符fd

失败:-1

3 参数:

socket(协议族/地址族, 数据传输方式,0);

决定使用什么网络类型 TCP/UDP

常用:socket(AF_INET,SOCK_STREAM,0)

connect()

1 作用:

客户端用它去连接指定IP和端口的服务器,即主动 “打电话” 连接服务器

2 前提:

用socket创建好fd

3 返回值:

成功0,失败-1

4 参数:

connect(fd,(sockaddr*)&serAddr,sizeof(serAddr))

fd:文件描述符

(sockaddr*)&serAddr:

结构体指针,存服务器的IP地址+端口号

因此,在写connect之前,要有:

sockaddr_in serAddr; serAddr.sin_family=AF_INET; serAddr.sin_port=htons(xxxx);//服务器端口 serverAddr.sin_addr.s_addr=inet_addr("x.x.x.x"); //服务器IP

常见形式:

//2连接服务端 127.0.0.1:8888 sockaddr_in serAddr; // ① 创建一个“地址信息”变量 serAddr.sin_family = AF_INET; // ② 用IPv4地址(固定写法) serAddr.sin_port = htons(8888); // ③ 端口号:8888 serAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ④ IP:本机 connect(clientFd, (sockaddr*)&serAddr, sizeof(serAddr)); // ⑤ 发起连接

send()

1 作用:

往TCP连接里“发数据”,把数据从本机发送到已经连接好的对端

2 返回值:

成功返回发送的字节数,失败返回-1

3 参数:

send(fd,要发送的数据的内存地址,len,0)

要发送的数据的内存地址:

如果是int netLen ,则为 &netLen

如果是string s,则为 s.c_str()

len:

要发送的字节数

如果是int netLen ,则为 4

如果是string s,则为 s.size()

recv()

1 作用:

从TCP连接里 “收数据”.

读取已连接socket上收到的数据

阻塞时会一直等待,直到有数据到来

2 返回值:

成功返回读到的字节数,断开连接返回 0,失败返回 -1

3 参数:

与send类似,len的意义变为,最多可以接受的字节数

常用:

char buf[1024]={0};

recv(fd,buf,datalen,0);

流程

客户端完整流程:

plaintext

socket() → 创建客户端插座

connect() → 【连接服务器】

send() → 发数据(你最开始的函数)

recv() → 收数据

close() → 关闭连接

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询