知识巩固源码落实之2:tcp服务端接收处理半包和粘包

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 知识巩固源码落实之2:tcp服务端接收处理半包和粘包

1:背景介绍

1.1:在处理tcp连接接收数据时,要考虑recv时(读取数据时),数据的半包,粘包问题

===》tcp是可靠的流式传输,意味着对于每个连接,tcp可以按顺序,可靠的接收到对端消息

===》理解:对于每个连接(fd对应五元组),tcp协议栈底层维持了一个发送缓冲区和接收缓冲区

=====》对于一个连接,对应的自己的接收缓冲区,一系列的数据,按顺序塞入在了缓冲区中,recv只是从中取数据。

=====》对于recv取接收缓冲区数据,需要一定策略(1:可能一次取到多个包(粘包) 2:可能recv参数设置不够,取了半个包(半包))

1.2:处理半包,粘包问题

半包粘包问题考虑有两点:

1:recv取数据时,要注意策略

2:需要用户层发送数据时,定义一定的协议。

===》方案1:发送数据时,数据构造特定的头/尾,接收后暂存在缓冲区中按逻辑处理(这里使用一块内存模拟了缓冲区)

===》方案2:发送数据时,特定字节标识发送的数据长度+实际data

2:测试代码

按照自己理解的处理半包和粘包的逻辑,两种处理方案分别使用测试代码进行模拟:

