带矢量的可变大小的数据包结构

时间:2010-03-07 08:59:47

标签: c++ network-programming networking

最近我一直在深入研究网络编程,我在构建具有可变“数据”属性的数据包时遇到了一些困难。以前的几个问题有很大帮助,但我仍然缺乏一些实施细节。我试图避免使用可变大小的数组,只使用向量。但我无法正确传输它,我相信它在序列化过程中的某个地方。

现在有些代码。

数据包标头

class Packet {

    public:         
        void* Serialize();
        bool Deserialize(void *message);

        unsigned int sender_id;
        unsigned int sequence_number;
        std::vector<char> data;
};

Packet ImpL

typedef struct {
    unsigned int sender_id;
    unsigned int sequence_number;
    std::vector<char> data;
} Packet;

void* Packet::Serialize(int size) {
    Packet* p = (Packet *) malloc(8 + 30);
    p->sender_id = htonl(this->sender_id);
    p->sequence_number = htonl(this->sequence_number);
    p->data.assign(size,'&'); //just for testing purposes
}

bool Packet::Deserialize(void *message) {
   Packet *s = (Packet*)message;
   this->sender_id = ntohl(s->sender_id);
   this->sequence_number = ntohl(s->sequence_number);
   this->data = s->data;
}

在执行期间,我只需创建一个数据包,分配它的成员,然后相应地发送/接收。以上方法仅负责序列化。不幸的是,数据永远不会被转移。

这里要指出一些事情。我猜测 malloc 是错误的,但我不确定如何计算它(即它会是什么其他值)。除此之外,我不确定以这种方式使用矢量的正确方法,并希望有人向我展示如何(代码示例请!):)

编辑:我已经将问题授予了关于使用矢量数据属性实现的最全面的答案。感谢所有回复!

5 个答案:

答案 0 :(得分:3)

这个技巧适用于结构末尾的C风格数组,但不适用于C ++向量。无法保证C ++向量类(并且很可能不会)将其包含的数据放入Packet结构中存在的“头对象”中。相反,该对象将包含指向其他地方的指针,存储实际数据。

答案 1 :(得分:3)

我想你可能想这样做: `

struct PacketHeader 
{
    unsigned int senderId;
    unsigned int sequenceNum;
};

class Packet
{
    protected:
        PacketHeader header;
        std::vector<char> data;
    public:
        char* serialize(int& packetSize);
        void deserialize(const char* data,int dataSize);
}

char* Packet::serialize(int& packetSize)
{
    packetSize = this->data.size()+sizeof(PacketHeader);
    char* packetData = new char[packetSize];
    PacketHeader* packetHeader = (PacketHeader*)packetData;
    packetHeader->senderId = htonl(this->header.senderId);
    packetHeader->sequenceNum = htonl(this->header.sequenceNum);
    char* packetBody = (packetData + sizeof(packetHeader));
    for(size_t i=0 ; i<this->data.size() ; i++)
    {
        packetBody[i] = this->data.at(i);
    }
    return packetData;
}

void deserialize(const char* data,int dataSize)
{
    PacketHeader* packetHeader = (PacketHeader*)data;
    this->header.senderId = ntohl(packetHeader->senderId);
    this->header.sequenceNum = ntohl(packetHeader->sequenceNum);
    this->data.clear();
    for(int i=sizeof(PacketHeader) ; i<dataSize ; i++)
    {
        this->data.push_back(data[i]);
    }
}

`

这些代码不包括绑定检查和自由分配的数据,不要忘记从serialize()函数中删除返回的缓冲区,也可以使用memcpy而不是使用循环将每个字节的字节复制到std中或从std中复制: :矢量

大多数编译器有时会在结构中添加填充,如果在不禁用填充的情况下完整地发送这些数据会导致问题,如果使用visual studio,则可以使用#pragma pack(1)来执行此操作

免责声明:我实际上并没有编译这些代码,您可能需要重新检查它

答案 2 :(得分:2)

我认为问题的中心是你尝试'序列化'那样的向量,你可能假设向量的状态信息被传输了。正如您所发现的那样,当您试图通过网络移动对象时,这并不会真正起作用,而指针之类的东西在其他计算机上并不意味着什么。

