Linux:TCP粘包问题的模拟实现以及解决方法
Posted It‘s so simple
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux:TCP粘包问题的模拟实现以及解决方法相关的知识,希望对你有一定的参考价值。
目录
1. TCP粘包问题的模拟实现
1.1 何谓TCP粘包
因为TCP协议是有连接,可靠有序,面向字节流的协议,也正是它面向字节流这个特性,导致存放在接收缓冲区的数据没有明显的界限,TCP的recv函数在接收数据的时候不会识别数据是第一条还是第二条,而是直接根据规定的大小进行读取数据,而我们每次都不知道发送数据方发送的数据大小,因此再读取数据的时候,极有可能读取到不完整的数据,或者说是粘连的数据。举个例子来看:
如果按照我们自己的逻辑,服务端应该给客户端返回两次结果 2、4;但是这里服务端只会给客户端返回一次结果,即1+12+2 = 15。这就与我们的预期不符,因此,这就是TCP的粘包问题。
接下来我们来对其进行模拟实现。
1.2 TCP粘包问题的模拟实现
本次我们使用的是多线程的TCP版本代码,同上篇文章Linux:TCP Socket编程(代码实战)一样,这里我们还是使用封装类的形式实现客户端和服务端之间的通信,为了实现TCP粘包问题,我们在这里规定客户端连续给服务端发送两次数据,然后服务端每次接收数据的时候,直接读取buf所能读取的最大数据。
封装类代码 tcp.hpp
#pragma once
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <string>
#include <iostream>
using namespace std;
class tcp
public:
tcp() : sockfd_(-1)
tcp(int sock) : sockfd_(sock)
~tcp()
close(sockfd_);
int createSockfd()
sockfd_ = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd_ < 0)
cout << "socket failed" << endl;
return -1;
return sockfd_;
int Bind(string ip = "0.0.0.0",uint16_t port = 18989)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(sockfd_,
(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
cout << "bind failed" << endl;
return -1;
return ret;
int Listen(int backlog = 2)
int ret = listen(sockfd_,backlog);
if(ret < 0)
cout << "listen failed" << endl;
return -1;
return ret;
int Accept(struct sockaddr_in* addr,socklen_t* socklen)
int new_sockfd = accept(sockfd_,
(struct sockaddr *)addr,socklen);
if(new_sockfd < 0)
cout << "accept failed" << endl;
return -1;
return new_sockfd;
ssize_t Recv(char* buf,size_t len)
ssize_t ret = recv(sockfd_,buf,len,0);
if(ret < 0)
cout << "recv failed" << endl;
return ret;
ssize_t Send(char* buf,size_t len)
ssize_t ret = send(sockfd_,buf,len,0);
if(ret < 0)
cout << "send faild" << endl;
return -1;
return ret;
ssize_t Recv(struct DataType* dt);
private:
int sockfd_;
;
服务端代码
#include <vector>
#include "tcp.hpp"
#include <boost/algorithm/string.hpp>
// 利用boost库中的split函数对字符串进行分割
class StringUtil
public:
static void Split(const std::string& input,
const std::string& split_char,
std::vector<std::string>* output)
boost::split(*output, input, boost::is_any_of(split_char),
boost::token_compress_off);
;
int Sum(string& data)
//切割或者数据, 按照“+”
vector<string> output;
StringUtil::Split(data, "+", &output);
int total_sum = 0;
for(size_t i = 0; i < output.size(); i++)
total_sum += atoi(output[i].c_str());
return total_sum;
void* TcpEntryPthread(void* arg)
pthread_detach(pthread_self());
tcp *tc = (tcp*) arg;
while(1)
char buf[1024] = 0;
ssize_t ret = tc->Recv(buf,sizeof(buf)-1);
if(ret < 0)
cout << "recv failed" << endl;
continue;
else if(ret == 0)
cout << "peer close" << endl;
break;
printf("client say: %s\\n",buf);
string tmp(buf);
int total = Sum(tmp);
memset(buf,'\\0',sizeof(buf));
sprintf(buf,"%d",total);
ret = tc->Send(buf,strlen(buf));
if(ret < 0)
cout << "send failed" << endl;
continue;
delete tc;
return nullptr;
int main()
tcp tc;
int ret = tc.createSockfd();
if(ret < 0)
return -1;
//默认ip 0.0.0.0,默认端口18989
ret = tc.Bind();
if(ret < 0)
return -1;
//默认已完成连接队列大小为2
ret = tc.Listen();
if(ret < 0)
return -1;
while(1)
struct sockaddr_in addr;
socklen_t socklen = sizeof(addr);
int new_sockfd = tc.Accept(&addr,&socklen);
if(new_sockfd < 0)
cout << "Please again to accept" << endl;
continue;
//从这里开始创建工作线程
//如果只是单纯的将new_sockfd传过去的话,是不行的
//因为它是一个局部变量,因此,我们需要在堆上开辟出一个空间
tcp *t = new tcp(new_sockfd);
if(t == nullptr)
cout << "new class tcp failed" << endl;
continue;
pthread_t tid;
ret = pthread_create(&tid,NULL,TcpEntryPthread,t);
if(ret < 0)
cout << "pthread_create failed" << endl;
delete t;
continue;
客户端代码
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <iostream>
using namespace std;
int main()
//1. 创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
cout << "TCP socket failed" << endl;
return 0;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(18989);
//此处我们使用的公网ip
addr.sin_addr.s_addr = inet_addr("118.89.67.215");
int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
cout << "connnect failed" << endl;
return 0;
//3.接收和发送数据
while(1)
char buf[1024] = 0;
sprintf(buf,"10+200+11");
ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
if(send_ret < 0)
cout << "send failed" << endl;
continue;
send_ret = send(sockfd,buf,strlen(buf),0);
if(send_ret < 0)
cout << "send failed" << endl;
continue;
memset(buf,'\\0',sizeof(buf));
ssize_t recv_size = recv(sockfd,buf,sizeof(buf)-1,0);
if(recv_size < 0)
cout << "recv failed" << endl;
continue;
else if(recv_size == 0)
cout << "Peer close" << endl;
close(sockfd);
return 0;
cout << "recv data is : " << buf << endl;
close(sockfd);
return 0;
运行结果如下:
服务端结果:
客户端结果:
这里可以清晰的看到TCP粘包问题的现象,就不再做过多描述了。
2. TCP粘包问题的解决办法
解决方法有两种:
① 使用定长字节发送:比如用一个定长结构体去封装数据,它在本质上限制了接收数据的字节大小,但是这种方法只是在 理论基础上能实现而已,因为在现实生活中,数据的种类太过繁杂,我们不可能对每一条数据,每一条消息都定义一个数据结构去进行封装,这样是不现实的。其实这就是自定制协议实现的一种形式。
② 在应用层中,对应用数据产生的数据进行包装,即给应用数据加上头部描述信息,并在应用数据的尾部加上相应的分割符。
话不多说,都在图里:
这里我们需要注意的是:自定制协议是应用层的一个协议,它是想要将应用层数据也按照某种格式的规范进行传输的一种协议。
这里我们使用第一种解决办法(自定制协议)来对上述我们自己造成的TCP粘包问题进行解决。
我们首先给出我们所定义出的定长的数据结构 DateType.hpp。
#pragma once
struct DataType
int data1_;
int data2_;
char c_;
;
然后再给出服务端和客户端的代码:
服务端代码:
#include <vector>
#include <algorithm>
#include "tcp.hpp"
//对Recv 进行重载
ssize_t tcp::Recv(struct DataType* dt)
ssize_t ret = recv(sockfd_,dt,sizeof(*dt),0);
if(ret < 0)
cout << "recv failed" << endl;
return ret;
int Sum(struct DataType* dt)
int ret = -1;
if(dt->c_ == '+')
ret = dt->data1_ + dt->data2_;
return ret;
void* TcpEntryPthread(void* arg)
pthread_detach(pthread_self());
tcp *tc = (tcp*) arg;
while(1)
struct DataType dt;
ssize_t ret = tc->Recv(&dt);
if(ret < 0)
cout << "recv failed" << endl;
continue;
else if(ret == 0)
cout << "peer close" << endl;
break;
printf("client say: %d,%d,'%c'\\n",dt.data1_,dt.data2_,dt.c_);
int sum_ret = Sum(&dt);
char buf[1024] = 0;
sprintf(buf,"%d\\n",sum_ret);
ret = tc->Send(buf,strlen(buf));
if(ret < 0)
cout << "send failed" << endl;
continue;
delete tc;
return nullptr;
int main()
tcp tc;
int ret = tc.createSockfd();
if(ret < 0)
return -1;
//默认ip 0.0.0.0,默认端口18989
ret = tc.Bind();
if(ret < 0)
return -1;
//默认已完成连接队列大小为2
ret = tc.Listen();
if(ret < 0)
return -1;
while(1)
struct sockaddr_in addr;
socklen_t socklen = sizeof(addr);
int new_sockfd = tc.Accept(&addr,&socklen);
if(new_sockfd < 0)
cout << "Please again to accept" << endl;
continue;
tcp *t = new tcp(new_sockfd);
if(t == nullptr)
cout << "new class tcp failed" << endl;
continue;
pthread_t tid;
ret = pthread_create(&tid,NULL,TcpEntryPthread,t);
if(ret < 0)
cout << "pthread_create failed" << endl;
delete t;
continue;
客户端代码:
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <iostream>
#include "DataType.hpp"
using namespace std;
int main()
//1. 创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
以上是关于Linux:TCP粘包问题的模拟实现以及解决方法的主要内容,如果未能解决你的问题,请参考以下文章
网络通信中TCP出现的黏包以及解决方法 socket 模拟黏包