Variable-Length Struct Serialization for TCP Communication in C++

When implementing TCP data transmission, serializing and deserializing variable-length structures requires careful handling. This article covers three practical approaches with working code examples.

Byte Alignment Consideration

Before diving into the implementation, it's critical to address byte alignment. TCP message definitions should use single-byte alignment to ensure consistent binary representation across different platforms and compilers.

#pragma pack(1)

Method 1: Using QDataStream (Qt Framework)

For Qt-based projects, QDataStream provides built-in serialization support. The stream operators handle type conversion automatically, though string handling requires special attention.

struct MessagePayload
{
    int count;
    double coefficient;
    std::string label;
    std::vector<int> sizeArray;
    std::vector<std::string> dataItems;

    void serialize(QByteArray* output)
    {
        QDataStream writer(output, QIODevice::WriteOnly);
        writer.setVersion(QDataStream::Qt_5_14);

        writer << count;
        writer << coefficient;

        QString qstring = QString::fromLocal8Bit(label.c_str());
        writer << qstring.toLocal8Bit();

        for (int i = 0; i < count; ++i)
        {
            writer << sizeArray[i];
        }

        for (int j = 0; j < count; ++j)
        {
            QString item = QString::fromLocal8Bit(dataItems[j].c_str());
            writer << item.toLocal8Bit();
        }
    }

    void deserialize(const QByteArray& input)
    {
        QDataStream reader(input);
        reader.setVersion(QDataStream::Qt_4_3);

        reader >> count;
        reader >> coefficient;

        QByteArray rawString;
        reader >> rawString;
        QString qstring = QString::fromLocal8Bit(rawString);
        label = std::string(qstring.toLocal8Bit().constData());

        for (int i = 0; i < count; ++i)
        {
            int temp;
            reader >> temp;
            sizeArray.push_back(temp);
        }

        for (int j = 0; j < count; ++j)
        {
            QByteArray itemBytes;
            reader >> itemBytes;
            QString item = QString::fromLocal8Bit(itemBytes);
            dataItems.push_back(std::string(item.toLocal8Bit().constData()));
        }
    }
};

Usage Pattern

MessagePayload payload;
// Initialize payload fields

QByteArray packet;
payload.serialize(&packet);

// On receiving end
MessagePayload received;
received.deserialize(packet);

Important Notes

  • The QDataStream format includes metadata for QString types (length prefix), so serialization and deserialization must both use QDataStream
  • Chinese characters in strings require encoding format consideration; the fromLocal8Bit() conversion works on Windows
  • The operator<< and writeBytes methods produce different binary formats

Method 2: Type Casting (Fixed-Length Structures Only)

This approach works only for structures containing fixed-size members. Variable-length types like std::string or std::vector cannot use this method.

struct NetworkHeader
{
    int destinationId;
    int sourceId;
    int messageKind;
    int payloadSize;
};

Pure C Implementation

int main()
{
    char* sendBuffer = new char[40960];
    char* receiveBuffer = new char[4096];

    NetworkHeader header;
    // Populate header fields

    // Serialize: struct to binary
    memcpy(sendBuffer, &header, sizeof(NetworkHeader));

    // Receive data from socket
    int bytesReceived = recv(socketFd, receiveBuffer, 4096, 0);
    memcpy(sendBuffer, receiveBuffer, bytesReceived);

    // Deserialize: binary to struct
    NetworkHeader* parsedHeader = reinterpret_cast<NetworkHeader*>(sendBuffer);

    delete[] sendBuffer;
    delete[] receiveBuffer;
    return 0;
}

Qt-Specific Implementation

NetworkHeader header;
// Initialize header

// Serialize
QByteArray packet;
packet.append(reinterpret_cast<char*>(&header), sizeof(NetworkHeader));

// Deserialize
NetworkHeader* parsed = reinterpret_cast<NetworkHeader*>(packet.data());

Method 3: Pure C++ Manual Serialization

For maximum portability and control, manual serialization using memcpy handles variable-length members explicitly. This approach supports std::string, std::vector, and other dynamic types.

struct StructuredData
{
    int identifier;
    int nameLength;
    std::string name;
    int arrayCount;
    std::vector<int> indices;
    std::vector<std::string> items;
    int valuesCount;
    std::vector<double> measurements;

