最平凡日子 最卑微梦想

c++解析网络消息

前几天在甲方驻场开发,碰到一个关于c++解析网络消息的共性问题,因为这个点卡了很多次bug。

解析网络消息是一项常见的任务,通常涉及将接收到的字节数据(如char*数组)转换为特定的应用程序所需的结构。这通常意味着你需要了解消息的格式,然后将其解析为自定义的结构或类对象。

基本方法举例

下面是一个基本的示例:

假设我们有一个简单的消息格式,其中包含一个整数和一个浮点数,需要将其解析为一个自定义的数据结构。

假设我们目标对象的类名为HFDJDNRTOutput(先别管为啥叫这个奇怪的名字了)。

struct HFDJDNRTOutput {
    int id;
    float value;
    std::string message;
};

接下来就是一个解析消息的函数,将char*类型的消息转换为HFDJDNRTOutput对象:

HFDJDNRTOutput parseComplexMessage(const char *message, size_t length) {
    if (length < 12)  
            // Minimum size check (4 bytes for id, 4 for float, 2 for string length, 2 for total length)
        throw std::invalid_argument("Message is too short");

    size_t offset = 0;

    // Read total message length (2 bytes, not used after this in this example)
    uint16_t totalLength;
    std::memcpy(&totalLength, message + offset, sizeof(totalLength));
    offset += sizeof(totalLength);

    // Read ID
    int id;
    std::memcpy(&id, message + offset, sizeof(id));
    offset += sizeof(id);

    // Read Value
    float value;
    std::memcpy(&value, message + offset, sizeof(value));
    offset += sizeof(value);

    // Read string length
    uint16_t strLength;
    std::memcpy(&strLength, message + offset, sizeof(strLength));
    offset += sizeof(strLength);

    // Boundary check
    if (offset + strLength > length) {
        throw std::length_error("String length exceeds message size");
    }

    // Read string data
    std::string strData(message + offset, strLength);

    return HFDJDNRTOutput{id, value, strData};
}

int main() {
    // Example message
    const char exampleMessage[] = {
            0x00, 0x12, // total message length
            0x00, 0x00, 0x00, 0x2A, // id = 42
            0x40, 0x48, 0xF5, 0xC3, // value = 3.14 in IEEE 754 format
            0x00, 0x05, // string length = 5
            'H', 'e', 'l', 'l', 'o'
    };

    size_t messageLength = sizeof(exampleMessage);

    try {
        HFDJDNRTOutput result = parseComplexMessage(exampleMessage, messageLength);

        std::cout << "ID: " << result.id
                  << ", Value: " << result.value
                  << ", Message: " << result.message << std::endl;
    } catch (const std::exception &e)
        std::cerr << "Failed to parse message: " << e.what() << std::endl;

    return 0;
}

这样解析不会发生任何错误,并且越界拷贝异常会被捕获。

带有vector对象的结构

如果HFDJDNRTOutput里包含std::vector对象会发生什么。

用一段实际的代码来解释。

#include <iostream>
#include <vector>

const int DATA_LEN = 24 * 1024;

struct CTranjInfo {
    double p_x = 0;
    double p_y = 0;
    double p_z = 0;
    double v_x = 0;
    double v_y = 0;
    double v_z = 0;
};

struct HFDJDNRTOutput {
    int ID;
    int ForceType;
    int WeaponType;
    double Size[3];                         //长宽高尺寸(m)
    int VehicleNum;                         //车队数量
    double VehicleSpeed;
    double CurLLA[3];                       //当前经纬高
    int size_Vec;                           //TranjInfoVec的大小
    std::vector<CTranjInfo> TranjInfoVec;   //弹道信息
};

struct BasicHeader {
    int head_num = 0;//Message packet header
};

void UupdateRealTimeData(const HFDJDNRTOutput &hfobj) {
    // do something...
}

void HandleHFDNRealTimeData(char *_data, int _side) {
    BasicHeader _head;
    std::memcpy(&_head, _data, sizeof(BasicHeader));//Retrieve interactive packet header
    int head_info = _head.head_num;

    char *p_recvbuf = new char[DATA_LEN];
    const char *p_body = _data;
    p_body = p_body + sizeof(BasicHeader);
    std::memcpy(p_recvbuf, p_body, DATA_LEN);

    char *p_recv = p_recvbuf;
    for (int i = 0; i < head_info; i++) {
        HFDJDNRTOutput objtemp;
        std::memcpy(&objtemp, p_recv, sizeof(HFDJDNRTOutput));
        UupdateRealTimeData(objtemp);
        p_recv = p_recv + sizeof(HFDJDNRTOutput);
    }
    delete[] p_recvbuf;
}

void TestHandleHFDNRealTimeDataV() {
    BasicHeader _header{3};
    HFDJDNRTOutput _body{1, 1, 1, {1.1, 2.2, 3.3}, 1,
                         1.1, {1.1, 2.2, 3.3}, 2, {}};
    char *data_buffer = new char[sizeof(BasicHeader) + 3 * sizeof(HFDJDNRTOutput)];

    std::memcpy(data_buffer, &_header, sizeof(BasicHeader));
    std::memcpy(data_buffer + sizeof(BasicHeader), &_body, sizeof(_body));
    std::memcpy(data_buffer + sizeof(BasicHeader), &_body, sizeof(_body));
    std::memcpy(data_buffer + sizeof(BasicHeader), &_body, sizeof(_body));

    HandleHFDNRealTimeData(data_buffer, 0);
    delete[]data_buffer;
}

int main() {
    TestHandleHFDNRealTimeDataV();
    return 0;
}

bug分析

这里看起来是没有问题的,其实这段代码在Linux下虽然可以通过编译,但是有时会报内存访问越界的error,在 for (int i = 0; i < head_info; i++) 退出第一个循环的时候报错。

