DEV Community

MinBapE
MinBapE

Posted on

TCP Variable-Length Packet Handling

Introduction

While developing a Socket Chatting program, I encountered a question: how should I handle messages that exceed the predefined buffer size?
This article documents my solution to this problem.


Packet Boundary Problem

// Client
send(sock, packet1, 10, 0);  // Send 10 bytes
send(sock, packet2, 20, 0);  // Send 20 bytes

// Server
char buffer[1024];
int bytes = recv(sock, buffer, 1024, 0);
Enter fullscreen mode Exit fullscreen mode

What will the value of bytes be? 10? 20?
The answer is: "We can't know."

The size of data received from the client is unpredictable.
recv() can:

  • Receive all 30 bytes at once
  • Receive 10 bytes and 20 bytes separately
  • Split into 15 bytes twice
  • Even split into 7 bytes, 13 bytes, and 10 bytes

This is called the Packet Boundary Problem.


TCP is a Stream Protocol

Unlike UDP, TCP transmits data in byte units rather than message units.

send(sock, "Hello", 5, 0);
send(sock, "World", 5, 0);
Enter fullscreen mode Exit fullscreen mode

When a client sends a 5-byte string twice as shown above, we might think the server will receive it twice as well. However, TCP can merge them into a single stream, resulting in receiving a 10-byte string all at once.

This happens due to the following reasons:

1. Nagle's Algorithm

  • A mechanism to improve TCP/IP network efficiency by reducing the number of packets to be transmitted.
  • Small data is buffered and sent together to prevent inefficiency where headers (40 bytes) are larger than data (1 byte).
  • Operating systems use this because Congestion Control across the entire network takes priority over individual program speed.
  • While you can reduce latency by disabling Nagle with TCP_NODELAY, this doesn't change TCP's fundamental stream-based nature, so packet boundary handling on the receiving side remains essential.
  setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
Enter fullscreen mode Exit fullscreen mode

2. Network Layer

  • MTU (Maximum Transmission Unit): Network transmission size is typically limited to 1500 bytes.
  • Large data is split into multiple IP packets for transmission. Depending on network conditions, packets may not arrive in order or may be lost, but the TCP protocol reassembles them to guarantee order.
    • However, during this reassembly process, data may accumulate or be split in the buffer, making data boundaries ambiguous at recv time.

3. recv() Call Timing

  • recv() returns as much data as is currently available, from a minimum of 1 byte to the maximum requested size.
  • The amount of data received varies depending on when recv() is called.

Solution

Header + Payload Structure

I solved this by including size information in the header.

#pragma pack(push, 1)  // Remove structure padding

struct PacketHeader
{
    PacketType type;   // 2 bytes - Packet type
    uint16_t size;     // 2 bytes - Payload size
};

struct Packet
{
    PacketHeader header;                 // 4 bytes (fixed)
    char payload[MAX_PAYLOAD_SIZE];      // Variable (actual data)
};

#pragma pack(pop)
Enter fullscreen mode Exit fullscreen mode

Both server and client can first read the header to determine the payload size. The packet is wrapped with #pragma pack(push, 1) to process it once the payload is completely accumulated to that size.

Sending

void send_packet(int sockfd, const Packet& packet)
{
    // Convert to network byte order
    Packet send_packet = packet;
    send_packet.header.type = htons(packet.header.type);
    send_packet.header.size = htons(packet.header.size);

    /*
        htons() = Host TO Network Short (2-byte conversion)
        - Converts to network format regardless of current system
          -> Solves the problem of different byte ordering across CPUs
    */

    // Cast data for transmission
    const char* data = reinterpret_cast<const char*>(&send_packet);
    size_t total_size = sizeof(PacketHeader) + packet.header.size;
    size_t sent = 0;

    // Loop until all data is sent
    while (sent < total_size)
    {
        ssize_t n = send(sockfd, 
                        data + sent, 
                        total_size - sent, 
                        0);

        if (n < 0)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                continue;  // Temporary error, retry

            perror("send failed");
            return;
        }

        // Add sent size
        sent += n;
    }
}
Enter fullscreen mode Exit fullscreen mode

Receiving

We need to create an accumulation buffer.

struct ClientInfo {
    int fd;
    std::vector<char> recv_buffer;  // Accumulation buffer
};
Enter fullscreen mode Exit fullscreen mode

This is necessary because when packets are split during transmission, they must be stored sequentially in this buffer.

bool receive_data(int sockfd)
{
    ClientInfo& client = clients[sockfd];

    char temp[4096];
    ssize_t n = recv(sockfd, temp, sizeof(temp), 0);

    if (n <= 0)
        return false;  // Connection closed or error

    // Accumulate into existing buffer
    client.recv_buffer.insert(
        client.recv_buffer.end(),
        temp,
        temp + n
    );

    return true;
}
Enter fullscreen mode Exit fullscreen mode
void parse_packets(int sockfd)
{
    ClientInfo& client = clients[sockfd];

    // Continue processing while complete packets exist
    while (client.recv_buffer.size() >= sizeof(PacketHeader))
    {
        // Read header first
        PacketHeader* header =
            reinterpret_cast<PacketHeader*>(client.recv_buffer.data());

        uint16_t payload_size = ntohs(header->size);
        size_t packet_size = sizeof(PacketHeader) + payload_size;

        // Check if complete packet has arrived; if not, wait for next recv()
        if (client.recv_buffer.size() < packet_size)
            break;

        // Extract complete packet
        Packet packet;
        memcpy(&packet, client.recv_buffer.data(), packet_size);

        // Endian conversion (network → host)
        packet.header.type = ntohs(packet.header.type);
        packet.header.size = payload_size;

        // Process packet
        handle_packet(sockfd, packet);

        // Remove processed packet
        client.recv_buffer.erase(
            client.recv_buffer.begin(),
            client.recv_buffer.begin() + packet_size
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Source Code

Socket Chatting Program


Tags

#cpp #networking #tcp #sockets #systemsprogramming

Top comments (0)