所有权、错误处理与 goto cleanup
概述
C没有析构函数和异常- 资源管理依靠接口约定与控制流完成
四种所有权关系
- 拥有
owned- 当前代码拥有这块内存,必须最终调用
free(),或者明确转交给其他对象
- 当前代码拥有这块内存,必须最终调用
|
1 |
void* buffer = malloc(1024); |
- 借用
borrowedprocess()通常只在调用期间借用data,不能释放它,也不能在调用结束后继续保存指针,除非接口另有说明const经常用于借用接口,但const本身并不表达生命周期
|
1 2 3 |
void process( const unsigned char* data, size_t length); |
- 转移
transferred- 如果
task_set_payload()成功接管所有权,原调用者必须停止访问和释放该资源
- 如果
|
1 2 |
task_set_payload(task, payload); payload = NULL; |
|
1 2 3 4 |
// 将本地指针设为 NULL 是 C 中常见的“手动移动”表达 task->payload = payload; payload = NULL; |
- 共享
shared- 多个对象共同引用同一资源时,需要额外机制:
- 明确谁最后释放
- 引用计数
- 父对象统一拥有
- 生命周期严格覆盖全部借用者
- 线程间同步
|
1 2 3 |
// 仅仅复制指针不等于共享所有权 void* second = first; // 它只是多了一个地址副本 |
接口必须说明失败时的所有权
|
1 2 3 |
bool queue_push( struct Queue* queue, struct Task* task); |
- 这个接口存在歧义
- 如果返回
false:- 队列是否已经释放
task? - 调用者是否仍拥有
task? - 任务是否可能已经部分入队
- 队列是否已经释放
|
1 2 3 4 5 6 7 8 9 |
// 推荐明确约定 /* * 成功:所有权转移给 queue。 * 失败:调用者仍然拥有 task。 */ bool queue_push_take( struct Queue* queue, struct Task* task); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Task* task = task_create(); if (task == NULL) { return false; } if (!queue_push_take(queue, task)) { task_destroy(task); return false; } /* 所有权已经转移 */ task = NULL; return true; |
资源变量先初始化为无效值
|
1 2 3 |
FILE* file = NULL; void* buffer = NULL; int socket = -1; |
- 这样所有失败路径都可以进入统一清理逻辑
- 注意
|
1 2 3 4 |
free(NULL); /* 安全 */ fclose(NULL); /* 未定义行为 */ close(-1); /* 返回错误,不是通用空操作 */ |
为什么C中 goto cleanup 是合理的
- 假设依次获得三种资源:
- 打开输入文件
- 打开输出文件
- 分配缓冲区
- 直接写多个提前返回
- 资源越多,每个失败分支重复的清理代码越多,也越容易漏掉
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
source = fopen(...); if (source == NULL) { return ERROR; } destination = fopen(...); if (destination == NULL) { fclose(source); return ERROR; } buffer = malloc(...); if (buffer == NULL) { fclose(destination); fclose(source); return ERROR; } |
- 统一清理模式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
result = ERROR; resource1 = acquire1(); if (resource1 == INVALID) { goto cleanup; } resource2 = acquire2(); if (resource2 == INVALID) { goto cleanup; } result = SUCCESS; cleanup: release(resource2); release(resource1); return result; |
- 这里的
goto:- 只向前跳
- 只有一个清理出口
- 不构造任意循环
- 不负责业务分支
文件复制示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
#include <stdio.h> #include <stdlib.h> enum CopyResult { COPY_OK, COPY_INVALID_ARGUMENT, COPY_OPEN_SOURCE_FAILED, COPY_OPEN_DESTINATION_FAILED, COPY_ALLOCATION_FAILED, COPY_READ_FAILED, COPY_WRITE_FAILED }; enum CopyResult copy_file( const char* source_path, const char* destination_path) { enum { BUFFER_SIZE = 4096 }; FILE* source = NULL; FILE* destination = NULL; unsigned char* buffer = NULL; enum CopyResult result = COPY_OK; if (source_path == NULL || destination_path == NULL) { return COPY_INVALID_ARGUMENT; } source = fopen(source_path, "rb"); if (source == NULL) { result = COPY_OPEN_SOURCE_FAILED; goto cleanup; } destination = fopen(destination_path, "wb"); if (destination == NULL) { result = COPY_OPEN_DESTINATION_FAILED; goto cleanup; } buffer = malloc(BUFFER_SIZE); if (buffer == NULL) { result = COPY_ALLOCATION_FAILED; goto cleanup; } for (;;) { size_t read_count = fread(buffer, 1, BUFFER_SIZE, source); size_t written = 0; while (written < read_count) { size_t current = fwrite( buffer + written, 1, read_count - written, destination); if (current == 0) { result = COPY_WRITE_FAILED; goto cleanup; } written += current; } if (read_count < BUFFER_SIZE) { if (ferror(source)) { result = COPY_READ_FAILED; } break; } } cleanup: free(buffer); if (destination != NULL) { if (fclose(destination) != 0 && result == COPY_OK) { result = COPY_WRITE_FAILED; } } if (source != NULL) { if (fclose(source) != 0 && result == COPY_OK) { result = COPY_READ_FAILED; } } return result; } int main(int argc, char* argv[]) { if (argc != 3) { fprintf( stderr, "usage: %s SOURCE DESTINATION\n", argv[0]); return 1; } enum CopyResult result = copy_file(argv[1], argv[2]); if (result != COPY_OK) { fprintf(stderr, "copy failed: %d\n", result); return 1; } return 0; } |
- 释放为什么是反序?
|
1 2 3 4 5 |
// 获取顺序 source → destination → buffer // 释放顺序 buffer → destination → source |
- 资源可能依赖较早获得的资源,因此反序释放是最稳定的通用规则,与
C++析构栈展开的顺序类似
清理函数应接受部分初始化状态
- 网络连接对象
|
1 2 3 4 5 |
struct Connection { int socket; unsigned char* receive_buffer; size_t capacity; }; |
- 销毁函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdlib.h> #include <unistd.h> void connection_destroy(struct Connection* connection) { if (connection == NULL) { return; } free(connection->receive_buffer); if (connection->socket >= 0) { close(connection->socket); } free(connection); } |
- 创建时先建立安全不变量
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
struct Connection* connection_create(void) { struct Connection* connection = calloc(1, sizeof *connection); if (connection == NULL) { return NULL; } connection->socket = -1; connection->receive_buffer = malloc(4096); if (connection->receive_buffer == NULL) { goto cleanup; } connection->capacity = 4096; connection->socket = socket(AF_INET, SOCK_STREAM, 0); if (connection->socket < 0) { goto cleanup; } return connection; cleanup: connection_destroy(connection); return NULL; } |
- 这里
connection_destroy()能处理:connection == NULL- 缓冲区尚未分配
Socket尚未创建- 全部初始化成功
|
1 2 3 4 5 6 |
// 但要注意 connection_destroy(connection); connection_destroy(connection); /* 错误 */ // 第一次调用后,connection 已经变成悬空指针 // destroy(NULL) 安全不代表用同一个悬空指针重复销毁安全 |
|
1 2 3 |
// 调用者应写 connection_destroy(connection); connection = NULL; |
输出参数必须先进入确定状态
- 创建函数常见接口
|
1 2 |
enum Result connection_create( struct Connection** output); |
- 推荐规则
|
1 2 3 4 5 6 7 8 9 10 11 |
enum Result connection_create( struct Connection** output) { if (output == NULL) { return RESULT_INVALID_ARGUMENT; } *output = NULL; /* 后续创建…… */ } |
|
1 2 3 4 5 6 7 8 9 10 |
// 这样失败后调用者不会获得未初始化指针 struct Connection* connection; enum Result result = connection_create(&connection); if (result != RESULT_OK) { /* connection 保证为 NULL */ } |
|
1 2 3 |
// 成功前最后一步才交付 *output = connection; connection = NULL; /* 所有权转移给调用者 */ |
立即保存errno
POSIX系统调用失败时
|
1 2 3 4 5 6 7 8 9 10 11 12 |
int socket_fd = socket(AF_INET, SOCK_STREAM, 0); if (socket_fd < 0) { int saved_errno = errno; cleanup_something(); fprintf( stderr, "socket failed: %s\n", strerror(saved_errno)); } |
- 为什么不能清理后再读取
errno?- 清理函数可能调用其他系统函数并覆盖
errno
- 清理函数可能调用其他系统函数并覆盖
|
1 2 3 4 |
if (socket_fd < 0) { cleanup_something(); printf("%s\n", strerror(errno)); } |
- 还要注意
- 成功的函数通常不保证把
errno清零 - 必须先检查函数返回值,只有确认失败后才读取
errno
- 成功的函数通常不保证把
|
1 2 3 4 5 |
socket(...); if (errno != 0) { /* 不能这样判断是否失败 */ } |
Windows对应地应在失败后立即保存
|
1 |
int error = WSAGetLastError(); |
异步任务的所有权交接
- 一个可靠的任务模型可以规定
|
1 2 3 4 5 |
struct Task { void (*run)(void* context); void (*destroy_context)(void* context); void* context; }; |
- 约定:
- 入队成功:队列接管整个任务及
context - 入队失败:调用者仍然拥有它们
- 执行完成:工作线程调用
destroy_context - 任务取消:取消路径也必须调用
destroy_context - 队列销毁:剩余任务全部执行或全部清理
- 入队成功:队列接管整个任务及
- 提交端
- 如果提交成功后调用者仍然释放
task,就会造成use-after-free - 如果提交失败而双方都认为对方负责释放,就会造成泄漏
- 如果提交成功后调用者仍然释放
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct Task* task = task_create(); void* context = create_context(); if (task == NULL || context == NULL) { goto cleanup; } task->context = context; context = NULL; /* 转移给 task */ if (!thread_pool_submit(pool, task)) { goto cleanup; } task = NULL; /* 转移给线程池 */ cleanup: destroy_context(context); task_destroy(task); |
goto cleanup 的边界
- 适合:
- 一个函数中按顺序获取多个资源
- 多个失败点需要相同清理
- 只向函数末尾跳转
- 每个资源都有明确无效状态
- 不适合:
- 在业务逻辑中任意跳转
- 在多个标签之间来回跳
- 代替正常循环和函数拆分
- 跳入包含变长数组等特殊对象的作用域
未定义行为、严格别名、整数提升与 volatile
四类行为
| 类型 | 含义 | 示例 |
| 明确定义 | 标准规定唯一语义 | 无符号整数按模运算回绕 |
| 实现定义 | 编译器必须选择并记录一种行为 | 普通 char 是否有符号 |
| 未指定 | 多种结果都合法,不必记录选择 | 独立函数参数的求值顺序 |
| 未定义行为 | 标准不再约束程序行为 | 越界、悬空指针、除零、数据竞争 |
- 常见未定义行为
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int value = INT_MAX; ++value; /* 有符号溢出 */ int array[4]; array[4] = 10; /* 越界写 */ int* pointer = NULL; *pointer = 10; /* 空指针解引用 */ free(pointer); *pointer = 10; /* use-after-free */ int shift = 1 << 32; /* 位移量超出类型宽度 */ |
- “未定义”不表示只会得到一个随机值,还可能:
- 代码被优化器删除
- 条件判断被恒定折叠
- 相邻对象被破坏
Debug和Release表现不同- 程序在修改前的代码处表现异常
|
1 2 3 4 5 6 7 8 |
bool add_one_overflows(int value) { return value + 1 < value; } // 优化器可以推断:合法 C 程序中有符号加法不能溢出,因此这个函数可能被优化为: // return false; // 当 value == INT_MAX 时,原表达式已经产生未定义行为 |
有符号与无符号溢出不同
- 无符号整数按照模
2^N回绕
|
1 2 3 4 |
uint8_t value = 255; ++value; /* value 通常变为 0 */ |
- 如果
uint8_t存在,它恰好是 8 位无符号整数,因此赋值转换结果按模256处理 - 有符号溢出则是未定义行为
|
1 2 3 4 5 6 7 8 9 |
int value = INT_MAX; ++value; /* 未定义行为 */ // 需要检查时应在运算前判断 if (value == INT_MAX) { /* 无法加一 */ } else { ++value; } |
C23提供<stdckdint.h>的检查算术功能
小整数通常先提升为 int
- 表达式中的
char、unsigned char、short、uint8_t、uint16_t等小整数类型,通常先进行整数提升
|
1 2 3 4 |
uint8_t first = 250; uint8_t second = 10; uint8_t result = first + second; |
- 计算过程通常是:
first提升为int,值为250second提升为int,值为10- 使用
int计算得到260 - 转换回
uint8_t - 最终得到
4
|
1 2 3 4 5 6 7 8 9 10 11 |
// 危险例子 uint16_t first = 60000; uint16_t second = 60000; uint32_t result = first * second; // 在 int 为 32 位的平台上,两个 uint16_t 都先提升为 int // 真正执行的是: // int * int // 数学结果 3,600,000,000 无法由 32 位 int 表示,所以在赋值给 uint32_t 之前就已经发生有符号溢出 |
|
1 2 3 4 5 6 7 8 9 |
// 修复 uint32_t result = (uint32_t)first * (uint32_t)second; // 此时使用 uint32_t 进行乘法 // 如果乘积还有可能超过 UINT32_MAX,则应提升到更宽类型并检查: uint64_t product = (uint64_t)first * (uint64_t)second; |
有符号数与无符号数比较
- 网络代码中的典型错误
|
1 2 3 4 5 6 7 8 |
ssize_t received = recv(...); size_t expected = 100; if (received < expected) { /* 数据不完整 */ } // 如果 received == -1,与无符号的 size_t 比较时,它可能被转换成一个巨大的无符号数,使条件为假 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
ssize_t received = recv(...); if (received < 0) { /* 读取错误 */ return; } if (received == 0) { /* 对端正常关闭 */ return; } size_t received_size = (size_t)received; if (received_size < expected) { /* 数据不完整 */ } // 先处理有符号返回值的特殊含义,确认非负后再转换成 size_t |
普通 char 的符号由实现决定
|
1 |
char byte = (char)0xFF; |
- 不同平台中,
byte可能是-1,也可能是255- 取决于普通
char是有符号还是无符号
- 取决于普通
- 所以:
- 文本字符使用
char - 原始字节使用
unsigned char或uint8_t - 不要用
char保存协议长度或二进制标志
- 文本字符使用
|
1 2 |
unsigned char byte = 0xFF; printf("%u\n", (unsigned int)byte); |
位移运算的陷阱
- 错误
- 左操作数
1的类型是int - 在典型
32位int平台上,结果无法由int表示,会产生未定义行为
- 左操作数
|
1 |
uint32_t mask = 1 << 31; |
|
1 2 3 4 5 |
// 改为无符号类型 uint32_t mask = UINT32_C(1) << 31; // 或者 uint32_t mask = (uint32_t)1 << 31; |
- 位移量也必须小于左操作数类型的位数
|
1 2 |
uint32_t value = 1; value << 32; /* 未定义行为 */ |
|
1 2 3 4 5 6 7 8 9 |
// 协议解析时,应确认位移发生在足够宽的无符号类型上 uint32_t value = ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | (uint32_t)data[3]; // 如果不先转换,data[i] 可能提升为 int,导致高位移产生有符号问题 |
严格别名规则
- 编译器通常可以假设
- 指向互不兼容类型的指针,不会指向同一个对象
|
1 2 3 4 5 6 7 8 |
// 危险的类型重解释 float value = 1.0f; uint32_t bits = *(uint32_t*)&value; /* 严格别名问题 */ // 优化器可以假设 uint32_t* 不会访问一个 float 对象 |
- 安全复制对象表示
memcpy通常会被编译器优化成普通寄存器操作,不必为了性能使用非法指针转换
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdint.h> #include <string.h> float value = 1.0f; uint32_t bits; _Static_assert( sizeof bits == sizeof value, "unexpected float representation"); memcpy(&bits, &value, sizeof bits); // 不过这个 bits 的数值仍然依赖浮点表示和字节序,不是可移植序列化格式 |
什么是有效类型?
- 具有声明的普通对象,其有效类型通常就是声明类型:
- 应该通过
float类型或允许的字符类型访问它
- 应该通过
|
1 |
float value; |
malloc()返回的存储没有声明类型
|
1 |
void* memory = malloc(sizeof(int)); |
|
1 2 3 4 5 6 |
// 通过 int* 写入后,可以把该区域作为 int 对象访问 int* value = memory; *value = 42; printf("%d\n", *value); |
- 一般原则:
- 通过与对象兼容的类型访问
- 可以通过
char*、signed char*或unsigned char*检查对象表示 - 不要把同一地址随意转换成不相关结构体或标量指针后解引用
- 类型转换只改变指针表达式,不会自动改变现有对象的真实类型
接收缓冲区不能直接转换成协议结构体
- 危险写法
|
1 2 3 4 |
struct DataHeader* header = (struct DataHeader*)receive_buffer; printf("%d\n", header->data_length); |
- 可能同时违反多个条件:
receive_buffer可能还没有完整头部- 地址可能没有满足
DataHeader对齐 - 字节对象不一定能直接作为结构体对象访问
- 结构体存在填充
- 网络字节序与主机字节序不同
- 原始位模式可能不是字段类型的合法表示
- 缓冲区扩容后
header变成悬空指针
未对齐访问
|
1 2 3 4 |
unsigned char buffer[8]; uint32_t value = *(uint32_t*)(buffer + 1); |
buffer + 1很可能不满足uint32_t的对齐要求- 某些
CPU能处理未对齐读取,另一些可能异常- 即便硬件支持,
C语言层面仍可能是未定义行为
- 即便硬件支持,
- 安全方法:
- 逐字节解码
- 或者
memcpy到正确对齐的本地对象,再处理字节序
安全协议头解码示例
- 协议格式
|
1 2 |
command: 2 字节,大端序 payload_length: 4 字节,大端序 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> enum { WIRE_HEADER_SIZE = 6, MAX_PAYLOAD_SIZE = 1024 * 1024 }; struct PacketHeader { uint16_t command; uint32_t payload_length; }; static uint16_t read_be16( const unsigned char* data) { return (uint16_t)( ((uint16_t)data[0] << 8) | (uint16_t)data[1]); } static uint32_t read_be32( const unsigned char* data) { return ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | (uint32_t)data[3]; } bool decode_header( const unsigned char* data, size_t size, struct PacketHeader* output) { if (data == NULL || output == NULL) { return false; } if (size < WIRE_HEADER_SIZE) { return false; } uint16_t command = read_be16(data); uint32_t payload_length = read_be32(data + 2); if (payload_length > MAX_PAYLOAD_SIZE) { return false; } *output = (struct PacketHeader) { .command = command, .payload_length = payload_length }; return true; } int main(void) { const unsigned char data[WIRE_HEADER_SIZE] = { 0x00, 0x02, /* command = 2 */ 0x00, 0x00, 0x04, 0x00 /* payload_length = 1024 */ }; struct PacketHeader header; if (!decode_header( data, sizeof data, &header)) { fprintf(stderr, "invalid header\n"); return 1; } printf( "command=%u, payload=%u\n", (unsigned int)header.command, (unsigned int)header.payload_length); return 0; } |
volatile 到底保证什么?
|
1 |
volatile unsigned int* status_register; |
volatile表示对该对象的访问具有可观察性,编译器不能像普通无副作用对象那样随意删除或合并这些访问- 常见用途:
- 内存映射硬件寄存器
- 特定底层环境中的设备状态
- 信号处理器与普通代码间的
volatile sig_atomic_t
- 但不要简单理解成
- 每次一定直接访问物理内存或绕过 CPU 缓存
volatile不保证- 原子性
- 线程安全
happens-beforeCPU内存屏障- 缓存一致性时序
- 多个操作组成事务
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 错误的线程通信 volatile bool ready = false; int data; void producer(void) { data = 42; ready = true; } void consumer(void) { while (!ready) { } printf("%d\n", data); } |
|
1 2 3 4 5 |
// 如果两个函数运行在不同线程,这仍然存在数据竞争,行为未定义 // 应使用 _Atomic bool ready; // 并选择正确内存序,或者使用互斥锁和条件变量 |
- 硬件只读状态寄存器可能写成
- 程序不能通过该指针写入
- 每次读取仍然是
volatile访问 - 外部硬件可能改变值
|
1 |
const volatile uint32_t* status_register; |
增量解析器
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
#include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <string.h> enum { WIRE_HEADER_SIZE = 6, RECEIVE_CAPACITY = 64, MAX_PAYLOAD_SIZE = 32 }; enum ParseResult { PARSE_NEED_MORE, PARSE_PACKET_READY, PARSE_INVALID }; struct ReceiveBuffer { unsigned char data[RECEIVE_CAPACITY]; size_t used; }; struct PacketView { uint16_t command; /* * 指向 ReceiveBuffer 内部的借用指针。 */ const unsigned char* payload; size_t payload_size; size_t frame_size; }; static uint16_t read_be16( const unsigned char* data) { return (uint16_t)( ((uint16_t)data[0] << 8) | (uint16_t)data[1]); } static uint32_t read_be32( const unsigned char* data) { return ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | (uint32_t)data[3]; } static bool command_is_valid(uint16_t command) { return command == 1 || command == 2; } static bool receive_buffer_append( struct ReceiveBuffer* buffer, const unsigned char* data, size_t size) { if (buffer == NULL) { return false; } if (size != 0 && data == NULL) { return false; } if (buffer->used > RECEIVE_CAPACITY) { return false; } /* * 使用减法,避免 buffer->used + size 先溢出。 */ if (size > RECEIVE_CAPACITY - buffer->used) { return false; } if (size != 0) { memcpy( buffer->data + buffer->used, data, size); } buffer->used += size; return true; } static bool receive_buffer_consume( struct ReceiveBuffer* buffer, size_t size) { if (buffer == NULL || size > buffer->used) { return false; } size_t remaining = buffer->used - size; if (remaining != 0) { /* * 源和目标区域重叠,所以必须使用 memmove。 */ memmove( buffer->data, buffer->data + size, remaining); } buffer->used = remaining; return true; } static enum ParseResult parse_one_packet( const unsigned char* data, size_t size, struct PacketView* output) { if (output == NULL) { return PARSE_INVALID; } *output = (struct PacketView){0}; if (size != 0 && data == NULL) { return PARSE_INVALID; } /* * 连协议头都不完整,不能读取任何字段。 */ if (size < WIRE_HEADER_SIZE) { return PARSE_NEED_MORE; } uint16_t command = read_be16(data); uint32_t wire_payload_size = read_be32(data + 2); if (!command_is_valid(command)) { return PARSE_INVALID; } /* * 在分配内存或计算总长度之前验证上限。 */ if (wire_payload_size > MAX_PAYLOAD_SIZE) { return PARSE_INVALID; } size_t payload_size = (size_t)wire_payload_size; size_t frame_size = WIRE_HEADER_SIZE + payload_size; if (size < frame_size) { return PARSE_NEED_MORE; } *output = (struct PacketView) { .command = command, .payload = data + WIRE_HEADER_SIZE, .payload_size = payload_size, .frame_size = frame_size }; return PARSE_PACKET_READY; } static void handle_packet( const struct PacketView* packet) { printf( "packet command=%u payload_size=%zu:", (unsigned int)packet->command, packet->payload_size); for (size_t i = 0; i < packet->payload_size; ++i) { printf( " %02X", (unsigned int)packet->payload[i]); } putchar('\n'); } static bool process_receive_buffer( struct ReceiveBuffer* buffer) { for (;;) { struct PacketView packet; enum ParseResult result = parse_one_packet( buffer->data, buffer->used, &packet); if (result == PARSE_NEED_MORE) { return true; } if (result == PARSE_INVALID) { fprintf(stderr, "invalid packet\n"); return false; } /* * payload 指向 buffer 内部。 * 必须在 consume() 之前同步处理。 */ handle_packet(&packet); if (!receive_buffer_consume( buffer, packet.frame_size)) { return false; } /* * 每次成功解析都会消费至少一个完整包, * 因此循环一定取得进展。 */ } } int main(void) { /* * 数据流中包含两个包: * * 包1:command=1, length=3, payload="ABC" * 包2:command=2, length=2, payload={0x00, 0xFF} */ static const unsigned char stream[] = { 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0xFF }; /* * 模拟三次 recv: * 第一次只收到部分包头; * 第二次完成第一个包,并带来第二个包的部分头; * 第三次完成第二个包。 */ static const size_t chunk_sizes[] = { 4, 8, 5 }; struct ReceiveBuffer buffer = {0}; size_t stream_offset = 0; for (size_t i = 0; i < sizeof chunk_sizes / sizeof chunk_sizes[0]; ++i) { size_t chunk_size = chunk_sizes[i]; if (!receive_buffer_append( &buffer, stream + stream_offset, chunk_size)) { fprintf(stderr, "receive buffer full\n"); return 1; } stream_offset += chunk_size; printf( "recv chunk=%zu, buffered=%zu\n", chunk_size, buffer.used); if (!process_receive_buffer(&buffer)) { return 1; } printf( "after parse, buffered=%zu\n", buffer.used); } return 0; } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C_学习一08/14
- ♥ COM组件_303/07
- ♥ C++_volatile10/08
- ♥ C标准库_cctype12/15
- ♥ X86_64汇编学习记述四08/09
- ♥ 排序_堆排序05/08
热评文章
- Zlib记述:一 0
- C相关记述一 0
- C++_volatile 0
- C_学习一 0
- C_学习二 0
- C标准库_cctype 0