草庐IT

TCP分包和粘包

小瑞的学习笔记 2024-05-01 原文

文章目录

TCP分包和粘包

TCP分包

  • 场景:发送方发送字符串”helloworld”,接收方却分别接收到了两个数据包:字符串”hello”和”world”发送端发送了数量较多的数据,接收端读取数据时候数据分批到达,造成一次发送多次读取;

造成分包的原因:

  • TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送.

  • 这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。

  • 相关的,路由器有一个MTU( 最大传输单元)一般是1500字节,除去IP头部20字节,留给TCP的就只有MTU-20字节。所以一般TCP的MSS为MTU-20=1460字节

  • 当应用层数据超过1460字节时,TCP会分多个数据包来发送。

TCP 粘包

  • 场景:发送方发送两个字符串”hello”和”world”,接收方却接收到了字符串”helloworld”
  • 发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数

造成TCP粘包的原因:

  • TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.
  • 该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送.
  • 如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端.

分包和粘包解决方案:

发送数据前,给数据附加的长度:

  • 粘包解决方案:传入数据长度,告诉服务器我本次发送的数据只有这么多,你可别读多了。
  • 分包解决方案:传入数据长度,告诉服务器我本次发送的数据有这么多,你可别读少了。

  1. 特殊标识: 包头部的特殊标识,用来标识包的开始 (自定义)
  2. 数据长度:数据包的大小,固定长度,2、4 或者8字节。
  3. 数据内容:数据内容,长度为数据头定义的长度大小。
  4. 一旦接收方发现标识FBEB,代表接下来N个字节代表数据长度

实际操作如下:
a)客户端:先发送包表示和长度,再发送数据内容。
b)服务端:先解析本次数据包,获取数据大小N,再读取N个字节,这N个字节就是一个完整的数据内容。

程序由回声服务器修改而来 , 客户端发送小写字母 , 服务端接收后把他转化为大写字母发送给客户端

先上结果

echo_sever.c

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

#define SERVER_PORT 9999

const int DATA_LEN_BYTES = 4; // 长度字段定义为两个字节
const char *TAG = "ABCD";// 标志位

int read_package(int client_sock , char *buf){
    int tag_len = strlen(TAG);

    // 读取前8个字节 即读取标志位和数据长度
    int readLen = read(client_sock , buf , tag_len + DATA_LEN_BYTES);

    int data_len = 0;
    if(strncmp(buf ,TAG , 4) == 0){
        // 有标志位 那么就要读取数据长度
        data_len = *((int* )(buf + tag_len));

        int count = 0;// 当前已经读取的字节数
        int len = 0;// 本次读取的字节数

        // 把标志位和数据长度清空,放数据
        memset(buf,'0',tag_len + DATA_LEN_BYTES);

        while(count < data_len){
            // 数据量比较大的话有可能一次读不完,要读取很多次
            // 直到读取的数据长度为data_len

            // 每次期望读取data_len - count 实际读取为len
            // len = read(client_sock , buf + count , data_len - count);

            // 读取数据写1,是为了模拟多次读取
            len = read(client_sock , buf + count , 2);
            printf("本次读取数据量为 %d\n" , len);

            count += len;
        }

    }else{
        /*如果没有标志位,表示传过来的是普通数据
          这里先不做处理*/
    }

    return data_len;
}



int main(void)
{

    int sock; // 代表信箱
    struct sockaddr_in server_addr;

    // 1.美女创建信箱
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // 2.清空标签,写上地址和端口号
    bzero(&server_addr, sizeof(server_addr));

    server_addr.sin_family = AF_INET;                // 选择协议族IPV4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);       // 绑定端口号

    // 实现标签贴到收信得信箱上
    bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // 3.把信箱挂置到传达室,这样,就可以接收信件了
    listen(sock, 128);

    // 万事俱备,只等来信
    printf("等待客户端的连接\n");

    int done = 1;

    while (done)
    {
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];

        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);

        // 打印客服端IP地址和端口号
        printf("client ip: %s\t port : %d\n",
               inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
               ntohs(client.sin_port));

        /*读取客户端发送的数据*/
        len = read_package(client_sock , buf);
        buf[len] = '\0';
        printf("receive[%d]: %s\n", len, buf);


        // 转换成大写
        for (i = 0; i < len; i++)
        {
            buf[i] = toupper(buf[i]);
        }

        len = write(client_sock, buf, len);

        printf("finished. len: %d\n", len);
        close(client_sock);
    }
    close(sock);
    return 0;
}

