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);
    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};
        
        //发送数据
        struct DataType dt;
        dt.data1_ = 10;
        dt.data2_ = 200;
        dt.c_ = '+';
        ssize_t send_ret = send(sockfd,&dt,sizeof(dt),0); 
        if(send_ret < 0)
        {
            cout << "send failed" << endl;
            continue;
        }
        send_ret = send(sockfd,&dt,sizeof(dt),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;
}

运行结果如下:

服务端结果:
在这里插入图片描述
客户端结果:
在这里插入图片描述

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
TCP协议是一种面向连接的可靠传输协议,它将数据分成一个一个的数据包进行传输。但是,由于网络传输的不确定性,TCP粘包和分包问题就会出现。 1. TCP粘包问题 TCP粘包问题是指发送方将多个数据包合并成一个数据包发送,接收方无法区分多个数据包的边界,从而无法正确处理数据包。造成TCP粘包问题的原因有多种,比如发送方发送的数据包过大、发送速度过快、网络延迟等。 解决方法: (1) 设置消息边界标识符 在发送的消息中添加一个特殊的标识符,如换行符、空格等,用来标识消息的边界。接收方根据标识符来判断消息的边界,将消息分隔成多个数据包。 (2) 定长消息 可以设置一个固定长度的消息,每次发送的数据都是定长的。这样接收方就可以根据固定长度来将消息分隔成多个数据包。 2. TCP分包问题 TCP分包问题是指发送方将一个数据包分成多个数据包发送,接收方接收后需要将多个数据包组合成一个完整的数据包,才能进行处理。造成TCP分包问题的原因有多种,比如发送方发送的数据包过大、网络拥塞等。 解决方法: (1) 设置消息长度 在消息中添加消息长度信息,接收方接收到数据后,根据长度信息将多个数据包组合成一个完整的数据包。 (2) 固定长度消息 发送方每次发送的数据都是固定长度的,接收方根据固定长度来将多个数据包组合成一个完整的数据包。 总之,TCP粘包和分包问题可以通过合理的协议设计和网络优化来解决

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值