/************************************************
info: 作为tcp的服务端,数据的粘包,半包问题,期望对其进行处理
data: 2022/02/10
author: hlp
************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//tcp是可靠的流式传输 我们能保证它可靠,顺序的接收到,这里是放在tcp的接收缓冲区中
//但是放入缓冲区中,我们取数据的方案,需要做控制,以识别特定的不同的包。(有的包很小,有的包很大,取数据时要注意)
//汇总:我们从缓存中取数据,要关注数据的完整,一次是否能取到完整的包。
int exec_one_data(char* data, int len);
void check_buff(char * ringbuff, int *ops, int buff_size);//第三个参数为了在处理成功后清空用
void recv_data_by_specific_tail_symbol(int fd);
void recv_data_by_length_and_data(int fd);
int main()
{
  //要解决识别数据的完整性问题 我们需要适配用户层协议  特定字节/特定终止符/长度+data
  int fd = 0;
  //特定的终止符+缓冲区处理   这里我是项目被要求使用这种复杂的头和尾标识,就这样演示 其实只要尾部也可以保证的
  recv_data_by_specific_tail_symbol(fd);
  //按照长度+data的方案也是一种可靠方案,   先接收特定字节的长度,再接收数据。
  recv_data_by_length_and_data(fd);
  return 0;
}
//发送时 特定的尾部标识+缓冲区方案
//每次不知道取多少数据,以及是否取到完整数据,一定需要缓冲区
//作为服务端  我们是有多个客户端fd连接的   最佳方案其实是每个fd连接应该有自己的缓冲区
//    假设我们的数据都能一次发送(业务不会有拆包现象),那么直接一个缓冲区做粘包的解析处理即可 (有拆包的话就会有问题的)
void recv_data_by_specific_tail_symbol(int fd)
{
  //缓冲区可以使用ringbuffer 这里demo只是演示,用了一块内存,并且所有连接公用一个(假设没有拆包,只是验证思路)
//client 发送 假设构造发送数据  依次再客户端进行发送了 业务不涉及多包
  const char * send1_data = "FFFF0D0A<header>my test of send 1. \\<tail>0D0AFEFE";
  const char * send2_data = "FFFF0D0A<header>my test of send 2. \\<tail>0D0AFEFE";
  const char * send3_data = "FFFF0D0A<header>my test of send 3. \\<tail>0D0AFEFE";
  //tcp是可靠的 流式传输,必然按顺序,完整的收到一个包,接收放入缓冲区后,依次处理就好
//server 接收 由于我recv时不知道接收的长度,可能每次接收特定len(可能小于一个单包,可能刚好截断缓冲区某个包)
  //所以  放在缓冲区中,判断缓冲区中“FFFF0D0A<header><tail>0D0AFEFE”头和尾的标识进行处理,我是每次接收后判断一次,可以定时器等其他方案
  //len = recv(fd, data, 44, 0);  memcpy(ringbuff +ops, data, len); check_buff(ringbuff, ops);
  //每个fd使用一个缓冲区是最佳方案,这里用一块内存进行简单测试处理
  char * ringbuff = (char *) malloc(1024); //假设缓冲区大小定义为1024
  memset(ringbuff, 1024, 0);
  int ops = 0;  
  //假设接收到数据 先放入缓冲区中 这里假设客户端发送的数据都符号标准,当然要做安全防护
  //假设我取数据两次取到 "FFFF0D0A<header>my test of send 1. \\<tail>0D0AFEFEFFFF0D0A<header>my test"
  //           " of send 2. \\<tail>0D0AFEFEFFFF0D0A<header>my test of send 3. \\<tail>0D0AFEFE"
  //len = recv(fd, recv1_data, my_len,0); my_len是我提前定义的recv1_data大小,这种情况应该是my_len == len
  //第一次recv提取
  const char* recv1_data = "FFFF0D0A<header>my test of send 1. \\<tail>0D0AFEFEFFFF0D0A<header>my test";
  memcpy(ringbuff+ops, recv1_data, strlen(recv1_data));
  ops += strlen(recv1_data);
  //消费缓冲区位置   修改ops
  check_buff(ringbuff, &ops, 1024); //校验缓冲区中是否有完整数据  有则处理 识别第一个FFFF0D0A<header> 到下一个<tail>0D0AFEFE
  //第二次recv提取 
  const char* recv2_data =" of send 2. \\<tail>0D0AFEFEFFFF0D0A<header>my test of send 3. \\<tail>0D0AFEFE";
  memcpy(ringbuff+ops, recv2_data, strlen(recv2_data));
  ops += strlen(recv2_data);
  check_buff(ringbuff, &ops, 1024); //这里是正常数据 应该已经全部处理了
  printf("ringbuff length is [%d] \n", ops);
  memset(ringbuff, 1024, 0); //每次处理完要清空 很有必要  不然下次处理也会有问题
  if(ringbuff)
  {
    free(ringbuff);
    ringbuff = NULL;
  }
}
//这个函数其实是recv后,放入ringbuff后的主要解析逻辑
//这里的处理与recv的逻辑也有关   尽量一次recv循环取完,则每次数据都是能完整处理 (否则其实会有半包的现象)
void check_buff(char * ringbuff, int *buffops, int buff_size)
{
  //循环一次取完 应该就不会有这种问题 但是半包问题肯定有
  if(*buffops <= strlen("FFFF0D0A<header><tail>0D0AFEFE"))
  {
    return;
  }
  printf ("check buff tail is [%s] \n", ringbuff+(*buffops)-strlen("<tail>0D0AFEFE"));
  //对比终结符相同再处理 否则留给下一次
  if(strcmp("<tail>0D0AFEFE", ringbuff+(*buffops)-strlen("<tail>0D0AFEFE")) !=0)
  {
    return;
  }
  //对接收到的数据做拆包处理
  int datalen = -1;
  char * onedata;
  char * ops;
  char * temp_data = ringbuff;
  //先判断是否有结尾的包 再判断头进行处理
  const char * end_str = "<tail>0D0AFEFE";
  //每次取一个尾部  然后处理一个包
  while((ops = strstr(temp_data, end_str)) != NULL)
  {
    datalen = ops - temp_data +strlen(end_str);
    exec_one_data(temp_data, datalen);
    temp_data = ops+strlen(end_str);
  }
  //有剩下的数据 这是不可能的  因为recv是循环取完放在缓冲区中的
  if(temp_data - ringbuff != *buffops)
  {
    printf("there is loss data: [%ld][%s] \n", strlen(temp_data), temp_data);
  }
  //清空处理
  memset(ringbuff, 1024, 0);
  buffops = 0;
}
//这是一个完整的发送数据包   FFFF0D0A<header> XXX <tail>0D0AFEFE
int exec_one_data(char* data, int len)
{
  const char * start_str = "FFFF0D0A<header>";
  char * ops;
  ops = strstr(data, start_str);
  if(ops == data) //如果中间包含header,解析有误,但是应该是不可能的
  {
    int out_len = len-strlen("FFFF0D0A<header><tail>0D0AFEFE");
    char * out_data = NULL;
    out_data =(char*)malloc(out_len +1);
    memset(out_data, out_len+1, 0);
    memcpy(out_data, data + strlen("FFFF0D0A<header>"), out_len);
    printf("out_data is [%lu][%s] \n", strlen(out_data), out_data);
    if(out_data != NULL)
    {
      free(out_data);
      out_data = NULL;
    }
  } 
  if(ops == NULL) //没有找到头  丢弃
  {
    printf("package data is error, not find start data. \n");
  }
  if(ops != data) //头前面有异常数据
  {
    printf("recv package data is error.");
  }
  return 0;
}
//发送时 特定的字节存储数据长度+实际data   
//个人理解  这种按照特定的结构取数据 不需要缓冲区是可以保证的
void recv_data_by_length_and_data(int fd)
{
//构造发送的数据
  const char * send_data_str = "my test of send data \\";
  unsigned short len = strlen(send_data_str);
  printf("vps short is : %lu \n", sizeof(unsigned short)); //vps short is : 2 
  //用2个字节的特定长度 +data的结构进行数据的构造
  //这里要转网络序 取出后再转回来
  char * send_data = NULL;
  send_data = (char*)malloc(2 +len +1);//预留了一个终结符
  memset(send_data,  2+len, 0);
  memcpy(send_data, &len, 2);
  memcpy(send_data+2, send_data_str, len);
  //这里应该按照十六进制打印特定的长度  last send data is [22][my test of send data \]  
  printf("last send data is [%u][%s]  \n", *((unsigned short *)send_data), send_data+2);
//接收端处理 应该先接收两字节长度 再接收后面数据长度
  //这里发送的其实就是send_data  接收端先接收两个字节的长度,再接收特定长度的数据 (这里直接做解析)
  unsigned short recv_data_len;
  memcpy(&recv_data_len, send_data, 2);
  printf("recv data is[%d: %s] \n", recv_data_len, send_data+2); //recv data is[22: my test of send data \] 
//注意发送端数据的free
  if(send_data != NULL)
  {
    free(send_data);
    send_data = NULL;
  }
}

3:测试结果:

只是模拟接收的逻辑,实际的接收可以根据tcp逻辑进行参考实现。

hlp@ubuntu:~/220107$ ./tag 
check buff tail is [header>my test] 
check buff tail is [<tail>0D0AFEFE] 
out_data is [20][my test of send 1. \] 
out_data is [20][my test of send 2. \] 
out_data is [20][my test of send 3. \] 
ringbuff length is [150] 
vps short is : 2 
last send data is [22][my test of send data \]  
recv data is[22: my test of send data \]

我开始试着积累一些常用代码:自己代码库中备用

我的知识储备更多来自这里,推荐你了解:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

目录
相关文章
|
6月前
|
存储 Python
Python网络编程基础(Socket编程) UDP 发送和接收数据
【4月更文挑战第10天】对于UDP客户端而言,发送数据是一个相对简单的过程。首先,你需要构建一个要发送的数据报,这通常是一个字节串(bytes)。然后,你可以调用socket对象的`sendto`方法,将数据报发送到指定的服务器地址和端口。
|
6月前
|
缓存 移动开发 网络协议
tcp业务层粘包和半包理解及处理
tcp业务层粘包和半包理解及处理
105 1
|
Python
143 python网络编程 - UDP发送、接收数据
143 python网络编程 - UDP发送、接收数据
75 0
|
6月前
|
编解码 缓存 移动开发
TCP粘包/拆包与Netty解决方案
TCP粘包/拆包与Netty解决方案
105 0
|
1月前
|
存储 XML JSON
【TCP】核心机制:延时应答、捎带应答和面向字节流
【TCP】核心机制:延时应答、捎带应答和面向字节流
51 2
|
缓存 网络协议 算法
TCP粘包、拆包原因与解决方案
TCP粘包、拆包原因与解决方案
236 0
|
存储 缓存 网络协议
Socket 如何处理粘包
Socket 如何处理粘包
81 0
|
网络协议 算法
第 9 章 TCP 粘包和拆包及解决方案
第 9 章 TCP 粘包和拆包及解决方案
221 0
|
存储 网络协议
TCP拆包和粘包的作用是什么
首先我们思考一个问题,应用层的传输一个10M的文件是一次性传输完成,而对于传输层的协议来说,为什么不是一次性传输完成呢。 这个有很多原因,比如稳定性,一次发送的数据越多,出错的概率越大。再比如说为了效率,网络中有时候存在并行的路径,拆分数据包就就能更好的利用这些并行的路径。再有,比如发送和接收数据的时候,都存在缓冲区,缓冲区是在内存中开辟的一块空间,目的是缓冲大量的应用频繁的通过网卡收发数据,这个时候,网卡只能一个一个处理应用的请求。当网卡忙不过来的时候,数据就需要排队了。也就是将数据放入缓冲区。如果每个应用都随意发送很大的数据,可能导致其他应用的实时性遭到破坏。
75 0
|
网络协议 图形学
Socket TCP协议解决粘包、半包问题的三种解决方案
Socket TCP协议解决粘包、半包问题的三种解决方案
319 2
Socket TCP协议解决粘包、半包问题的三种解决方案