echo_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define SERVER_PORT 9999
#define SERVER_IP  "127.0.0.1"


const int DATA_LEN_BYTES = 4; // 长度字段定义为两个字节
const char *TAG = "ABCD";// 标志位

int main(int argc, char *argv[]){

    int sockfd;
    char *message;
    struct sockaddr_in servaddr;
    int n;
    char *buf = NULL;

    if(argc != 2){
        fputs("Usage: ./echo_client message \n", stderr);
        exit(1);
    }

    message = argv[1];

    printf("message: %s\n", message);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&servaddr, '\0', sizeof(struct sockaddr_in));

    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
    servaddr.sin_port = htons(SERVER_PORT);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    /**********************************************/
    // 组装数据包
    int ms_len = strlen(message);
    int tag_len = strlen(TAG);

    // 标志位 + 数据长度 + 数据 + \0
    buf = (char *)malloc(tag_len + DATA_LEN_BYTES + ms_len);
    strcpy(buf , TAG);// 标志位
    *((int*)(buf + tag_len)) = ms_len;// 数据长度
    memcpy(buf + tag_len + DATA_LEN_BYTES , message , ms_len);//数据

    
    // 发送数据
    write(sockfd , buf , tag_len + DATA_LEN_BYTES);// 发送标志位和数据长度
    sleep(1);
    write(sockfd , buf + tag_len + DATA_LEN_BYTES , ms_len);// 发送数据




    n = read(sockfd, buf, sizeof(buf)-1);

    if(n>0){
        buf[n]='\0';
        printf("receive: %s\n", buf);
    }else {
        printf("read error\n");
    }

    printf("finished.\n");
    close(sockfd);

    return 0;
}

