ARM-驱动-10自定义通信协议

一、为什么需要自定义协议

TCP 是流式传输 ,没有"消息边界"的概念。发送方发了3个包,接收方可能一次收到1个,也可能一次收到3个拼在一起,这就是经典的粘包问题

复制代码
发送方发送:
  [帧1: 7字节] [帧2: 7字节] [帧3: 8字节]

接收方可能收到:
  情况A:[帧1+帧2+帧3 共22字节]   ← 粘包
  情况B:[帧1前3字节] + [帧1后4字节+帧2]  ← 半包
  情况C:正好每次一帧              ← 理想情况,实际很少

解决方案:设计自定义协议,用帧头 + 长度 + 帧尾标记每帧的边界,接收方按协议解析,无论怎么粘包都能正确还原。


二、自定义协议格式

2.1 帧结构

复制代码
┌────────┬────────┬──────────────────────┬────────┬────────┐
│ 帧头   │ 长度   │ 数据(n字节)         │ 校验   │ 帧尾   │
│ 0xAA   │ 1字节  │ 标识(1) + 值(n-1)    │ 1字节  │ 0xBB   │
└────────┴────────┴──────────────────────┴────────┴────────┘
  byte0    byte1       byte2 ~ byte2+n-1    byte2+n  byte2+n+1
字段 大小 说明
帧头 1字节 固定值 0xAA,标记一帧的开始
长度 1字节 仅包含数据字段的长度(不含帧头、长度本身、校验、帧尾)
数据 n字节 第1字节为标识,后续为值(温度/湿度为2字节 short)
校验 1字节 对数据字段所有字节求和,取低8位
帧尾 1字节 固定值 0xBB,标记一帧的结束

2.2 数据标识定义

标识值 含义 数据字段内容 数据字段总长度
0x01 温度帧 标识(1) + temp值(2) 3字节
0x02 湿度帧 标识(1) + humi值(2) 3字节
0x03 温湿度组合帧 标识(1) + temp(2) + humi(2) 5字节

2.3 数值编码规则

⚠️ 温度和湿度的实际值 = 原始数据 ÷ 10

例如:温度 22.2℃ 存储为 222(即 0x00DE),读取时 222 / 10 = 22(整数部分)


三、三种帧的完整字节示例

温度帧(标识 0x01)

复制代码
0xAA  0x03  0x01  0x16  0x00  0xXX  0xBB
│     │     │     │           │     │
帧头  长度=3 标识  低字节 高字节 校验  帧尾

数据字段 = [0x01, 0x16, 0x00](长度=3)
值 = 0x0016 = 22 → 22 / 10 = 2.2℃(实际表示 22,不是2.2,/10取整=2)

图片中示例:0xAA 0x02 0x01 22 22 0xBB
→ 长度字段=0x02(数据2字节:标识+值各1字节,此处为简化示意)

湿度帧(标识 0x02)

复制代码
0xAA  0x03  0x02  0x37  0x00  0xXX  0xBB
│     │     │     │           │     │
帧头  长度=3 标识  低字节 高字节 校验  帧尾

图片中示例:0xAA 0x02 0x02 55 55 0xBB
→ 湿度帧

温湿度组合帧(标识 0x03)

复制代码
0xAA  0x05  0x03  temp_L temp_H  humi_L humi_H  0xXX  0xBB
│     │     │     ├──────────┤   ├──────────┤   │     │
帧头  长度=5 标识   temp(2字节)    humi(2字节)   校验  帧尾

图片中示例:0xAA 0x03 0x03 22 55 77 0xBB
→ temp+humi 组合帧

帧尾位置计算公式

复制代码
帧尾地址 = buf[i + len + 3]

其中:
  i      = 帧头所在下标
  len    = buf[i+1](长度字段值)
  +1     = 帧头本身占1字节
  +1     = 长度字段本身占1字节
  +len   = 数据字段 len 字节
  +1     = 校验字段占1字节
  合计偏移 = 1 + 1 + len + 1 - 1 = len + 2(从 i 开始,偏移 len+2 即为帧尾)

