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
QDataStreamformat includes metadata forQStringtypes (length prefix), so serialization and deserialization must both useQDataStream - Chinese characters in strings require encoding format consideration; the
fromLocal8Bit()conversion works on Windows - The
operator<<andwriteBytesmethods 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::stringwrite 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.