首页 > 其他分享 >tcp 粘包问题及其解决

tcp 粘包问题及其解决

时间:2025-01-13 15:28:29浏览次数:3  
标签:serv addr int tcp 粘包 len 解决 include 数据包

tcp 粘包问题及其解决

在这里插入图片描述

tcp 粘包问题及其解决

tcp及粘包介绍

      TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输协议,它保证了数据的可靠性和顺序性。
      TCP粘包是指在TCP协议中发送方发送的若干个小数据包在接收方接收时被合并成一个大数据包的现象,或者多个大数据包在接收时被分割成若干个小数据包的现象。

      这种现象的产生是因为TCP协议中的流控制和数据传输机制。TCP是面向流的传输协议,发送方将数据流划分为一系列的报文段进行传输,而接收方通过接收和缓存报文段来进行重组和还原数据流。当发送方连续发送多个小的数据包时,如果这些数据包在传输过程中没有被拆分或者合并,那么接收方在接收时就会将它们当作一个大的数据包来处理,从而导致粘包现象的产生。

      TCP粘包可能会导致接收方在处理数据时出现错误,因为接收方可能会将多个数据包误认为是一个完整的数据包,或者将一个完整的数据包错误地拆分成多个小的数据包。[1]

粘包原因

      因为TCP是基于字节流的协议,它只负责把接收到的字节流按照顺序交给应用层,根据TCP的发送和接收策略,它会尽可能多的将数据发送出去或者接收进来,因此多个发送的小数据包就有一定的可能被合并成一个大的数据包发送或者接收,或者一个超大的数据包被拆分成多个合适的小数据包发送或接收。

粘包造成的现象

  1. 服务器一次接收到 n 个完整的数据包(n>=1)的合并

  2. 服务器一次接收到 n 个完整的数据包 以及 前半个数据包 的合并

  3. 服务器一次接收到后半个数据包 以及 n 个完整的数据包 的合并

  4. 服务器一次接收到后半个数据包 以及 前半个数据包 的合并

粘包问题复现

1. 小数据包合并复现

复现描述:下面代码将复现小数据包合并成大数据包发送和接收

服务器端代码:

//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>

int main(){

    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置端口复用
    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);
    bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(sfd, 1024);

    struct sockaddr_in clientsocket;
    socklen_t peerAddrSize=sizeof(struct sockaddr);
    int len = sizeof(struct sockaddr);

    while (1) {
        bzero(&clientsocket, len);

        int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddrSize);

        char buff[10240];
        int times = 0;

        while (1)
        {
            int n = read(cfd,buff,sizeof(buff));
            if (n<0)
            {
                perror("read error\n");
                return -1;
            }else if (n==0)
            {
                printf("client close\n");
                break;
            }
            printf("第%d次 recv len: %d\n",++times,n);

        }

    }


    return 0;
}

客户端代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>


int main(){
    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);

    int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
    if (ret<0)
    {
        perror("conn error");
        return -1;
    }

    char buff[10];
    memset(buff,'1',sizeof(buff));

    while (1)
    {
        write(sfd,buff,sizeof(buff));
    }


    return 0;
}
复现结果

数据传输到后面就开始出现 tcp 粘包现象

在这里插入图片描述
在这里插入图片描述

2. 大数据包拆分复现

复现描述:下面代码将复现大数据包拆分成小数据包发送和接收

服务端代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>

int main(){

    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置端口复用
    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);
    bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(sfd, 1024);

    struct sockaddr_in clientsocket;
    socklen_t peerAddrSize=sizeof(struct sockaddr);
    int len = sizeof(struct sockaddr);

    while (1) {
        bzero(&clientsocket, len);

        int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddr //修改部分 //修改部分//修改部分//修改部分Size);

        char buff[102400];   //修改部分
        int times = 0;

        while (1)
        {
            int n = read(cfd,buff,sizeof(buff));
            if (n<0)
            {
                perror("read error\n");
                return -1;
            }else if (n==0)
            {
                printf("client close\n");
                break;
            }
            printf("第%d次 recv len: %d\n",++times,n);

        }

    }


    return 0;
}

客户端代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>


int main(){
    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);

    int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
    if (ret<0)
    {
        perror("conn error");
        return -1;
    }                

    char buff[102400];   //修改部分
    memset(buff,'1',sizeof(buff));

    while (1)
    {
        write(sfd,buff,sizeof(buff));
    }


    return 0;
}
复现结果

数据包过大,传输过程中拆成若干小包
在这里插入图片描述

在这里插入图片描述

粘包解决

粘包的解决方式

  1. 标志位
          设置一个标志位,当接收端看到标志位表示一个包结束。需要挨个字节判断,不好。

  2. 禁用Nagle算法
          可以禁用Nagle算法,一有数据包就发送,不等待。但是这只是解决了发送端不粘包的问题,接收端还是会粘包,无法解决。

  3. 定长包
          发送方在发送数据前将数据按照固定长度进行切分,接收方在接收到数据后按照固定长度进行处理,这样可以保证每个数据包的长度是固定的,从而避免了粘包现象的产生,但适用场景单一。

  4. 包头添加长度界定
          可以重新封装数据包,在发送数据包的时候,在包头添加长度信息。然后在接收包信息的时候先获取长度信息,然后再读取数据包。下面根据这个写下面代码。