代码中:buf[buf[i+1] + 3](i=0时)即 buf[len+3]

对应图片中的表达式:

c 复制代码
if( *p == 0xAA && *(p + (*(p+1) + 3)) == 0xBB)
//  帧头判断          帧尾判断(偏移量 = len + 3)

四、校验和计算

c 复制代码
// 计算校验和:对数据字段中所有值字节求和,取低8位
unsigned char checksum(const unsigned char *value_bytes, int len)
{
  unsigned char sum = 0;
  for (int i = 0; i < len; i++)
  {
    sum += value_bytes[i];
  }
  return sum;  //取低8位
}

关键点:

  • 校验范围是数据字段(从标识字节开始,含标识,不含帧头/长度/校验/帧尾)
  • unsigned char 自然溢出即取低8位,无需额外操作
  • 发送方打包时计算并填入,接收方解包时重新计算并比对

温度帧校验示例:

复制代码
数据字段 = [0x01, 0x16, 0x00]
校验 = (0x01 + 0x16 + 0x00) & 0xFF = 0x17

五、客户端(cli.c)代码详解

5.1 数据结构

c 复制代码
typedef struct data 
{
    float temp;
    float humi;
}data_t;

typedef struct package
{
    unsigned char  frame_head;   // 帧头 0xAA
    unsigned char  len;          // 数据长度
    unsigned short data;         // 数据值(2字节,温度或湿度)
    unsigned char  sum;          // 校验
    unsigned char  frame_tail;   // 帧尾 0xBB
}package_t;

typedef struct package_both
{
    unsigned char  frame_head;   // 帧头 0xAA
    unsigned char  len;          // 数据长度
    unsigned short temp;         // 温度值
    unsigned short humi;         // 湿度值
    unsigned char  sum;          // 校验
    unsigned char  frame_tail;   // 帧尾 0xBB
}package_both_t;

⚠️ 注意 :直接用结构体打包发送存在字节对齐风险,实际项目中应加 __attribute__((packed)) 禁止填充,或手动逐字节填充 unsigned char 数组(send_temp 函数的做法更安全)。

5.2 send_temp 函数(手动逐字节打包)

c 复制代码
void send_temp(int sockfd, float temp)
{
  unsigned short value = (unsigned short)(temp * 10);  // 四舍五入取整
  unsigned char frame[7];
  frame[0] = 0xAA;                        // 帧头
  frame[1] = 3;                           // 数据长度(标识1 + 值2 = 3字节)
  frame[2] = 0x01;                        // 标识:温度
  frame[3] = value & 0xff;                // 温度值低字节(小端序)
  frame[4] = value >> 8;                  // 温度值高字节
  frame[5] = calc_checksum(&frame[2], 3); // 校验(从标识字节开始,共3字节)
  frame[6] = 0xBB;                        // 帧尾
  send(sockfd, frame, sizeof(frame), 0);
  printf("发送温度帧: %.1f℃\n", temp);
}

字节布局(frame 数组):

复制代码
frame[0] = 0xAA          帧头
frame[1] = 0x03          长度(数据3字节)
frame[2] = 0x01          标识(温度)
frame[3] = value低字节   温度值小端低字节
frame[4] = value高字节   温度值小端高字节
frame[5] = checksum      校验(对frame[2..4]求和取低8位)
frame[6] = 0xBB          帧尾

为什么校验从 frame[2] 开始?

因为校验范围是数据字段,数据字段从 frame[2](标识字节)开始,长度为 frame[1](=3)。

5.3 send_data 函数(结构体打包,仅实现了 0x01)

c 复制代码
void send_data(int sockfd, data_t data, int flag)
{
    package_t pack_data;
    switch(flag)
    {
    case 0x01:
        pack_data.frame_head = 0xAA;
        pack_data.frame_tail = 0xBB;
        pack_data.data = data.temp * 10;
        //
        send(sockfd, &pack_data, sizeof(pack_data), 0);
        break;
    case 0x02:
        break;
    case 0x03:
        break;
        default:
            break;
    }
}

