socket网络编程(五)——粘包拆包问题

摘要:
}而(0)intmain(){//创建套接字intm_sockfd=套接字(AF_INET;}//初始化套接字元素structsockaddr_inserver_addr;intserver_len=sizeof(server_addr);服务器_ len);等待用户发起请求intm_listenfd=listen(m_sockfd;

今天和大家讲一下socket网络编程中粘包和拆包的问题。

1、出现粘包拆包的原因

假设一个这样的场景,客户端要利用send()函数发送字符“asd”到服务端,连续发送3次,但是服务端休眠10秒之后再去缓冲池中接收。那么请问10秒之后服务端从缓冲区接收到的信息是“asd”还是“asdasdasd”呢?如果大家有去做实验的话,可以知道服务端收到的是“asdasdasd”,为什么会这样呢?按正常的话,服务端收到的应该是“asd”,剩下的两个asd要不就是收不到要不就是下次循环收到,怎么会一次性收到“asdasdasd”呢?如果要说罪魁祸首的话就是那个休眠10秒,导致数据粘包了!

服务端代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         
    do                      
    {                       
        perror(m);          
        exit(EXIT_FAILURE); 
    } while (0)
 
int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);
 
    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(39002);
 
    //绑定文件描述符和服务器的ip和端口号
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }
 
    //进入监听状态,等待用户发起请求
    int m_listenfd = listen(m_sockfd, 20);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }
 
    //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
    
  //这里休眠了10秒
  sleep(10);
 
    //接收客户端数据
    char buffer[BUF_SIZE];
    recv(m_connfd, buffer, sizeof(buffer)-1, 0);
    printf("server recv:%s
", buffer);
    strcat(buffer, "+ACK");
    send(m_connfd, buffer, strlen(buffer), 0);
 
    //关闭套接字
    close(m_connfd);
    close(m_sockfd);
 
    return 0;
}

客户端代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         
    do                      
    {                       
        perror(m);          
        exit(EXIT_FAILURE); 
    } while (0)
 
int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //服务器的ip为本地,端口号
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);
 
    //向服务器发送连接请求
    int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (m_connectfd < 0)
    {
        ERR_EXIT("connect server fail");
    }
    //发送并接收数据
    char buffer[BUF_SIZE] = "asd";
    int datasize = strlen(buffer);
    send(m_sockfd, buffer, datasize, 0);
    send(m_sockfd, buffer, datasize, 0);
    send(m_sockfd, buffer, datasize, 0);
    recv(m_sockfd, buffer, sizeof(buffer)-1, 0);
    printf("client recv:%s
", buffer);
 
    //断开连接
    close(m_sockfd);
 
    return 0;
}

以上代码在Linux平台上运行之后就会出现粘包现象,大家可以把以上代码复制去验证看看。

socket网络编程(五)——粘包拆包问题第1张

socket网络编程(五)——粘包拆包问题第2张

2、粘包拆包的几种情况

这个问题在socket网络编程中非常的常见,数据不仅会粘包,还会被拆包,就是一段数据被拆成两部分。那么拆包、粘包问题产生的原因都有哪些呢

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

而数据之所以会发送粘包拆包的根本原因是TCP的数据包是流的方式传输的,就像水流一样,没有一个分界的东西。

3、处理粘包拆包的方法

处理拆包、粘包问题的方法:

那么最关键的就是我们该怎么处理粘包拆包问题呢?因为这个问题在socket无法很好的处理,所以必须要在应用层上面处理,所以就需要要求大家在封装网络通信接口的时候要自己实现粘包拆包的处理方法。解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

  • 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
  • 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

第1种和第2种方法都会存在一些误差,没有办法很好处理好粘包拆包,所以一般的方法都是采用第3种。以下我先给出代码,然后再结合代码分析第3种粘包拆包的处理方式。

3.1、服务端代码

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#include "protocol.h"
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         
    do                      
    {                       
        perror(m);          
        exit(EXIT_FAILURE); 
    } while (0)
 