解决代码

读写部分封装

读写封装:

//base_server.h
#ifndef __BASE_SERVER_H___
#define __BASE_SERVER_H___

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

int writen(int fd, const char* msg, int size);
 
int sendMsg(int cfd, const char* msg, int len);

int readn(int fd, char* buf, int size);

int recvMsg(int cfd, char** msg);

#endif

读写封装实现:

//base_server.c
#include "base_server.h"

/*
函数描述: 发送指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int writen(int fd, const char* msg, int size) {
    const char* buf = msg; // 指向待发送数据的指针
    int count = size;      // 记录剩余待发送的数据字节数
 
    while (count > 0) {
        // 尝试发送剩余数据
        int len = send(fd, buf, count, 0);
        if (len == -1) {
            perror("send");
            close(fd);
            return -1; // 发送失败
        } else if (len == 0) {
            continue; // 发送未成功,继续尝试
        }
        buf += len;    // 更新待发送数据的起始地址
        count -= len;  // 更新剩余待发送的数据字节数
    }
    return size; // 全部数据发送完毕,返回发送的总字节数
}
 
/*
函数描述: 发送带有数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, const char* msg, int len) {
    if (msg == NULL || len <= 0 || cfd <= 0) {
        return -1; // 参数无效
    }
 
    // 申请内存空间: 数据长度 + 包头4字节(存储数据长度)
    char* data = (char*)malloc(len + 4);
    if (data == NULL) {
        perror("malloc");
        return -1; // 内存申请失败
    }
 
    // 将数据长度转换为网络字节序(大端序)并存储在包头
    int bigLen = htonl(len);
    memcpy(data, &bigLen, 4);
 
    // 将待发送的数据拷贝到包头后面
    memcpy(data + 4, msg, len);
 
    // 发送带有包头的数据包
    int ret = writen(cfd, data, len + 4);
 
    // 释放申请的内存
    free(data);
 
    return ret; // 返回发送的字节数
}


/*
函数描述: 接收指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - buf: 存储待接收数据的内存的起始地址
    - size: 指定要接收的字节数
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int readn(int fd, char* buf, int size) {
    char* pt = buf; // 指向待接收数据的缓冲区
    int count = size; // 记录剩余需要接收的字节数
 
    while (count > 0) {
        // 尝试接收数据
        int len = recv(fd, pt, count, 0);
        if (len == -1) {
            perror("recv");
            return -1; // 接收失败
        } else if (len == 0) {
        
            return size - count; // 对方关闭连接,返回已接收的字节数
        }
        pt += len;    // 更新缓冲区指针
        count -= len; // 更新剩余需要接收的字节数
    }
    return size; // 返回实际接收的字节数
}
 
/*
函数描述: 接收带数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int recvMsg(int cfd, char** msg) {
    // 接收数据头(4个字节)
    int len = 0;
    int n = readn(cfd, (char*)&len, 4);
    if ( n == 0 ) {
        return 0; // 客户端断开连接了
    }else if (n != 4)
    {
        return -1;// 接收数据头失败
    }
    
 
    // 将数据头从网络字节序转换为主机字节序,得到数据长度
    len = ntohl(len);
    // printf("数据块大小: %d\n", len);
 
    // 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'
    char* buf = (char*)malloc(len + 1);
    if (buf == NULL) {
        perror("malloc");
        return -1; // 内存分配失败
    }
 
    // 接收数据
    int ret = readn(cfd, buf, len);
    if (ret != len) {
        close(cfd);
        free(buf);
        return -1; // 接收数据失败
    }
 
    buf[len] = '\0'; // 添加字符串结束符
    *msg = buf;
 
    return ret; // 返回接收的字节数
}
网络通信部分
服务器端
//server.c
#include <netinet/in.h>
#include <strings.h>
#include "base_server.h"

int main(){

    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置端口复用
    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);
    bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(sfd, 1024);

    struct sockaddr_in clientsocket;
    socklen_t peerAddrSize=sizeof(struct sockaddr);
    int len = sizeof(struct sockaddr);

    while (1) {
        bzero(&clientsocket, len);

        int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddrSize);

        char *recvbuff;
        char buff[10240];
        int times = 0;

        while (1)
        {
            int n = recvMsg(cfd,&recvbuff);
            if (n<0)
            {
                perror("read error\n");
                return -1;
            }else if (n==0)
            {
                printf("client close\n");
                break;
            }
            printf("第%d次 recv len: %d\n",++times,n);
            
            
        }
        
    }


    return 0;
}
客户端
//client.c
#include "base_server.h"

#include <netinet/in.h>
#include <strings.h>


int main(){
    int sfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
    serv_addr.sin_port = htons(9999);

    int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
    if (ret<0)
    {
        perror("conn error");
        return -1;
    }

    char buff[10];
    memset(buff,'1',sizeof(buff));
    int num =0;

    while (1)
    {
    
        int n = sendMsg(sfd,buff,sizeof(buff));
        printf("第%d次发送 %d 个\n",++num,n-4);
        // 403138这个数字随便写的,写大点就行
        if (num==403138)
        {
                break;
        }
        
    }
    

    return 0;
}
编译
gcc server.c base_server.c -o server -I./
gcc client.c base_server.c -o client -I./

运行结果

可以看到粘包问题解决
在这里插入图片描述

在这里插入图片描述

结尾

粘包问题解决。

参考链接

TCP — 百度百科

标签:serv,addr,int,tcp,粘包,len,解决,include,数据包
From: https://blog.csdn.net/qq_44653106/article/details/145012362

相关文章

  • 【docker】docker desktop换国内源时 apply按钮为灰色or换源失败 解决方法
    配docker环境时复制进去国内镜像源后,发现apply按钮为灰色,点不了,如下图解决方法:往下滑,找到下图圈住的选项打勾再回到DockerEngine界面,发现可以点apply按钮了在文本框中添加"registry-mirrors":["http://mirrors.ustc.edu.cn", "http://mirror.azure.cn"]......
  • Mounriver Studio编译器在当前工程中添加文件夹后编译报错问题的解决方法
    在开发一些例程时,往往需要将自己现有的封装好的函数接口文件夹移植进来,但工程编译后往往会出现报未包含的错误,可按以下步骤处理解决:一、这边做示例,随便打开一个工程,假设在该工程目录下添加了一个MOUSE文件夹, 此时这个MOUSE文件夹并没有包含在这个工程的编译路径中,如果在mai......
  • 如何更好的对React Hooks的理解?都解决了什么问题?
    ReactHooks详解:理解与实际应用ReactHooks是React16.8引入的一项重要特性,它彻底改变了组件的写法和管理状态的方式,极大地简化了函数组件的开发。本文将深入探讨ReactHooks的概念、解决的问题,并结合实际项目代码进行讲解。目录结构ReactHooks简介Hooks解决的......
  • 网页上的验证码是为了解决什么问题?说说你了解的验证码种类有哪些
    网页上的验证码主要是为了解决安全问题,确保进行特定操作(如注册、登录、发表评论等)的用户是真实的人,而非自动化的程序或机器人。这有助于防止恶意行为,如密码破解、刷票、论坛灌水等,从而保护网站和用户的安全。以下是我所了解的验证码种类:图形验证码:这是最常见的一种验证码,通常......
  • Ubuntu22.04 解决 E: 无法定位软件包 yum
    1、修改sudovim/etc/apt/sources.list的内容,将下文内容增加至该文件中:debhttp://archive.ubuntu.com/ubuntu/trustymainuniverserestrictedmultiverse#默认注释了源码镜像以提高aptupdate速度,如有需要可自行取消注释debhttps://mirrors.tuna.tsinghua.edu.cn/ubu......
  • 【大数据】beeline 导出文件有特殊字符的问题解决
    问题近期在做大数据查询引擎导出文件的功能,使用了hive的beeline客户端。发现beeline导出的文件以及终端输出有许多特殊字符。按照官方文档使用beeline导出命令:[1]/usr/bin/beeline--silent=true--showHeader=true--outputformat=csv2-fquery.hql</dev/null>/tm......
  • 解决 Git SSL 连接错误:OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno
    问题描述在执行gitpull命令时遇到以下错误:>gitpull--tagsoriginmainfatal:unabletoaccess'对应github仓库':OpenSSLSSL_read:SSL_ERROR_SYSCALL,errno0这个错误通常表示Git在尝试通过HTTPS连接到GitHub时遇到了SSL连接问题。解决方案1.检查网络......
  • 【Ubuntu 无法使用ifconfig解决办法】
    【Ubuntu无法使用ifconfig解决办法】 Ubuntu无法使用ifconfig解决办法在使用ubuntu时需要使用ifconfig命令提示Command'ifconfig'notfound,didyoumean: command'iconfig'fromdebipmiutil(3.1.5-1)  command'pifconfig'fromdebpython3-ethtool(0.14-3......
  • 【详解】SQLServerJDBC到主机的TCP/IP连接失败
    目录SQLServerJDBC到主机的TCP/IP连接失败错误描述原因分析解决步骤1.检查SQLServer服务状态2.检查网络连接3.检查端口4.配置SQLServer接受TCP/IP连接5.检查JDBC驱动版本6.检查连接字符串解释:常见问题排查:1.0x2749(10061)-无法建立连接......
  • MES系统需求设计文档,智能制造系统解决方案,工业制造软件建设方案,工序,生产排程设计(word
    1项目概述1.1项目背景1.2项目目标1.3适用规范标准2需求分析2.1计划管理2.2工艺管理2.3设备管理2.4生产报工2.5异常管理2.6质量管理2.7看板管理2.8统计报表2.9系统安全管理2.10系统接口3系统解决方案3.1网络拓扑图3.2数据......