注意:case 0x02case 0x03 为空,为待实现内容(湿度帧和组合帧)。

5.4 get_temp 模拟函数

c 复制代码
float get_temp(void)
{
    return rand() % 1000 * 10.0;
}

5.5 main 函数发送流程

c 复制代码
while (1)
{
    send_temp(sockfd, temp_val);      // 发送温度帧
    usleep(30 * 1000);                // 间隔 30ms

    // send_humi 等已注释掉,待实现
    usleep(50 * 1000);
    usleep(50 * 1000);
    usleep(30 * 1000);
    usleep(30 * 1000);

    temp_val += get_temp();
    humi_val += 0.2;
    if (temp_val > 25.0) temp_val = 20.0;
    if (humi_val > 70.0) humi_val = 50.0;
}

图片中描述的完整发送序列(待实现):

复制代码
temp(3s) → humi(5s) → temp → temp → humi → temp → temp+humi 组合帧

六、服务器端(ser.c)代码详解

6.1 数据结构

c 复制代码
typedef struct 
{
    short temp;
    short humi;
}data_t;

typedef union 
{
    short    temp;
    short    humi;
    data_t   data;
}package_t;

package_t deal_data;

为什么用 union?

复制代码
union 中所有成员共用同一块内存:
  - 收到 0x01 温度帧 → 用 deal_data.temp 读取2字节
  - 收到 0x02 湿度帧 → 用 deal_data.humi 读取2字节
  - 收到 0x03 组合帧 → 用 deal_data.data.temp + deal_data.data.humi 读取4字节
节省内存,不需要对每种帧单独定义变量。

6.2 parse_package 函数

c 复制代码
void parse_package(unsigned char * data, int len)
{
    printf("**********\t");
    for(int j = 0; j < len; j++)
        printf("0x%02x\t", data[j]);
    printf("**********\n");
        
    memcpy(&deal_data, &data[1], len - 1);   // 跳过标识字节,拷贝值字节
    switch(data[0])                           // data[0] 是标识字节
    {
    case 0x01:    // 温度帧
        printf("temp = %d\n", deal_data.temp / 10);
        break;
    case 0x02:    // 湿度帧
        printf("humi = %d\n", deal_data.humi / 10);
        break;
    case 0x03:    // 组合帧
        printf("temp = %d  humi = %d\n", 
               deal_data.data.temp / 10, 
               deal_data.data.humi / 10);
        break;
    default:
        break;
    }
}

parse_package 接收的参数说明:

复制代码
调用方式:parse_package(&buf[i + 2], buf[i + 1])

  &buf[i + 2] = 数据字段起始地址(跳过帧头和长度字段)
  buf[i + 1]  = 数据字段长度(即长度字段的值)

函数内部:
  data[0]          = 标识字节(0x01 / 0x02 / 0x03)
  &data[1]         = 值字节起始
  len - 1          = 值字节的字节数
  memcpy 目标      = deal_data(union,按需用不同成员访问)

6.3 核心解析循环(拆包逻辑,重点!)

c 复制代码
unsigned char buf[1024];
int len_data = 0;
int i = 0;
while(1)
{
    usleep((rand() % 200) * 1000);
    i = 0;
    int len_r = recv(conn_fd, &buf[len_data], sizeof(buf) - len_data, 0);
    if(0 == len_r)
        break;
    len_data += len_r;

    // 打印当前缓冲区所有字节(调试用)
    for(int j = 0; j < len_data; j++)
        printf("0x%02x\t", buf[j]);
    printf("\n");

    while(len_data - i >= 7)   // 最短帧长7字节才尝试解析
    {
        if(!(buf[i] == 0xAA && buf[buf[i+1] + 3] == 0xBB))  // 帧头帧尾验证
            i++;
        else
        {
            if(!(buf[i + buf[i + 1] + 2] == checksum(&buf[i + 2], buf[i + 1])))  // 校验和验证
                i++;
            else
            {
                parse_package(&buf[i + 2], buf[i + 1]);     // 解析数据字段
                int len_rm = i + buf[i + 1] + 4;            // 计算本帧总长度
                memcpy(buf, &buf[i + buf[i + 1] + 4], len_data - len_rm);  // 移除已解析的帧
                len_data -= len_rm;
                i = 0;
            }
        }

        if(i > 8)   // 连续8字节找不到合法帧头,丢弃脏数据
        {
            int len_rm = i + 1;
            memcpy(buf, &buf[i + 1], len_data - len_rm);
            len_data -= len_rm;
            i = 0;
        }
    }
}