int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //初始化socket元素
    struct sockaddr_in server_addr;
    int server_len = sizeof(server_addr);
    memset(&server_addr, 0, server_len);
 
    server_addr.sin_family = AF_INET;
    //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(39002);
 
    //绑定文件描述符和服务器的ip和端口号
    int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
    if (m_bindfd < 0)
    {
        ERR_EXIT("bind ip and port fail");
    }
 
    //进入监听状态,等待用户发起请求
    int m_listenfd = listen(m_sockfd, 20);
    if (m_listenfd < 0)
    {
        ERR_EXIT("listen client fail");
    }
 
    //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
 
    //接收客户端数据
    char recv_buffer[10000]; //接收数据的buffer
    memset(recv_buffer, 0, sizeof(recv_buffer)); //初始化接收buffer
 
    while (1)
    {
        if (m_connfd < 0)
        {
            m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
            printf("client accept success again!!!
");
        }
 
        //休眠10秒才能有粘包现象出现
        sleep(10);
 
        int nrecvsize = 0;      //一次接收到的数据大小
        int sum_recvsize = 0; //总共收到的数据大小
        int packersize;          //数据包长度
 
        int disconn = false;
 
        //先从缓存池取出包头
        while (sum_recvsize != sizeof(NetPacketHeader))
        {
            nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, sizeof(NetPacketHeader) - sum_recvsize, 0);
            if (nrecvsize == 0)
            {
                close(m_connfd);
                m_connfd = -1;
                printf("client lose connection!!!
");
                disconn = true;
                break;
            }
            sum_recvsize += nrecvsize;
        }
 
        if (disconn)
        {
            continue;
        }
 
        NetPacketHeader *phead = (NetPacketHeader *)recv_buffer;
        packersize = phead->wDataSize;                     //客户端发过来的数据包长度(包含包头)
 
        //从缓冲池中取出数据(不包含包头)
        while (sum_recvsize != packersize)
        {
            nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, packersize - sum_recvsize, 0);
            if (nrecvsize == 0)
            {
                close(m_connfd);
                m_connfd = -1;
                printf("client lose connection!!!
");
                disconn = true;
                break;
            }
            else if (nrecvsize < 0)
            {
                ERR_EXIT("recv fail");
            }
            printf("server recv:%s, size:%d
", recv_buffer + sum_recvsize, nrecvsize);
 
            sum_recvsize += nrecvsize;
        }
        if (disconn)
        {
            continue;
        }
    }
 
    //关闭套接字
    close(m_connfd);
    close(m_sockfd);
 
    return 0;
}

3.2、客户端代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#include "protocol.h"
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         
    do                      
    {                       
        perror(m);          
        exit(EXIT_FAILURE); 
    } while (0)
 
 
int main()
{
    //创建套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //服务器的ip为本地,端口号
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);
 
    //向服务器发送连接请求
    if (connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        ERR_EXIT("connect server fail");
    }
 
    //发送并接收数据
    char data_buffer[BUF_SIZE] = "asd";
    int datasize = strlen(data_buffer);
 
    NetPacket send_packet;                                             //数据包
    send_packet.Header.wDataSize = datasize + sizeof(NetPacketHeader); //数据包大小
 
    memcpy(send_packet.Data, data_buffer, datasize); //数据拷贝
 
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
    send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0);
 
    //断开连接
    close(m_sockfd);
 
    return 0;
}

3.3、公用的部分

//protocol.h
 
#ifndef _PROTOCOL_H
#define _PROTOCOL_H
 
#define NET_PACKET_DATA_SIZE 5000
 
/// 网络数据包包头
struct NetPacketHeader
{
    unsigned short wDataSize; ///< 数据包大小,包含包头的长度和数据长度
};
 
/// 网络数据包
struct NetPacket
{
    NetPacketHeader Header;                   /// 包头
    unsigned char Data[NET_PACKET_DATA_SIZE]; /// 数据
};
 
 
#endif

首先定义一个新的文件protocol.h,主要是客户端和服务端共用的部分,包含数据包和包头的结构体定义。

然后客户端发送的时候记得发送数据体的长度是数据加包头的长度。

而在接收端的代码则稍微要花点心思了。首先接收端需要分两次来从缓冲池中接收数据,先取出长度为包头的数据,然后去取数据体的部分的时候一定要记得每次从缓冲区取数据的偏移量。

这样子就可以正确的处理好粘包拆包的问题了。当然从服务端向客户端发送数据的话,两者则是颠倒过来,这里就不在说明了。最后希望大家可以从这边文章获得一点收获,有什么疑问欢迎在下方评论说明。

更多精彩内容,请关注同名公众:一点月光(alittle-moon)

socket网络编程(五)——粘包拆包问题第3张

免责声明:文章转载自《socket网络编程(五)——粘包拆包问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SSH框架之-hibernate 三种状态的转换Hbase关于Java常用API举例下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

上下拉刷新

一.上拉刷新 1.为什么要做上拉刷新?      想要看一些旧的(更多)数据,就需要上拉刷新,加载更多数据   2.上拉刷新永远都显示在tableView最底部,用什么搭建?      tableFootView永远在tableView最底部,可以用它来搭建   3.上拉刷新业务逻辑      3.1当上拉刷新控件(footView)全部显示的时候,加载...

sqlserver 多行转一行

sql 例子: SELECT STUFF((SELECT ',' + CONVERT(VARCHAR, b.SCsinfoSourceId) FROM PZDataCsinfo b WHERE b.DId = a.PFId FOR XML PATH ('')), 1, 1, '') AS cids, *FROM PZFocusImg a WHERE a.P...

C# HMAC_SHA1加密

hmacsha1在很多签名计算中都很常用了,这里对两种可能返回的字符串类型做了分类 一种是直接返回字符串,一种是baset64后返回 需要看第三方对接文档中是否有特别说明,调试时如果报错,要比对串的内容看对方是否做了base64 #region HMACSHA1加密 将二进制数据直接转为字符串返回 /// <summary>...

Opentelemetry Collector的配置和使用

Collector的配置和使用 目录 Collector的配置和使用 Collector配置 Receivers Processors Exporters Service Extensions 使用环境变量 Collector的使用 部署到Kubernetes 部署Prometheus operator 使用Makefile 配置OpenT...

一起谈.NET技术,.NET中锁6大处理方法 悲观乐观自己掌握 狼人:

  本文介绍了处理.NET中锁的6种方法,首先我们讨论一下并发性问题,然后讨论处理乐观锁的3种方法,乐观锁不能从根源上解决并发问题,因此后面我们介绍了悲观锁,最后介绍隔离级别如何帮助我们实现悲观锁,每个隔离级别都列举了示例进行说明,使得概念更加清晰。   我们为什么需要锁?   在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这就会产生冲突,这...

SpringCloud 之 Netflix Hystrix 服务监控

本文较大篇幅引用https://www.mrhelloworld.com/hystrix-dashboard-turbine/,相关内容版权归该文章作者所有 引用上篇文章的工程数据   Actuator Hystrix 除了可以实现服务容错之外,还提供了近乎实时的监控功能,将服务执行结果和运行指标,请求数量成功数量等等这些状态通过 Actuator 进行收...