有关TCP分包和粘包的更多相关文章

  1. 计算机网络笔记:TCP三次握手和四次挥手过程 - 2

    TCP是面向连接的协议,连接的建立和释放是每一次面向连接的通信中必不可少的过程。TCP连接的管理就是使连接的建立和释放都能正常地进行。三次握手TCP连接的建立—三次握手建立TCP连接①若主机A中运行了一个客户进程,当它需要主机B的服务时,就发起TCP连接请求,并在所发送的分段中用SYN=1表示连接请求,并产生一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x。主机B收到A的连接请求报文,就完成了第一次握手。客户端发送SYN=1表示连接请求客户端发送一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x②主机B如果同意建立连接,则向主机A发送确认报

  2. ruby - yarn 未初始化常量 Socket::SOL_TCP - 2

    我在这里尝试使用yarn,遇到了一个可能与ruby​​相关的问题。在执行任何yarn命令,我收到错误.../.rvm/gems/ruby-2.3.0/gems/yarn-0.1.1/lib/yarn/server.rb:14:in':uninitializedconstantSocket::SOL_TCP(NameError)错误堆栈:$yarn.../.rvm/gems/ruby-2.3.0/gems/yarn-0.1.1/lib/yarn/server.rb:14:in':uninitializedconstantSocket::SOL_TCP(NameError)Didyoume

  3. ruby - 你如何使用 Ruby 找到空闲的 TCP 服务器端口? - 2

    我正在尝试创建一个使用一次的HTTP服务器来处理单个回调,并且需要帮助在Ruby中找到一个空闲的TCP端口。这是我正在做的事情的框架:require'socket't=STDIN.readport=8081whiles=TCPServer.new('127.0.0.1',port).acceptputss.getss.print"HTTP/1.1200/OK\rContent-type:text/plain\r\n\r\n"+ts.closeexitend(它回显标准输入到第一个连接然后死掉。)如何自动找到空闲端口进行监听?这似乎是在远程服务器上启Action业然后使用唯一作业ID回调

  4. ruby-on-rails - 服务器是否在主机 "localhost"(::1) 上运行并在端口 5432 上接受 TCP/IP 连接? - 2

    首先请注意,我在StackOverflow和网络上的文章中发现了几个类似的问题,但没有一个能帮助我解决我的问题:PGErrorcouldnotconnecttoserver:ConnectionrefusedIstheserverrunningonport5432?PG::ConnectionBad-couldnotconnecttoserver:Connectionrefusedpsql:couldnotconnecttoserver:Connectionrefused问题来了:我有一个非常棒的Rails应用程序。我和我的合作者使用GitHub一起工作。我们有一个master和一个m

  5. 【操作系统】十分钟了解关于TCP/IP网络的基础知识(二)ARP、路由器、DHCP、DNS以及TCP/IP - 2

    承接上篇文章(十分钟了解关于TCP/IP网络的基础知识)五.ARP(地址解析协议)        虽说使用IP地址确实方便了我们使用者记忆以及整理归类、寻找信息的发送目的地,但是最终接收数据的地方,还是MAC地址,于是乎,为了实现有IP地址到MAC地址的转换,引入了名为ARP(AddressResolutionProtocol)又称之为地址解析协议。      ARP通过广播(Broadcast,这是个专业名词,后面还会继续提起)的方式对LAN中所有的计算机提问:“哎,谁IP地址是10.165.7.116(上篇文章中的例子)呀?你MAC地址多少啊,快过来登记一下!”,如果有哪台计算机回复了MA

  6. ruby - TCP 服务器错误 : Address already in use - bind(2) - 2

    几周前Jekyll对我来说工作正常,但现在突然出现以下错误:TCPServerError:Addressalreadyinuse-bind(2)INFOWEBrick::HTTPServer#start:pid=7300port=4000%lsof-i:4000即使端口上没有任何运行。以下是详细信息:%jekyll--versionJekyll0.11.2%wherejekyll/home/bhaarat/.rvm/gems/ruby-1.9.2-p290/bin/jekyll/usr/bin/jekyll%ruby--versionruby1.9.2p290(2011-07-09re

  7. javascript - 如何围绕标题的前半部分包装一个类? - 2

    我正在尝试围绕标题的前半部分或后半部分包装一个类,以便我可以使用jQuery创建更加动态和炫酷的标题。理论上我想找到句子中的所有空格并将其分成两部分。如果标题包含奇数个单词,脚本应该检测到这一点并将该类添加到最近的单词。 最佳答案 这是一个有趣的问题。我会使用方便的javascriptsplice方法。Splice可用于插入和删除数组的项目。我建议打开一个检查器并尝试我在下面写的一些例子。首先,我们将使用jQuery选择header,然后操作html内容字符串。我假设您要操作的特定标题将有一个类,并且我已经替换为“动态”:varhe

  8. javascript - 有没有办法用 javascript 建立到 IP 的 tcp 连接? - 2

    让我介绍一下我正在努力完成的事情的背景。我有一个具有本地IP地址的设备(芯片和pin终端),它已被编程为接收特定数据并处理它。示例:我发送十六进制格式的字符串"05""3035",终端读取它并重新启动。我试过使用SockJS-Client以及内置的WebSockets.但是使用Websockets我注意到浏览器正在发送:GET/HTTP/1.1Host:IP:PORTConnection:UpgradePragma:no-cacheCache-Control:no-cacheUpgrade:websocketOrigin:MYIPSec-WebSocket-Version:13User

  9. SpringBoot+Netty实现TCP客户端实现接收数据按照16进制解析并存储到Mysql以及Netty断线重连检测与自动重连 - 2

    场景在SpringBoot项目中需要对接三方系统,对接协议是TCP,需实现一个TCP客户端接收服务端发送的数据并按照16进制进行解析数据,然后对数据进行过滤,将指定类型的数据通过mybatis存储进mysql数据库中。并且当tcp服务端断连时,tcp客户端能定时检测并发起重连。全流程效果 注:博客:霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主实现1、SpringBoot+Netty实现TCP客户端本篇参考如下博客,在如下博客基础上进行修改Springboot+Netty搭建基于TCP协议的客户端(二):https://www.cnblogs.com/haolb

  10. TCP 中 Flags 标志位 ACK、SYN 与 seq、ack - 2

    单独存在时ACK(Acknowledgment):向对方确认它已成功接收。当ACK=1时,确认号字段才有效。SYN(Synchronization):用于发起和建立连接,连接建立后无用。FIN(Finish):当FIN=1时,表明数据已经发送完毕,要求释放连接。seq(SequenceNumber):占4字节。首先,在传输过程的每一个字节都会有一个编号。在建立连接后,序号代表:这一次传给对方的TCP数据部分的第一个字节的编号。ack(AcknowledgmentNumber):占4字节。在建立连接后,确认号代表:期望对方下一次传过来的TCP数据部分的第一个字节的编号。组合时SYN=1、ACK=

随机推荐