七、拆包算法完整流程图

复制代码
recv 接收数据,追加到 buf 尾部
        ↓
缓冲区剩余字节 >= 7 ?
        ↓ 是
buf[i] == 0xAA ?  且  buf[i + len + 3] == 0xBB ?
        ↓ 否                      ↓ 是
       i++              校验和匹配?
                        ↓ 否         ↓ 是
                       i++        parse_package 解析
                                       ↓
                               从 buf 中删除该帧
                               len_data -= 帧总长
                               i = 0,继续循环
        ↓
    i > 8 ?
        ↓ 是
  丢弃 buf[0..i],i=0

关键下标计算(以 i 为帧头位置)

复制代码
buf[i]              帧头 0xAA
buf[i+1]            长度字段(设为 L)
buf[i+2]            数据字段起始(标识字节)
buf[i+2 .. i+2+L-1] 数据字段(共L字节)
buf[i+2+L]          校验字段
buf[i+2+L+1]        帧尾 0xBB = buf[i + L + 3]

帧总长度 = 1(帧头) + 1(长度) + L(数据) + 1(校验) + 1(帧尾) = L + 4
删除后:len_data -= (i + L + 4)

帧尾位置验证代码:

c 复制代码
buf[buf[i+1] + 3]   // 当 i=0 时 等价于 buf[L+3]

展开:buf[i + buf[i+1] + 3],即从帧头 i 出发,偏移 L+3,正好是帧尾位置。

校验字段位置验证代码:

c 复制代码
buf[i + buf[i + 1] + 2]   // 偏移 L+2,正好是校验字节
checksum(&buf[i + 2], buf[i + 1])  // 从数据字段起始,算 L 个字节

八、服务器解析示例(逐字节追踪)

以接收温度帧为例,假设 buf 中数据为:

复制代码
buf = [0xAA, 0x03, 0x01, 0xDE, 0x00, 0xDF, 0xBB]
下标   [0]    [1]   [2]   [3]   [4]   [5]   [6]

解析步骤:

复制代码
1. len_data = 7,i = 0
2. buf[0] == 0xAA ✓
3. buf[i+1] = 0x03(L=3)
4. 帧尾检查:buf[0 + 3 + 3] = buf[6] = 0xBB ✓
5. 校验检查:
     checksum(&buf[2], 3) = buf[2]+buf[3]+buf[4] = 0x01+0xDE+0x00 = 0xDF
     buf[0 + 3 + 2] = buf[5] = 0xDF ✓
6. 调用 parse_package(&buf[2], 3)
     data[0] = 0x01 → case 0x01
     memcpy(&deal_data, &data[1], 2)
     → deal_data.temp = 0x00DE = 222
     → printf("temp = %d\n", 222 / 10) → 输出 temp = 22
7. len_rm = 0 + 3 + 4 = 7
8. 删除已解析帧:len_data = 7 - 7 = 0

九、网络通信基础部分

9.1 服务器 socket 初始化流程

c 复制代码
// 1. 创建 TCP socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定地址和端口
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family      = AF_INET;
ser.sin_port        = htons(50000);       // 监听端口 50000
ser.sin_addr.s_addr = INADDR_ANY;         // 监听所有网卡
bind(listfd, (SA)&ser, sizeof(ser));

// 3. 监听(最大等待队列3)
listen(listfd, 3);

// 4. 接受连接
int conn_fd = accept(listfd, (SA)&cli, &len);

9.2 客户端 socket 连接流程