调试过程会发现TranjInfoVec对象会出现size很大的情况(结合业务显然是一个错误的size),并且每一个成员为空,所以基本可以断定问题出在HFDJDNRTOutputTranjInfoVec的身上。

综上我认为问题出在std::memcpy(&objtemp, p_recv, sizeof(HFDJDNRTOutput));,直接把一块已知内存长度的内存数据,复制给未知std::vector元素个数的内存空间上,是一个很危险的操作。特殊的,当出现vector.size() == 0时,但是代码依旧给出了固定内存长度的数据进行复制,就会导致内存访问越界。

所以修改后较为安全的HandleHFDNRealTimeData函数如下:

void HandleHFDNRealTimeDataV2(char *_data, int _side) {
    BasicHeader _head;
    std::memcpy(&_head, _data, sizeof(BasicHeader));//取交互数据包头
    int head_info = _head.head_num;

    char *p_recvbuf = new char[DATA_LEN];
    const char *p_body = _data;
    p_body = p_body + sizeof(BasicHeader);
    std::memcpy(p_recvbuf, p_body, DATA_LEN);

    char *p_recv = p_recvbuf;
    for (int i = 0; i < head_info; i++) {
        HFDJDNRTOutput objtemp;
        size_t p_offset = 0;
        std::memcpy(&objtemp + p_offset, p_recv, 3 * sizeof(int));
        p_offset += 3 * sizeof(int);
        std::memcpy(&objtemp + p_offset, p_recv, 3 * sizeof(double));
        p_offset += 3 * sizeof(double);
        std::memcpy(&objtemp + p_offset, p_recv, sizeof(int));
        p_offset += sizeof(int);
        std::memcpy(&objtemp + p_offset, p_recv, 4 * sizeof(double));
        p_offset += 4 * sizeof(double);
        std::memcpy(&objtemp + p_offset, p_recv, sizeof(int));
        p_offset += sizeof(int);

        int size_Vec = 0;
        std::memcpy(&size_Vec, p_recv, sizeof(int));
        std::memcpy(&objtemp + p_offset, p_recv, sizeof(int));
        p_offset += sizeof(int);

        if (size_Vec != 0) {
            std::vector<CTranjInfo> _TranjInfoVec{};
            for (int i = 0; i < size_Vec; i++) {
                CTranjInfo _temp{};
                std::memcpy(&objtemp + p_offset, p_recv, sizeof(CTranjInfo));
                p_offset += sizeof(CTranjInfo);
                _TranjInfoVec.emplace_back(_temp);
            }
        }

        UupdateRealTimeData(objtemp);
        p_recv = p_recv + sizeof(HFDJDNRTOutput);
    }
    delete[] p_recvbuf;
}

看起来很蠢很麻烦是不是,没错。

一点个人编码习惯

个人倾向将复制部分写进HFDJDNRTOutput的构造函数里,然后在每次需要复制相同类型的消息包时,不需要重复写解析消息包的方法,只需要在构造函数里传入char *指针即可完成。

结构体改为如下:

struct HFDJDNRTOutput {
    int ID;
    int ForceType;
    int WeaponType;
    double Size[3];                         //长宽高尺寸(m)
    int VehicleNum;                         //车队数量
    double VehicleSpeed;
    double CurLLA[3];                       //当前经纬高
    int size_Vec;                           //TranjInfoVec的大小

    std::vector<CTranjInfo> TranjInfoVec;   //弹道信息

    HFDJDNRTOutput(char *){
        // ... to do the copy things like func HandleHFDNRealTimeDataV2
    }
};

解析函数改为如下:

void HandleHFDNRealTimeDataV3(char *_data, int _side) {
    BasicHeader _head;
    std::memcpy(&_head, _data, sizeof(BasicHeader));//取交互数据包头
    int head_info = _head.head_num;

    char *p_recvbuf = new char[DATA_LEN];
    const char *p_body = _data;
    p_body = p_body + sizeof(BasicHeader);
    std::memcpy(p_recvbuf, p_body, DATA_LEN);

    char *p_recv = p_recvbuf;
    for (int i = 0; i < head_info; i++) {
        HFDJDNRTOutput objtemp(p_recv);
        p_recv+=sizeof (objtemp);
    }
    delete[] p_recvbuf;
}

闲言多叙

这个bug的优化方式还有很多,比如C++20 引入的位操作和改进的标准库,都可以更好的改进解析,特别是对于简单场景:

借助 std::copy

std::vector<int> readVectorData(const char* buffer, size_t vectorSize) {
    std::vector<int> data(vectorSize);
    std::copy(buffer, buffer + vectorSize * sizeof(int), reinterpret_cast<char*>(data.data()));
    return data;
}

借助std::span 提供数组视图,可以更安全地操作:

#include <span>

HFDJDNRTOutput parseWithSpan(const char* buffer, size_t length) {
    std::span<const char> spanBuffer(buffer, length);

    size_t offset = 0;

    auto read = [&](auto& dest) {
        std::memcpy(&dest, spanBuffer.data() + offset, sizeof(dest));
        offset += sizeof(dest);
    };

    HFDJDNRTOutput output;
    int vectorSize;

    // Ensure safe bounds checks using span
    read(output.id);
    read(output.value);
    read(vectorSize);

    // Check available length for vector data
    if (offset + vectorSize * sizeof(int) <= spanBuffer.size()) {
        output.data.resize(vectorSize);
        std::memcpy(output.data.data(), spanBuffer.data() + offset, vectorSize * sizeof(int));
    } else {
        throw std::length_error("Insufficient data for vector");
    }

    return output;
}

这个bug的深究让我想起来之前第一个带我写c++的师傅,那时候他曾多次督促我阅读《重构》。