我认为最简单的方法是将Packet更改为以下结构:

struct Packet {
    unsigned int sender_id;
    unsigned int sequence_number;
    unsigned int vector_size;
    char data[1];
};

data[1]位是可变长度数组的旧C技巧 - 它必须是结构中的最后一个元素,因为您实际上是在编写结构的大小。您拥有来为此获取数据结构的分配,否则您将处于受伤的世界。

您的序列化功能看起来像这样:

void* Packet::Serialize(std::vector<char> &data) {
    Packet* p = (Packet *) malloc(sizeof(Packet) + data.size());
    p->sender_id = htonl(this->sender_id);
    p->sequence_number = htonl(this->sequence_number);
    p->vector_size = htonl(data.size());
    ::memcpy(p->data, data[0], size);
}

正如您所看到的,我们将传输数据大小和向量的内容,复制到一个容易传输的普通C数组中。你必须记住,在你的网络发送例程中,你必须正确计算结构的大小,因为你必须发送sizeof(Packet) + sizeof(data),否则你将得到矢量切断并重新变好缓冲区溢出区域。

免责声明 - 我没有测试上面的代码,它只是从内存中编写的,因此您可能需要修复奇怪的编译错误。

答案 3 :(得分:2)

我认为您需要直接使用套接字函数返回的字节数组。

出于这些目的,在协议中有两个不同的消息部分是很好的。第一部分是固定大小的“标题”。这将包括后面跟随的大小,“有效负载”或示例中的data

所以,借用你的一些片段并扩展它们,也许你会有这样的东西:

typedef struct {
    unsigned int sender_id;
    unsigned int sequence_number;
    unsigned int data_length;   // this is new
} PacketHeader;

那么当你得到一个缓冲区时,你会把它当作PacketHeader*,并检查data_length以了解后面的字节向量中会出现多少字节。

我还想补充几点......

  • 制作这些字段unsigned int并不明智。 C和C ++的标准没有指定int有多大,而且你想要在所有编译器上都可以预测的东西。我建议在uint32_t

  • 中定义C99类型<stdint.h>
  • 请注意,当您从套接字获取字节时...绝不能保证与另一端写入send()write()的大小相同。您可能会收到不完整的消息(术语中的“数据包”),或者您可能会在一次read()recv()调用中获得多个消息。如果缺少单个请求,您有责任缓冲这些请求,或者如果您在同一个通道中收到多个请求,则循环使用它们。

答案 4 :(得分:1)

由于您已经分配了一些原始内存,然后将其视为非POD类类型的初始化对象,因此这种强制转换非常危险。这可能会在某些时候导致崩溃。

Packet* p = (Packet *) malloc(8 + 30);

查看代码,我假设你想要从调用seralize函数的Packet对象写出一个字节序列。在这种情况下,您不需要第二个数据包对象。您可以创建适当大小的字节向量,然后跨数据复制。

e.g。

void* Packet::Serialize(int size)
{
    char* raw_data = new char[sizeof sender_id + sizeof sequence_number + data.size()];
    char* p = raw_data;
    unsigned int tmp;

    tmp = htonl(sender_id);
    std::memcpy(p, &tmp, sizeof tmp);
    p += sizeof tmp;

    tmp = htonl(sequence_number);
    std::memcpy(p, &tmp, sizeof tmp);
    p += sizeof tmp;

    std::copy(data.begin(), data.end(), p);

    return raw_data;
}

这可能不完全是你想要的,因为我不确定你的size参数的最终对象是什么,并且你的接口可能不安全,因为你返回一个我认为应该是原始数据的指针动态分配。使用管理动态分配内存生命周期的对象会更安全,然后调用者不必猜测是否以及如何释放内存。

此外,调用者无法知道分配了多少内存。这可能与解除分配无关,但可能是要复制或流式传输此缓冲区,则需要此信息。

最好返回std::vector<char>或者通过引用获取一个,或者甚至使该函数成为模板并使用输出迭代器。