c 复制代码
// 1. 创建 TCP socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 设置服务器地址
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family      = AF_INET;
ser.sin_port        = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");  // 连接本机(测试用)

// 3. 连接服务器
connect(sockfd, (SA)&ser, sizeof(ser));

9.3 typedef struct sockaddr *(SA) 用法说明

c 复制代码
typedef struct sockaddr *(SA);
// 将 (SA) 定义为强制类型转换的简写
// 等价于 (struct sockaddr *)
// bind / connect / accept 第2个参数要求 struct sockaddr *
// 而实际用的是 struct sockaddr_in *,需要强转
bind(listfd, (SA)&ser, sizeof(ser));
// 等价于
bind(listfd, (struct sockaddr *)&ser, sizeof(ser));

十、协议设计总结与待完善部分

10.1 已实现功能

功能 客户端 服务器
TCP 连接建立
温度帧发送(0x01) ✅ send_temp() ✅ parse_package()
湿度帧发送(0x02) ❌ 已注释 ✅ 解析框架已有
组合帧发送(0x03) ❌ 已注释 ✅ 解析框架已有
粘包处理 --- ✅ 滑动窗口解析
脏数据丢弃 --- ✅ i>8 时丢弃
校验和验证

10.2 待完善部分(客户端)

c 复制代码
// 1. 实现 send_humi(湿度帧,标识 0x02)
void send_humi(int sockfd, float humi)
{
    // 参考 send_temp,把 frame[2] 改为 0x02,其余相同结构
}

// 2. 实现 send_both(组合帧,标识 0x03)
// frame 需要 8 字节:帧头+长度+标识+temp低+temp高+humi低+humi高+校验+帧尾 = 9字节
// frame[1] = 5(数据长度:标识1+temp2+humi2)

// 3. send_data 函数补全 case 0x02 和 case 0x03

10.3 协议关键设计点回顾

复制代码
① 帧头 0xAA + 帧尾 0xBB:快速定位帧边界
② 长度字段:精确确定数据区域,解决变长帧问题
③ 校验和:检测数据传输错误
④ 帧尾二次确认:长度字段被污染时的保护
⑤ 数值 ×10 存储:用整型传输浮点,避免浮点精度问题
⑥ 小端序:值字节低字节在前(frame[3]=低字节,frame[4]=高字节)

十一、关键概念速查

概念 说明
粘包 TCP 流式传输,多帧数据可能合并接收
帧头/帧尾 自定义协议中标记帧边界的固定字节
长度字段 指示数据区长度,服务器据此确定帧结束位置
校验和 数据字段所有字节之和取低8位,用于验证数据完整性
memcpy 移除帧 将缓冲区中已解析部分删除,剩余数据前移
htons 主机字节序转网络字节序(大端),用于端口号
inet_addr 将点分十进制IP字符串转换为网络字节序整数
INADDR_ANY 绑定所有网卡接口
usleep 微秒级睡眠,1000微秒=1毫秒
小端序 低字节存低地址(x86/ARM默认),与网络大端序相反
相关推荐
j_xxx404_2 小时前
Linux:缓冲区
linux·运维·c++·后端
亚空间仓鼠2 小时前
Ansible之Playbook(六):实例部署实战
linux·网络·ansible
犽戾武2 小时前
VR遥操作机械臂系统:核心算法与数学方法全解析
linux·人工智能
MIXLLRED2 小时前
随笔——ROS Ubuntu版本变化详解
linux·ubuntu·机器人·ros
爱学习的小囧2 小时前
ESXi CPU 使用率高怎么排查?esxtop 一键定位占用高的虚拟机与进程
java·linux·运维·服务器·网络·虚拟化
Fanfanaas2 小时前
Linux 进程篇 (四)
linux·运维·服务器·开发语言·c++·学习
发发就是发2 小时前
触摸屏驱动调试手记:从I2C鬼点到坐标漂移的实战录
linux·服务器·驱动开发·单片机·嵌入式硬件
Jacob程序员2 小时前
Linux 下启动达梦数据库 Manager 图形化客户端
linux·运维·服务器