    char* pack() const
    {
        int position = 0;
        char* storage = new char[1024];

        memcpy(storage + position, &identifier, sizeof(int));
        position += sizeof(int);

        memcpy(storage + position, &nameLength, sizeof(int));
        position += sizeof(int);

        memcpy(storage + position, name.data(), nameLength);
        position += nameLength;
        position += 1; // Null terminator safety

        memcpy(storage + position, &arrayCount, sizeof(int));
        position += sizeof(int);

        for (int i = 0; i < arrayCount; ++i)
        {
            memcpy(storage + position, &indices[i], sizeof(int));
            position += sizeof(int);
        }

        for (int i = 0; i < arrayCount; ++i)
        {
            int itemSize = indices[i];
            memcpy(storage + position, items[i].data(), itemSize);
            position += itemSize;
            position += 1;
        }

        memcpy(storage + position, &valuesCount, sizeof(int));
        position += sizeof(int);

        for (int i = 0; i < valuesCount; ++i)
        {
            memcpy(storage + position, &measurements[i], sizeof(double));
            position += sizeof(double);
        }

        return storage;
    }

    void unpack(char* storage)
    {
        int position = 0;

        memcpy(&identifier, storage + position, sizeof(int));
        position += sizeof(int);

        memcpy(&nameLength, storage + position, sizeof(int));
        position += sizeof(int);

        name = std::string(storage + position, nameLength);
        position += nameLength;
        position += 1;

        memcpy(&arrayCount, storage + position, sizeof(int));
        position += sizeof(int);

        for (int i = 0; i < arrayCount; ++i)
        {
            int value;
            memcpy(&value, storage + position, sizeof(int));
            position += sizeof(int);
            indices.push_back(value);
        }

        for (int i = 0; i < arrayCount; ++i)
        {
            int itemLength = indices[i];
            std::string item(storage + position, itemLength);
            position += itemLength;
            position += 1;
            items.push_back(item);
        }

        memcpy(&valuesCount, storage + position, sizeof(int));
        position += sizeof(int);

        for (int i = 0; i < valuesCount; ++i)
        {
            double value;
            memcpy(&value, storage + position, sizeof(double));
            position += sizeof(double);
            measurements.push_back(value);
        }
    }
};

Usage Example

int main()
{
    std::string personName = "Jane Smith";
    std::vector<double> readings = {4.5, 6.7, 8.9};

    std::string item1 = "component_a";
    std::string item2 = "module_b";
    std::string item3 = "中文测试";
    std::string item4 = "item_004_length";

    std::vector<std::string> itemList = {item1, item2, item3, item4};
    std::vector<int> sizeList = {
        item1.size(), item2.size(), item3.size(), item4.size()
    };

    StructuredData original;
    original.identifier = 1;
    original.nameLength = personName.size();
    original.name = personName;
    original.arrayCount = itemList.size();
    original.indices = sizeList;
    original.items = itemList;
    original.valuesCount = readings.size();
    original.measurements = readings;


    char* buffer = original.pack();


    StructuredData restored;
    restored.unpack(buffer);

    std::cout << "ID: " << restored.identifier << std::endl;
    std::cout << "Name: " << restored.name << std::endl;

    for (double val : restored.measurements)
    {
        std::cout << val << std::endl;
    }

    for (const auto& str : restored.items)
    {
        std::cout << str << std::endl;
    }

    delete[] buffer;
    return 0;
}

Key Implementation Details

  • Each std::string write includes an extra byte after the content to prevent issues with null terminators
  • Integer and floating-point members use direct memcpy
  • Dynamic containers require pre-storing the count, followed by individual element serialization
  • Chinese characters in strings encode correctly without special handling

Summary Comparison

Approach Use Case Complexity Encoding Handling
QDataStream Qt projects Low Requires attention for Chinese
Type Casting Fixed-size structs Lowest N/A
Manual memcpy Portable, variable-length High Automatic

The manual memcpy approach offers the most flexibility for cross-platform TCP applications, while QDataStream provides convenience within the Qt ecosystem.

Tags: C++ tcp serialization deserialization Network Programming

Posted on Wed, 30 Sep 2026 16:49:25 +0000 by jariizumi