GNU/Linux下粘包问题的简单处理

粘包问题的准确描述

  • 本篇博客主要描述的是发生在客户端与服务端基于TCP的套接字通信时产生的粘包问题
    • 通信中数据的传输基于流,发送端与接收端对数据的处理数量与频率可以是不对等的,可以根据自身需求做决策
    • 也就是说在实际的传输中包长是不确定的,若想取得指定长度的包就需要程序员自己努力
  • TCP是 面向连接的,安全的,流式传输层协议, 粘包问题的发生是由于程序的设计存在特殊的需求(比如每次处理指定数量的数据),才需要将包分开,才会存在粘包问题
  • 所以粘包问题并不是TCP的设计存在缺陷,解决TCP粘包问题其实是解决设计应用层协议
  • 将粘包问题怪罪于TCP协议是对双方的侮辱

如何解决粘包问题

  • 解决粘包问题就是在服务端处理好客户端发来的不定长度数据包

解决方案:

  • 使用标准的应用层协议(http / https)封装要传输的不定长的数据包

  • 在每条数据的尾部添加特殊字符,遇到特殊字符则代表数据接收完毕

    • 需逐字节处理,效率低
  • 在发送数据块之前在数据块最前面添加一个固定大小的数据头,新数据包数据头+数据块组成

    • 数据头:存储数据块的总字节数,接收端先接受数据头,然后再根据数据头接受相应大小的字节
    • 数据块:原始内容
    • HTTP 中的Content-Length就起了类似的作用
  • 数据块可能包含多种格式内容,此时可以用JSON序列化内容,我后面大概率会发一篇关于cJson的博客(挖坑)

新增数据头

发送端解决方案

  1. 根据待发送的数据长度N动态申请一块固定空间,空间大小为:N+4(4是包头占用的字节数)
  2. 将待发送数据长度写入前四个字节,将其转换为网络字节序(大端)
  3. 将待发送的数据拷贝到包头后面的空间中,将数据包完整发出(字符串不需要考虑字节序问题,但要考虑部分I/O问题)
  4. 释放动态申请到的堆内存

SHOW ME THE CODE

  • 发送端需实现一个解决部分I/O的函数与一个加包头的函数

  • 发送指定字节数的函数

/*
    writen() 的参数与write()相同
    循环使用了write()系统调用
    确保请求的字节数总是能够得到全部传输
    ssize_t writen(int fd, void *buffer, size_t count);
    return number of bytes written, -1 on failure
*/

#include <unistd.h>
#include <sys/socket.h>

ssize_t writen(int fd, const char *msg, size_t size)
{   
    // 只读,所以const
    const char *buf = msg;
    int count = size;
    // 循环处理, 直至全部发送
    while(count > 0) {
        // 专用于套接字的 I/O 系统调用
        int len = send(fd, buf, count, 0);
        if(len == -1) {
            close(fd);
            return -1;
        }else if (len == 0){
            continue;
        }
        // offset
        buf += len;
        // 计算剩余待发送量
        count -= len;
    }

    return size;
}
  • 加包头函数
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>


int sendMsg(int cfd, char* msg, int len)
{
    // 错误处理
    if(cfd <= 0 || msg == NULL || len <= 0){
        return -1;
    }

    // 分配空间
    char *data = (char *)malloc(len+4);
    /* 
    字符串没有字节序问题
    数据头为整型,存在字节序问题
    需要将数据头转换网络字节序
    */
    int bigLen = htonl(len);

    // 写入数据
    memcpy(data, &bigLen, 4);
    memcpy(data+4, msg, len);

    // 发送数据
    int ret = writen(cfd, data, len+4);

    // 释放内存
    free(data);

    return ret;
}

接收端解决方案

  1. 接收数据头4字节数据,将其从网络字节序转换为主机字节序,得到待接收数据长度
  2. 根据得到的长度申请固定大小的堆内存,用来存储数据
  3. 接受固定数目的数据保存到申请到的内存之中
  4. 处理数据
  5. 释放堆内存

SHOW ME THE CODE

  • 接收指定字节函数
/*
    readn() 的参数与read()相同
    循环使用了read()系统调用
    确保请求的字节数总是能够得到全部传输
    ssize_t readn(int fd, void *buffer, size_t count);
    return number of bytes read, 0 on EOF, -1 on failure
*/

#include <unistd.h>
#include <sys/socket.h>

ssize_t readn(int fd, char *buf, size_t size) {
    char *p = buf;
    int count = size;
    while (count > 0) {
        int len = recv(fd, p, count, 0);
        if(len == -1) {
            return -1;
        }else if(len == 0) {
            return size-count;
        }
        p += len;
        count -= len;
    }
    return size;
}
  • 解包头函数
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int recvMsg(int cfd, char **msg) {
    // 首先接受报头
    int len = 0;
    readn(cfd, (char*)&len, 4);
    len = ntohl(len);
    printf("需接收的数据包大小: %d\n", len);

    // 根据大小分配内存
    char *buf = (char *)malloc(len + 1);
    int ret = readn(cfd, buf, len);
    if(ret != len) {
        close(cfd);
        free(buf);
        return -1;
    }
    buf[len] = '\0';

    *msg = buf;

    return ret;
}

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值