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 模拟黏包

TCP粘包和拆包

tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法

TCP粘包,拆包及解决方法

TCP网络通讯如何解决分包粘包问题(有模拟代码)

TCP粘包以及解决方案