Build a TCP and UDP Message Lab in C

Build C TCP and UDP echo services to compare streams, frames, and datagrams.

Introduction

30 Second Summary

A sent message can look complete on your screen while the receiving program sees only the bytes available at that moment. That mismatch is easy to miss until a split transfer or missing reply exposes it.

In this project, you will build matching echo services in C over TCP and UDP. You will test the cases that reveal where each transport succeeds or falls short.

What You'll Build

Watch matching echo services reveal TCP's byte-stream behavior beside UDP's preserved datagrams in two terminals.

By the end of this project, you'll have:

  • Run a naive TCP exchange and see HELLO-WORLD echoed in both terminals.
  • Trigger a split transfer and watch the server echo only HELLO. Use length-prefixed framing to reconstruct the complete message. Reject malformed clients without stopping the server.
  • Send a UDP datagram and receive one complete echo. Send an oversized datagram and watch the server reject it.
  • Secret Mission: add a timed UDP receive that retries once when no reply arrives.

Are there any prerequisites?

You need a Mac with Visual Studio Code. Basic networking familiarity is helpful. No previous C socket programming experience is required.

Before We Start

This step helps you commit to the local C socket lab before the hands-on work begins. The lab demonstrates TCP as a byte stream that needs application framing while UDP carries separate datagrams.

Set Up the C Toolchain

Your socket lab needs a compiler before any client or server can run.

Visual Studio Code handles the editing. Command Line Tools for Xcode supply Apple Clang. They also install the macOS SDK required for socket programs.

In this step, get ready to:
  • Confirm that Apple Clang is available in macOS Terminal.
  • Prepare the socket-lab workspace with two Terminal panes.
  • Define the shared network settings in common.h.
Confirm Apple Clang

Clang compiles your C files into executable programs. A version check reveals whether your Mac already has the required toolchain.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into the search field.
  • Press Return to open macOS Terminal.
  • Check for Apple Clang by running this command:
clang --version

What does this check?

The clang --version command asks the compiler to identify itself. A successful result confirms that Terminal can find Apple Clang.

✔️ I see Apple Clang information

Your compiler is ready. The output confirms that Apple Clang is installed on your Mac.

ⓧ Clang is unavailable

Your Mac needs the compatible Command Line Tools package. The installation command opens Apple's installer dialog.

  • Request the compatible Command Line Tools package by running:
xcode-select --install

What does this command do?

The command asks macOS to install the Command Line Tools for Xcode package. This package includes Clang plus the macOS SDK.

The download can take a few minutes. The progress indicator in the installer shows that the setup is moving.

  • Complete the installation in Apple's installer dialog.
  • Return to the macOS Terminal window from earlier.
  • Confirm the installation by running the version check again:
clang --version

What should I see?

Terminal prints Apple Clang version information when the installation is complete. This confirms that the compiler is available from your shell.

Your toolchain is now ready to compile the socket programs in this lab.

Installer not completing?

Keep the installer dialog open. Follow the recovery option shown by macOS if the download stops.

Help me diagnose the Command Line Tools installation on my Mac.

Prepare the lab workspace

A named workspace keeps every source file in one place. Two Terminal panes let you run a server beside its client.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into the search field.
  • Press Return to open Visual Studio Code.
  • Select File in the menu bar.
  • Select Open Folder....

The folder picker lets you create the lab workspace directly on your Desktop.

  • Select Desktop in the folder picker.
  • Select New Folder.
  • Type socket-lab into the folder name field.
  • Press Return to create the folder.
  • Select Open.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

You'll see socket-lab in the Explorer sidebar. This confirms that Visual Studio Code is using the correct workspace.

  • Select Terminal in the menu bar.
  • Select New Terminal.
  • Move your pointer over the terminal entry on the right side of the Terminal panel.
  • Select the inline split button.

You'll see two Terminal panes side by side. Each pane is ready to run one side of a socket exchange.

Don't see two Terminal panes?

Use the Terminal menu to create the first pane. Move your pointer over its entry to reveal the inline split button.

Help me create two side-by-side Terminal panes in Visual Studio Code.

Create the shared network settings

Every program in the lab uses the same address plus port values. A shared header keeps those settings consistent across future clients and servers.

  • Select the new-file button in the Explorer header.
  • Type common.h into the filename field.
  • Press Return to create the file.
  • Populate common.h by pasting this code:
#ifndef COMMON_H
#define COMMON_H

#define LOOPBACK_ADDRESS "127.0.0.1"
#define TCP_PORT 9090
#define UDP_PORT 9091
#define MAX_PAYLOAD 1024

#endif

What does this header define?

  • The header guard prevents repeated inclusion of common.h from defining the settings more than once.
  • LOOPBACK_ADDRESS uses 127.0.0.1 as the local IPv4 loopback address.
  • TCP_PORT plus UDP_PORT assign separate local ports with values 9090 plus 9091.
  • MAX_PAYLOAD caps each payload at 1024 bytes.
  • Save common.h by pressing Cmd+S.
  • Confirm that common.h remains listed under socket-lab in the Explorer sidebar.

The editor should show the loopback address plus all three numeric settings. That shared header is now the one source of network settings for every program you build.

Header missing from Explorer?

Check that the filename is exactly common.h. Confirm that the file appears directly inside socket-lab.

Help me check why common.h is missing or saved outside socket-lab.

✔️ Awesome, I've got everything!

Your common.h file matches the shared settings required by the lab.

ⓧ I'd like to double check the full code

The complete common.h file at this point is:

#ifndef COMMON_H
#define COMMON_H

#define LOOPBACK_ADDRESS "127.0.0.1"
#define TCP_PORT 9090
#define UDP_PORT 9091
#define MAX_PAYLOAD 1024

#endif

Before the final check, what toolchain name do you expect to see?

  • Select the left Terminal pane in Visual Studio Code.
  • Confirm Apple Clang by running:
clang --version

What does this final check prove?

This check confirms that the compiler remains available inside your project workspace. Your future compile commands can run from either Terminal pane.

You'll see Apple Clang version information in the Terminal pane. That's the setup locked in: your Mac can compile the C programs you add to socket-lab.

Your C workspace is ready for socket code. Next, you'll build a naive TCP echo service plus watch its first message travel across the loopback address.

Build a Naive TCP Echo

Your C toolchain is ready. Now you can turn the shared settings in common.h into a working TCP exchange.

A small TCP echo gives you a visible baseline before you test message boundaries. This first version treats one recv() result as one message.

In this step, get ready to:
  • Build a sequential TCP server that echoes one received chunk.
  • Build a TCP client that can choose its send mode.
  • Compile both programs and run a normal loopback exchange.
Build the One-Client Server

A socket is one endpoint of a network exchange. The server creates a stream socket for TCP over IPv4.

How Does the Server Flow Work?

The server binds its socket to 127.0.0.1:9090. That loopback address keeps the entire exchange on your Mac.

The server listens for one client. It accepts that client before receiving one available chunk.

  • In the Explorer sidebar of Visual Studio Code, click the New File icon.
  • Enter naive_tcp_server.c as the file name.
  • Add the server headers and socket setup by copying this first section:
#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

    int reuse = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(server_fd);
        return 1;
    }

What Does This Section Do?

  • The header group provides the IPv4 types and socket functions used by the server.
  • signal(SIGPIPE, SIG_IGN) prevents a broken connection from terminating the whole process.
  • socket(AF_INET, SOCK_STREAM, 0) creates the TCP socket.
  • SO_REUSEADDR makes the local address easier to reuse after restarting the server.
  • Add the address setup and listening path immediately below the previous section:

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(server_fd);
        return 1;
    }

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    if (listen(server_fd, 4) < 0) {
        perror("listen");
        close(server_fd);
        return 1;
    }

    printf("Naive TCP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, TCP_PORT);

    int client_fd;
    do {
        client_fd = accept(server_fd, NULL, NULL);
    } while (client_fd < 0 && errno == EINTR);

What Does This Section Do?

  • sockaddr_in stores the server's IPv4 address and port.
  • htons(TCP_PORT) converts the port to network byte order.
  • bind() assigns the loopback address to the socket.
  • listen() prepares the socket to accept connections.
  • accept() waits for one client. The loop retries when the call is interrupted.
  • Complete the server by adding its one-call receive and echo path:

    if (client_fd < 0) {
        perror("accept");
        close(server_fd);
        return 1;
    }

    char buffer[128];
    ssize_t received = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (received > 0) {
        buffer[received] = '\0';
        printf("Naive server treated this chunk as one message: %s\n", buffer);
        if (send(client_fd, buffer, (size_t)received, 0) < 0) {
            perror("send");
        }
    } else if (received == 0) {
        printf("Client disconnected before sending data\n");
    } else {
        perror("recv");
    }

    close(client_fd);
    close(server_fd);
    return 0;
}

What Does This Section Do?

  • recv() reads up to 127 bytes into the buffer with one call.
  • The null terminator makes the received bytes printable as a C string.
  • send() echoes only the chunk returned by that receive call.
  • The final calls close both socket descriptors before the program exits.
  • Save naive_tcp_server.c.
  • Switch back to the server Terminal from earlier.
  • Compile the server by running this command:
clang naive_tcp_server.c -o naive_tcp_server

What Does This Command Do?

Clang compiles naive_tcp_server.c. The -o option names the executable naive_tcp_server.

  • Confirm that the Terminal returns to its prompt without compiler errors.
  • Confirm that naive_tcp_server now appears in the Explorer sidebar.

Server Not Compiling?

  • Check that naive_tcp_server.c is saved inside the same socket-lab folder as common.h.
  • Compare every brace and parenthesis with the complete file below.

Help me find the difference in my TCP server code.

✔️ Awesome, I've Got Everything

Your server source is saved. The naive_tcp_server executable is ready to accept one client.

ⓧ I'd Like to Double Check the Full Code

Compare your complete naive_tcp_server.c file with this reference.

#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

    int reuse = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(server_fd);
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(server_fd);
        return 1;
    }

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    if (listen(server_fd, 4) < 0) {
        perror("listen");
        close(server_fd);
        return 1;
    }

    printf("Naive TCP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, TCP_PORT);

    int client_fd;
    do {
        client_fd = accept(server_fd, NULL, NULL);
    } while (client_fd < 0 && errno == EINTR);

    if (client_fd < 0) {
        perror("accept");
        close(server_fd);
        return 1;
    }

    char buffer[128];
    ssize_t received = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (received > 0) {
        buffer[received] = '\0';
        printf("Naive server treated this chunk as one message: %s\n", buffer);
        if (send(client_fd, buffer, (size_t)received, 0) < 0) {
            perror("send");
        }
    } else if (received == 0) {
        printf("Client disconnected before sending data\n");
    } else {
        perror("recv");
    }

    close(client_fd);
    close(server_fd);
    return 0;
}

How to Use This Reference

Check the file from top to bottom. Pay close attention to the braces around each error path.

Build the Normal TCP Client

The client creates its own stream socket. It connects to the address already shared through common.h.

Normal mode sends HELLO-WORLD in one call. The file also includes the split mode that you use in the next step.

  • In the Explorer sidebar, click the New File icon.
  • Enter naive_tcp_client.c as the file name.
  • Add the client headers and mode validation by copying this first section:
#include "common.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    const char *mode = argc > 1 ? argv[1] : "normal";
    if (strcmp(mode, "normal") != 0 && strcmp(mode, "split") != 0) {
        fprintf(stderr, "Usage: %s [normal|split]\n", argv[0]);
        return 1;
    }

    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

What Does This Section Do?

  • argc and argv expose the mode typed after the executable name.
  • The mode defaults to normal when no argument is provided.
  • strcmp() rejects any mode other than normal or split.
  • socket(AF_INET, SOCK_STREAM, 0) creates the client's TCP socket.
  • Add the server address and connection path immediately below the previous section:

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (connect(socket_fd, (struct sockaddr *)&server_address,
                sizeof(server_address)) < 0) {
        perror("connect");
        close(socket_fd);
        return 1;
    }

    if (strcmp(mode, "split") == 0) {
        const char *first = "HELLO";
        const char *second = "-WORLD";
        if (send(socket_fd, first, strlen(first), 0) < 0) {
            perror("send first chunk");
        }
        sleep(1);
        if (send(socket_fd, second, strlen(second), 0) < 0) {
            perror("send second chunk");
        }
    } else {

What Does This Section Do?

  • server_address holds the loopback address and TCP port.
  • connect() establishes the client side of the TCP connection.
  • Split mode sends HELLO first. It waits one second before sending -WORLD.
  • Complete the client by adding the normal send and reply path:
        const char *message = "HELLO-WORLD";
        if (send(socket_fd, message, strlen(message), 0) < 0) {
            perror("send");
            close(socket_fd);
            return 1;
        }
    }

    char reply[128];
    ssize_t received = recv(socket_fd, reply, sizeof(reply) - 1, 0);
    if (received > 0) {
        reply[received] = '\0';
        printf("Naive reply: %s\n", reply);
    } else if (received == 0) {
        printf("Server closed without a reply\n");
    } else {
        perror("recv");
    }

    close(socket_fd);
    return 0;
}

What Does This Section Do?

  • Normal mode sends the complete HELLO-WORLD message.
  • recv() waits for one echoed chunk from the server.
  • The client prints the reply after adding a null terminator.
  • close() releases the client socket before the program exits.
  • Save naive_tcp_client.c.
  • Switch back to the client Terminal from earlier.
  • Compile the client by running this command:
clang naive_tcp_client.c -o naive_tcp_client

What Does This Command Do?

Clang compiles naive_tcp_client.c. The resulting executable is named naive_tcp_client.

  • Confirm that the Terminal returns to its prompt without compiler errors.
  • Confirm that naive_tcp_client now appears in the Explorer sidebar.

Client Not Compiling?

  • Check that the line beginning with } else { sits directly before the normal mode message.
  • Check that naive_tcp_client.c is saved in the same folder as common.h.

Help me debug my TCP client compilation problem.

✔️ Awesome, I've Got Everything

Your client source is saved. The naive_tcp_client executable supports both send modes.

ⓧ I'd Like to Double Check the Full Code

Compare your complete naive_tcp_client.c file with this reference.

#include "common.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    const char *mode = argc > 1 ? argv[1] : "normal";
    if (strcmp(mode, "normal") != 0 && strcmp(mode, "split") != 0) {
        fprintf(stderr, "Usage: %s [normal|split]\n", argv[0]);
        return 1;
    }

    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (connect(socket_fd, (struct sockaddr *)&server_address,
                sizeof(server_address)) < 0) {
        perror("connect");
        close(socket_fd);
        return 1;
    }

    if (strcmp(mode, "split") == 0) {
        const char *first = "HELLO";
        const char *second = "-WORLD";
        if (send(socket_fd, first, strlen(first), 0) < 0) {
            perror("send first chunk");
        }
        sleep(1);
        if (send(socket_fd, second, strlen(second), 0) < 0) {
            perror("send second chunk");
        }
    } else {
        const char *message = "HELLO-WORLD";
        if (send(socket_fd, message, strlen(message), 0) < 0) {
            perror("send");
            close(socket_fd);
            return 1;
        }
    }

    char reply[128];
    ssize_t received = recv(socket_fd, reply, sizeof(reply) - 1, 0);
    if (received > 0) {
        reply[received] = '\0';
        printf("Naive reply: %s\n", reply);
    } else if (received == 0) {
        printf("Server closed without a reply\n");
    } else {
        perror("recv");
    }

    close(socket_fd);
    return 0;
}

How to Use This Reference

Check the connection path first. Compare the two mode branches before checking the reply logic.

Run the Normal Echo

Both executables are ready. The server must be listening before the client tries to connect.

Before you run the pair, what message do you expect both Terminal windows to display?

  • Switch back to the server Terminal from earlier.
  • Start the one-client server by running this command:
./naive_tcp_server

What Should You See?

The server prints that it is listening on 127.0.0.1:9090. It waits at that point because no client has connected.

  • Switch to the client Terminal from earlier.
  • Run the normal client mode with this command:
./naive_tcp_client normal

What Should You See?

The client prints Naive reply: HELLO-WORLD. The server prints Naive server treated this chunk as one message: HELLO-WORLD.

Both programs exit after this single exchange. That successful round trip is your baseline.

Normal Echo Not Working?

  • Start naive_tcp_server before running the client.
  • Run each executable from the socket-lab folder.
  • Recompile the source file that you changed most recently.

Help me diagnose why my local TCP echo does not complete.

You have a working local TCP round trip. The client sends HELLO-WORLD. The server echoes the received chunk.

Your baseline exchange works. Next, you will test whether one TCP receive call always matches the message the client intended to send.

Break the Byte-Stream Assumption

Your normal TCP exchange proved that the client and server can communicate over IPv4 loopback.

The server still assumes that one recv() call returns one complete application message. This step tests that assumption by separating one intended message across two sends.

In this step, get ready to:
  • Restart the naive server for a fresh client connection.
  • Run the client in split mode.
  • Explain what the two terminals prove about TCP message boundaries.
Prepare the one-call server

The naive server accepts one client before it exits. A fresh run gives the split client a new connection to test.

  • Return to the server Terminal window from earlier.
  • Start a fresh one-client run by running this command:
./naive_tcp_server

What does this command do?

The ./naive_tcp_server command starts the existing server executable. The server waits for one client on 127.0.0.1:9090.

The server calls recv() once after the client connects. That single call creates the assumption you are testing.

You'll see Naive TCP server listening on 127.0.0.1:9090 in the server Terminal.

Run the split client

The existing split mode sends HELLO first. It waits one second before sending -WORLD.

  • Switch to the client Terminal window from earlier.
  • Predict whether the server's single recv() call captures the intended message as one unit.
  • Run the split test by running this command:
./naive_tcp_client split

What does this command test?

The split argument selects the client's two-send path. The one-second pause separates the writes long enough for the server to process the first available bytes.

In the client Terminal, you'll see Naive reply: HELLO.

In the server Terminal, you'll see Naive server treated this chunk as one message: HELLO.

Good catch. Your terminals now expose the message-boundary bug hidden by the normal run.

Split test looks different?

  • Confirm that the server Terminal showed the listening message before you started the client.
  • Check that the client mode is split.
  • Restart the server before retrying because it handles one client per run.

Help me diagnose why the split test did not show the first chunk on its own.

Explain the result

The result exposes a core property of a TCP byte stream. TCP keeps the bytes in order without retaining the application's send boundaries.

Why does the split test fail?

The server's recv() call returns the bytes available at that moment. The call has no information about the client's intended message boundary.

The server immediately treats HELLO as complete. Application framing must define where one TCP message ends.

You changed no source code. The different result came entirely from splitting one intended message across two sends.

You have exposed the one-call receiver's flaw. Next, you'll give TCP messages explicit boundaries that survive split transfers.

Add Length-Prefixed TCP Framing

Your split test exposed the weak point in the naive TCP server. It echoes the first available bytes because the byte stream does not carry your intended message boundary.

A length-prefixed frame solves that problem. Each message starts with a 4-byte length in network byte order. Exact-transfer loops then collect the complete header and payload before the server responds.

In this step, get ready to:
  • Define exact-transfer helpers and the framed TCP protocol.
  • Build the framed TCP server and client.
  • Test split transfers plus malformed frames.
Create the framed protocol

A frame gives the receiver a concrete byte count. The protocol accepts payloads from 1 through MAX_PAYLOAD bytes.

Why use a length prefix?

A 4-byte length tells the receiver exactly how many payload bytes belong to the current message. This works even when the header or payload arrives across several receive calls.

The sender converts the length with htonl(). The receiver converts it back with ntohl().

  • In the VS Code Explorer sidebar for socket-lab, create a file named tcp_protocol.h.
  • Add the header guard plus the protocol status constants by pasting this code:
#ifndef TCP_PROTOCOL_H
#define TCP_PROTOCOL_H

#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <sys/socket.h>

#define EXACT_ERROR -1
#define EXACT_CLOSED 0
#define EXACT_OK 1
#define EXACT_TRUNCATED 2

#define FRAME_ERROR -1
#define FRAME_CLOSED 0
#define FRAME_OK 1
#define FRAME_TRUNCATED 2
#define FRAME_INVALID_LENGTH 3

What do these constants represent?

  • The EXACT_ constants classify an exact-byte transfer as successful, closed, truncated, or failed.
  • The FRAME_ constants classify complete frames plus invalid declared lengths.
  • Add send_all() below the constants by pasting this code:
static int send_all(int socket_fd, const void *buffer, size_t length) {
    const char *bytes = buffer;
    size_t sent_total = 0;

    while (sent_total < length) {
        ssize_t sent = send(socket_fd, bytes + sent_total,
                            length - sent_total, 0);
        if (sent > 0) {
            sent_total += (size_t)sent;
        } else if (sent < 0 && errno == EINTR) {
            continue;
        } else {
            return -1;
        }
    }

    return 0;
}

How does sending become reliable?

  • The sent_total counter tracks how many bytes have left the process.
  • The loop retries after EINTR or continues with any bytes still unsent.
  • A return value of 0 means the complete buffer was handed to the socket.
  • Add recv_exact() below send_all() by pasting this code:
static int recv_exact(int socket_fd, void *buffer, size_t length) {
    char *bytes = buffer;
    size_t received_total = 0;

    while (received_total < length) {
        ssize_t received = recv(socket_fd, bytes + received_total,
                                length - received_total, 0);
        if (received > 0) {
            received_total += (size_t)received;
        } else if (received == 0) {
            return received_total == 0 ? EXACT_CLOSED : EXACT_TRUNCATED;
        } else if (errno == EINTR) {
            continue;
        } else {
            return EXACT_ERROR;
        }
    }

    return EXACT_OK;
}

How does receiving become exact?

  • The loop continues until received_total reaches the requested length.
  • A close before any bytes means the client disconnected cleanly.
  • A close after some bytes marks an incomplete transfer as truncated.
  • Add send_frame() below recv_exact() by pasting this code:
static int send_frame(int socket_fd, const char *message, uint32_t length) {
    if (length == 0 || length > MAX_PAYLOAD) {
        return -1;
    }

    uint32_t network_length = htonl(length);
    if (send_all(socket_fd, &network_length, sizeof(network_length)) < 0) {
        return -1;
    }

    return send_all(socket_fd, message, length);
}

How is a frame sent?

  • The function rejects empty payloads plus lengths above MAX_PAYLOAD.
  • The 32-bit length is converted to network byte order before transmission.
  • The header and payload both pass through send_all().

The final helper has two logical sections. The first reads the header and classifies an early close.

  • Start recv_frame() below send_frame() by pasting this first section:
static int recv_frame(int socket_fd, char *message, uint32_t capacity,
                      uint32_t *length_out) {
    uint32_t network_length;
    int status = recv_exact(socket_fd, &network_length,
                            sizeof(network_length));

    if (status == EXACT_CLOSED) {
        return FRAME_CLOSED;
    }
    if (status == EXACT_TRUNCATED) {
        return FRAME_TRUNCATED;
    }
    if (status == EXACT_ERROR) {
        return FRAME_ERROR;
    }

What does the header check prove?

The receiver requests exactly 4 header bytes. The result distinguishes a clean disconnect from a partial header or socket error.

  • Complete recv_frame() by pasting this section directly after the header checks:
    uint32_t length = ntohl(network_length);
    if (length == 0 || length > capacity) {
        return FRAME_INVALID_LENGTH;
    }

    status = recv_exact(socket_fd, message, length);
    if (status == EXACT_ERROR) {
        return FRAME_ERROR;
    }
    if (status != EXACT_OK) {
        return FRAME_TRUNCATED;
    }

    message[length] = '\0';
    *length_out = length;
    return FRAME_OK;
}

#endif

How is the payload validated?

  • The receiver converts the header to host byte order before checking its value.
  • A zero length or a length above the buffer capacity is rejected before any payload read.
  • A valid payload is null-terminated for safe display as a C string.
  • Save tcp_protocol.h.
  • Confirm the file ends with the closing #endif directive.

Does the protocol header look incomplete?

Check that each function appears once in the same order shown above. Confirm that the final #endif sits after recv_frame().

Ask for help with the structure if the braces no longer line up. Help me compare my tcp_protocol.h structure with a header containing send_all, recv_exact, send_frame, and recv_frame.

Build the framed server and client

The server applies the frame parser to one client at a time. A malformed client is closed without stopping the listening process, so the next connection still gets a clean attempt.

  • In the VS Code Explorer sidebar for socket-lab, create a file named tcp_server.c.
  • Add the server headers plus startup checks by pasting this code:
#include "tcp_protocol.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

How does the server start safely?

The server ignores SIGPIPE so a broken client connection becomes a normal send error. It then creates an IPv4 stream socket.

  • Add the socket reuse setting plus loopback address configuration below the socket check:
    int reuse = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(server_fd);
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(server_fd);
        return 1;
    }

What address does the server use?

The server binds to the shared LOOPBACK_ADDRESS and TCP_PORT values from common.h. Socket reuse makes repeated local test runs easier.

  • Add the bind and listen setup below the address configuration:
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    if (listen(server_fd, 4) < 0) {
        perror("listen");
        close(server_fd);
        return 1;
    }

    printf("Framed TCP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, TCP_PORT);

What becomes ready here?

The socket claims the loopback endpoint and starts listening with a pending-connection queue. The printed address confirms where the client must connect.

The remaining sections form one continuous client-processing loop. Add them in order before saving the file.

  • Start the server's accept loop below the listening message:
    for (;;) {
        int client_fd = accept(server_fd, NULL, NULL);
        if (client_fd < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("accept");
            break;
        }

        printf("TCP client connected\n");

        for (;;) {
            char message[MAX_PAYLOAD + 1];
            uint32_t message_length = 0;
            int status = recv_frame(client_fd, message, MAX_PAYLOAD,
                                    &message_length);

How are clients received?

The outer loop accepts clients sequentially. The inner loop reads framed messages from the current client into a bounded buffer.

  • Add the frame result handling directly after the recv_frame() call:
            if (status == FRAME_CLOSED) {
                printf("TCP client closed cleanly\n");
                break;
            }
            if (status == FRAME_TRUNCATED) {
                printf("Rejected truncated TCP frame\n");
                break;
            }
            if (status == FRAME_INVALID_LENGTH) {
                printf("Rejected invalid TCP frame length\n");
                break;
            }
            if (status == FRAME_ERROR) {
                perror("recv_frame");
                break;
            }

How are bad clients contained?

Each failure status ends only the current client's inner loop. The listening socket stays available for later clients.

  • Complete the server below the status checks by pasting this code:
            printf("TCP message: %s\n", message);
            if (send_frame(client_fd, message, message_length) < 0) {
                perror("send_frame");
                break;
            }
        }

        close(client_fd);
        printf("Ready for the next TCP client\n");
    }

    close(server_fd);
    return 0;
}

How does the server recover?

Valid frames are echoed with the same framing helper. When a client finishes or fails, its socket closes before the outer loop accepts another connection.

  • Save tcp_server.c.
  • Create tcp_client.c in the same VS Code Explorer sidebar.
  • Add the client headers plus the first part of connect_to_server() by pasting this code:
#include "tcp_protocol.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

static int connect_to_server(void) {
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(TCP_PORT);

What does the connection helper prepare?

The helper creates a stream socket and prepares an IPv4 server address. It uses the same shared TCP port as the server.

  • Complete connect_to_server() by pasting this code directly below the port assignment:
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return -1;
    }

    if (connect(socket_fd, (struct sockaddr *)&server_address,
                sizeof(server_address)) < 0) {
        perror("connect");
        close(socket_fd);
        return -1;
    }

    return socket_fd;
}

When does the helper succeed?

The helper converts the loopback address and connects the socket. It returns the connected descriptor or -1 after cleaning up a failure.

  • Start main() below the connection helper by pasting this code:
int main(int argc, char *argv[]) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    const char *mode = argc > 1 ? argv[1] : "normal";
    const char *message = argc > 2 ? argv[2] : "hello over framed TCP";

    int socket_fd = connect_to_server();
    if (socket_fd < 0) {
        return 1;
    }

How does the client choose a test?

The first argument selects the mode. The second argument supplies a message for valid normal or split frames.

  • Add the truncated-frame mode below the connection check:
    if (strcmp(mode, "truncate") == 0) {
        uint32_t network_length = htonl(20);
        const char *partial = "short";
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0 ||
            send_all(socket_fd, partial, strlen(partial)) < 0) {
            perror("send truncated frame");
        }
        printf("Sent 5 payload bytes for a declared 20-byte frame\n");
        close(socket_fd);
        return 0;
    }

How is truncation produced?

This mode declares a 20-byte payload but sends only the 5 bytes in short. Closing the connection forces the server to classify the incomplete payload.

  • Add the oversized-frame mode below the truncated mode:
    if (strcmp(mode, "oversize") == 0) {
        uint32_t network_length = htonl(MAX_PAYLOAD + 1);
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0) {
            perror("send oversized length");
        }
        printf("Sent an oversized frame length\n");
        close(socket_fd);
        return 0;
    }

How is an invalid length produced?

This mode sends a declared length one byte above MAX_PAYLOAD. The server can reject the header without allocating or reading that payload.

  • Add the valid-mode and message-length checks below the oversized mode:
    if (strcmp(mode, "normal") != 0 && strcmp(mode, "split") != 0) {
        fprintf(stderr,
                "Usage: %s [normal|split|truncate|oversize] [message]\n",
                argv[0]);
        close(socket_fd);
        return 1;
    }

    size_t message_length = strlen(message);
    if (message_length == 0 || message_length > MAX_PAYLOAD) {
        fprintf(stderr, "Message must contain 1 to %d bytes\n", MAX_PAYLOAD);
        close(socket_fd);
        return 1;
    }

Which messages are allowed?

The remaining path accepts only normal or split mode. Its message must fit the protocol's 1-byte through MAX_PAYLOAD range.

  • Add the normal and split send paths below the message-length check:
    if (strcmp(mode, "split") == 0) {
        uint32_t network_length = htonl((uint32_t)message_length);
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0) {
            perror("send frame header");
            close(socket_fd);
            return 1;
        }
        printf("Sent the frame header; delaying the payload\n");
        sleep(1);
        if (send_all(socket_fd, message, message_length) < 0) {
            perror("send frame payload");
            close(socket_fd);
            return 1;
        }
    } else if (send_frame(socket_fd, message,
                          (uint32_t)message_length) < 0) {
        perror("send_frame");
        close(socket_fd);
        return 1;
    }

Why does split mode work now?

Split mode sends the valid header first and delays the payload for one second. The server waits for the declared payload length instead of treating the first available bytes as a complete message.

  • Complete the client by adding the framed reply handling below the send paths:
    char reply[MAX_PAYLOAD + 1];
    uint32_t reply_length = 0;
    int status = recv_frame(socket_fd, reply, MAX_PAYLOAD, &reply_length);
    if (status == FRAME_OK) {
        printf("TCP reply (%u bytes): %s\n", reply_length, reply);
    } else {
        fprintf(stderr, "TCP reply failed with frame status %d\n", status);
        close(socket_fd);
        return 1;
    }

    close(socket_fd);
    return 0;
}

How is the echo verified?

The client parses the response with the same frame rules as the server. A valid reply includes both the decoded byte count and the echoed message.

  • Save tcp_client.c.
  • Compile the framed server in one Terminal window by running:
clang tcp_server.c -o tcp_server

What does this command create?

Clang compiles tcp_server.c plus its included headers. The -o option names the executable tcp_server.

  • Confirm the command returns to the terminal prompt without compiler errors.
  • Compile the framed client in the other Terminal window by running:
clang tcp_client.c -o tcp_client

What does the second build create?

This command creates the tcp_client executable from tcp_client.c and the shared protocol header.

  • Confirm the second command returns to the terminal prompt without compiler errors.

Seeing compiler errors?

  • Check that tcp_protocol.h ends with one closing #endif.
  • Check that every opening brace in tcp_server.c and tcp_client.c has a matching closing brace.
  • Confirm all three new files sit beside common.h inside socket-lab.

Use the first file and line number in the compiler output to start debugging. Help me trace my Clang error in the framed TCP files.

Test valid and malformed frames

The strongest test includes both success and failure. A robust sequential server echoes valid frames, rejects malformed clients, and remains available afterward.

  • Start the framed server in the server Terminal by running:
./tcp_server

What should the server show?

You should see Framed TCP server listening on 127.0.0.1:9090. The process stays active while it waits for clients.

Before you run the split client, do you think the one-second gap will still cut the message at the header boundary?

  • Send a deliberately split frame from the client Terminal by running:
./tcp_client split "lab frame"

What did framing change?

You should see TCP reply (9 bytes): lab frame in the client Terminal. The server waits through the delay because the header declares a 9-byte payload.

The split exchange now succeeds. Your protocol uses the declared length instead of the sender's write boundaries.

  • Send the deliberately truncated frame from the client Terminal by running:
./tcp_client truncate

What should truncation reveal?

The client reports that it sent 5 payload bytes for a declared 20-byte frame. The server prints Rejected truncated TCP frame before returning to its accept loop.

  • Send an invalid oversized length from the client Terminal by running:
./tcp_client oversize

What should the length check reject?

The client reports an oversized frame length. The server prints Rejected invalid TCP frame length without waiting for an oversized payload.

Before the final check, do you think both malformed clients left the listening server usable?

  • Prove the server still accepts valid clients by running:
./tcp_client normal "still alive"

What proves recovery worked?

You should see TCP reply (11 bytes): still alive in the client Terminal. The server prints the message and returns to Ready for the next TCP client.

That's the core protocol working. Split transfers produce complete messages, while bad frames affect only their own connections.

Did a framed test fail?

  • Confirm the server Terminal still shows the listening process before each client test.
  • Check that the server and client were recompiled after your latest saved changes.
  • Confirm the framed server uses recv_frame() instead of a one-call receive path.

Share the mode plus both terminal outputs when asking for help. Help me diagnose a framed TCP test that produced the wrong server or client output.

✔️ Awesome, I've got everything!

Great. Your framed TCP server survives split transfers plus malformed clients. Confirm that tcp_protocol.h, tcp_server.c, and tcp_client.c are saved.

ⓧ I'd like to double check the full code

Compare your complete tcp_protocol.h file with this reference:

#ifndef TCP_PROTOCOL_H
#define TCP_PROTOCOL_H

#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <sys/socket.h>

#define EXACT_ERROR -1
#define EXACT_CLOSED 0
#define EXACT_OK 1
#define EXACT_TRUNCATED 2

#define FRAME_ERROR -1
#define FRAME_CLOSED 0
#define FRAME_OK 1
#define FRAME_TRUNCATED 2
#define FRAME_INVALID_LENGTH 3

static int send_all(int socket_fd, const void *buffer, size_t length) {
    const char *bytes = buffer;
    size_t sent_total = 0;

    while (sent_total < length) {
        ssize_t sent = send(socket_fd, bytes + sent_total,
                            length - sent_total, 0);
        if (sent > 0) {
            sent_total += (size_t)sent;
        } else if (sent < 0 && errno == EINTR) {
            continue;
        } else {
            return -1;
        }
    }

    return 0;
}

static int recv_exact(int socket_fd, void *buffer, size_t length) {
    char *bytes = buffer;
    size_t received_total = 0;

    while (received_total < length) {
        ssize_t received = recv(socket_fd, bytes + received_total,
                                length - received_total, 0);
        if (received > 0) {
            received_total += (size_t)received;
        } else if (received == 0) {
            return received_total == 0 ? EXACT_CLOSED : EXACT_TRUNCATED;
        } else if (errno == EINTR) {
            continue;
        } else {
            return EXACT_ERROR;
        }
    }

    return EXACT_OK;
}

static int send_frame(int socket_fd, const char *message, uint32_t length) {
    if (length == 0 || length > MAX_PAYLOAD) {
        return -1;
    }

    uint32_t network_length = htonl(length);
    if (send_all(socket_fd, &network_length, sizeof(network_length)) < 0) {
        return -1;
    }

    return send_all(socket_fd, message, length);
}

static int recv_frame(int socket_fd, char *message, uint32_t capacity,
                      uint32_t *length_out) {
    uint32_t network_length;
    int status = recv_exact(socket_fd, &network_length,
                            sizeof(network_length));

    if (status == EXACT_CLOSED) {
        return FRAME_CLOSED;
    }
    if (status == EXACT_TRUNCATED) {
        return FRAME_TRUNCATED;
    }
    if (status == EXACT_ERROR) {
        return FRAME_ERROR;
    }

    uint32_t length = ntohl(network_length);
    if (length == 0 || length > capacity) {
        return FRAME_INVALID_LENGTH;
    }

    status = recv_exact(socket_fd, message, length);
    if (status == EXACT_ERROR) {
        return FRAME_ERROR;
    }
    if (status != EXACT_OK) {
        return FRAME_TRUNCATED;
    }

    message[length] = '\0';
    *length_out = length;
    return FRAME_OK;
}

#endif

What should match in the protocol header?

Check the status constants plus all four helpers. Their order and return paths should match the reference exactly.

Compare your complete tcp_server.c file with this reference:

#include "tcp_protocol.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

    int reuse = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(server_fd);
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(server_fd);
        return 1;
    }

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    if (listen(server_fd, 4) < 0) {
        perror("listen");
        close(server_fd);
        return 1;
    }

    printf("Framed TCP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, TCP_PORT);

    for (;;) {
        int client_fd = accept(server_fd, NULL, NULL);
        if (client_fd < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("accept");
            break;
        }

        printf("TCP client connected\n");

        for (;;) {
            char message[MAX_PAYLOAD + 1];
            uint32_t message_length = 0;
            int status = recv_frame(client_fd, message, MAX_PAYLOAD,
                                    &message_length);

            if (status == FRAME_CLOSED) {
                printf("TCP client closed cleanly\n");
                break;
            }
            if (status == FRAME_TRUNCATED) {
                printf("Rejected truncated TCP frame\n");
                break;
            }
            if (status == FRAME_INVALID_LENGTH) {
                printf("Rejected invalid TCP frame length\n");
                break;
            }
            if (status == FRAME_ERROR) {
                perror("recv_frame");
                break;
            }

            printf("TCP message: %s\n", message);
            if (send_frame(client_fd, message, message_length) < 0) {
                perror("send_frame");
                break;
            }
        }

        close(client_fd);
        printf("Ready for the next TCP client\n");
    }

    close(server_fd);
    return 0;
}

What should match in the server?

Check the outer accept loop plus the inner frame loop. Every client outcome should lead back to the next accept attempt.

Compare your complete tcp_client.c file with this reference:

#include "tcp_protocol.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

static int connect_to_server(void) {
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(TCP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return -1;
    }

    if (connect(socket_fd, (struct sockaddr *)&server_address,
                sizeof(server_address)) < 0) {
        perror("connect");
        close(socket_fd);
        return -1;
    }

    return socket_fd;
}

int main(int argc, char *argv[]) {
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    const char *mode = argc > 1 ? argv[1] : "normal";
    const char *message = argc > 2 ? argv[2] : "hello over framed TCP";

    int socket_fd = connect_to_server();
    if (socket_fd < 0) {
        return 1;
    }

    if (strcmp(mode, "truncate") == 0) {
        uint32_t network_length = htonl(20);
        const char *partial = "short";
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0 ||
            send_all(socket_fd, partial, strlen(partial)) < 0) {
            perror("send truncated frame");
        }
        printf("Sent 5 payload bytes for a declared 20-byte frame\n");
        close(socket_fd);
        return 0;
    }

    if (strcmp(mode, "oversize") == 0) {
        uint32_t network_length = htonl(MAX_PAYLOAD + 1);
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0) {
            perror("send oversized length");
        }
        printf("Sent an oversized frame length\n");
        close(socket_fd);
        return 0;
    }

    if (strcmp(mode, "normal") != 0 && strcmp(mode, "split") != 0) {
        fprintf(stderr,
                "Usage: %s [normal|split|truncate|oversize] [message]\n",
                argv[0]);
        close(socket_fd);
        return 1;
    }

    size_t message_length = strlen(message);
    if (message_length == 0 || message_length > MAX_PAYLOAD) {
        fprintf(stderr, "Message must contain 1 to %d bytes\n", MAX_PAYLOAD);
        close(socket_fd);
        return 1;
    }

    if (strcmp(mode, "split") == 0) {
        uint32_t network_length = htonl((uint32_t)message_length);
        if (send_all(socket_fd, &network_length,
                     sizeof(network_length)) < 0) {
            perror("send frame header");
            close(socket_fd);
            return 1;
        }
        printf("Sent the frame header; delaying the payload\n");
        sleep(1);
        if (send_all(socket_fd, message, message_length) < 0) {
            perror("send frame payload");
            close(socket_fd);
            return 1;
        }
    } else if (send_frame(socket_fd, message,
                          (uint32_t)message_length) < 0) {
        perror("send_frame");
        close(socket_fd);
        return 1;
    }

    char reply[MAX_PAYLOAD + 1];
    uint32_t reply_length = 0;
    int status = recv_frame(socket_fd, reply, MAX_PAYLOAD, &reply_length);
    if (status == FRAME_OK) {
        printf("TCP reply (%u bytes): %s\n", reply_length, reply);
    } else {
        fprintf(stderr, "TCP reply failed with frame status %d\n", status);
        close(socket_fd);
        return 1;
    }

    close(socket_fd);
    return 0;
}

What should match in the client?

Check all four modes plus the shared framed reply path. The split mode must delay only the payload after sending a valid header.

Your TCP lab now has a protocol that survives split transfers and isolates malformed clients. Next, you'll build the UDP version and compare its preserved datagram boundaries with TCP's byte stream.

Build the UDP Datagram Version

Your framed TCP service now survives split transfers. It also rejects malformed clients without taking down the server.

UDP preserves one datagram for each receive operation. You will build a connectionless echo path that captures the sender address while accepting that a reply is not guaranteed.

In this step, get ready to:
  • Build a UDP server that receives one bounded datagram at a time.
  • Build a UDP client with normal or oversized datagram modes.
  • Run both modes to compare datagram boundaries with TCP framing.
Create and compile the UDP server

A UDP server uses SOCK_DGRAM to receive separate messages. The recvfrom() call also records the sender address that sendto() needs for the reply.

  • Create udp_server.c inside the socket-lab folder from the VS Code Explorer sidebar.
  • Paste this first section into udp_server.c:
#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

    int reuse = 1;
    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(socket_fd);
        return 1;
    }

What does this section do?

  • The headers provide the socket functions plus the IPv4 address types used by the server.
  • The socket() call creates an AF_INET datagram socket.
  • The SO_REUSEADDR option helps the server bind to its local port again after a restart.
  • Save udp_server.c.
  • Confirm the final visible line closes the setsockopt() error branch.
  • Append the address setup directly below that closing brace:
    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(UDP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(socket_fd);
        return 1;
    }

    printf("UDP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, UDP_PORT);

How does the server choose its address?

  • The sockaddr_in structure holds the local address family plus the port and address.
  • The htons() call stores UDP_PORT in network byte order.
  • The inet_pton() call converts 127.0.0.1 into its binary form.
  • The bind() call assigns the socket to local port 9091.
  • Save udp_server.c.
  • Confirm the last two lines print the UDP listening address and port.
  • Append the receive loop below the listening message:
    for (;;) {
        char message[MAX_PAYLOAD + 1];
        struct sockaddr_in client_address;
        socklen_t client_length = sizeof(client_address);

        ssize_t received = recvfrom(socket_fd, message, sizeof(message), 0,
                                    (struct sockaddr *)&client_address,
                                    &client_length);
        if (received < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("recvfrom");
            break;
        }

        if (received == 0 || received > MAX_PAYLOAD) {
            printf("Rejected empty or oversized UDP datagram\n");
            continue;
        }

How does the receive loop protect the server?

  • The receive buffer has space for MAX_PAYLOAD + 1 bytes. That extra byte lets the server detect a 1025-byte datagram instead of silently treating it as valid.
  • The recvfrom() call retrieves one complete datagram. It also writes the sender details into client_address.
  • The EINTR branch retries a receive that was interrupted before data arrived.
  • The length check rejects empty datagrams or messages above MAX_PAYLOAD.
  • Save udp_server.c.
  • Confirm the rejection branch returns to the top of the receive loop with continue.
  • Append the echo path below the rejection branch:
        message[received] = '\0';
        printf("UDP datagram (%zd bytes): %s\n", received, message);

        ssize_t sent = sendto(socket_fd, message, (size_t)received, 0,
                              (struct sockaddr *)&client_address,
                              client_length);
        if (sent < 0) {
            perror("sendto");
        } else if (sent != received) {
            fprintf(stderr, "UDP reply size mismatch\n");
        }
    }

    close(socket_fd);
    return 0;
}

How does the echo reach the right client?

  • The server adds a null terminator after the received bytes so printf() can display the payload as text.
  • The sendto() call uses the captured client_address to return the datagram to its sender.
  • The final check catches a send failure or a reply length mismatch.
  • Save udp_server.c.
  • Compile the UDP server in the first Terminal window from earlier by running:
clang udp_server.c -o udp_server

What does this command do?

The clang compiler builds udp_server.c and writes the executable to udp_server. A successful build returns you to the prompt without compiler errors.

The Terminal prompt returns without an error when the server compiles successfully.

Is the UDP server failing to compile?

  • Confirm udp_server.c is saved inside the open socket-lab folder.
  • Compare the braces around the receive loop with the full file below.
  • Check that common.h still defines UDP_PORT and MAX_PAYLOAD.

Help me diagnose my UDP server compilation error.

✔️ Awesome, I've got everything!

Your udp_server.c file is saved. The udp_server executable also compiles without errors.

ⓧ I'd like to double check the full code

Compare your server with the complete udp_server.c file:

#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

    int reuse = 1;
    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt");
        close(socket_fd);
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(UDP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS, &address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind");
        close(socket_fd);
        return 1;
    }

    printf("UDP server listening on %s:%d\n",
           LOOPBACK_ADDRESS, UDP_PORT);

    for (;;) {
        char message[MAX_PAYLOAD + 1];
        struct sockaddr_in client_address;
        socklen_t client_length = sizeof(client_address);

        ssize_t received = recvfrom(socket_fd, message, sizeof(message), 0,
                                    (struct sockaddr *)&client_address,
                                    &client_length);
        if (received < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("recvfrom");
            break;
        }

        if (received == 0 || received > MAX_PAYLOAD) {
            printf("Rejected empty or oversized UDP datagram\n");
            continue;
        }

        message[received] = '\0';
        printf("UDP datagram (%zd bytes): %s\n", received, message);

        ssize_t sent = sendto(socket_fd, message, (size_t)received, 0,
                              (struct sockaddr *)&client_address,
                              client_length);
        if (sent < 0) {
            perror("sendto");
        } else if (sent != received) {
            fprintf(stderr, "UDP reply size mismatch\n");
        }
    }

    close(socket_fd);
    return 0;
}
Create and compile the UDP client

The client sends one bounded datagram to the server. Its separate oversize mode sends MAX_PAYLOAD + 1 bytes so you can test the server's rejection path.

  • Create udp_client.c inside the socket-lab folder from the VS Code Explorer sidebar.
  • Paste this first section into udp_client.c:
#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(UDP_PORT);

What does the client setup do?

  • The client creates its own SOCK_DGRAM socket for UDP messages.
  • The server_address structure identifies the destination for every outgoing datagram.
  • The htons() call converts UDP_PORT to network byte order.
  • Save udp_client.c.
  • Confirm the final line assigns UDP_PORT to server_address.sin_port.
  • Append the address conversion and oversized test mode below that line:
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (argc > 1 && strcmp(argv[1], "oversize") == 0) {
        char oversized[MAX_PAYLOAD + 1];
        memset(oversized, 'X', sizeof(oversized));
        if (sendto(socket_fd, oversized, sizeof(oversized), 0,
                   (struct sockaddr *)&server_address,
                   sizeof(server_address)) < 0) {
            perror("sendto oversized datagram");
            close(socket_fd);
            return 1;
        }
        printf("Sent a %zu-byte UDP datagram\n", sizeof(oversized));
        close(socket_fd);
        return 0;
    }

How does oversized mode work?

  • The inet_pton() call sets the destination to the shared loopback address.
  • The strcmp() check activates the test when the first argument is oversize.
  • The oversized array contains 1025 copies of X.
  • The client sends that array as one datagram before closing its socket.
  • Save udp_client.c.
  • Confirm the oversized branch closes the socket before returning.
  • Append the normal message validation and send path below the oversized branch:
    const char *message = argc > 1 ? argv[1] : "hello over UDP";
    size_t message_length = strlen(message);
    if (message_length == 0 || message_length > MAX_PAYLOAD) {
        fprintf(stderr, "Message must contain 1 to %d bytes\n", MAX_PAYLOAD);
        close(socket_fd);
        return 1;
    }

    if (sendto(socket_fd, message, message_length, 0,
               (struct sockaddr *)&server_address,
               sizeof(server_address)) < 0) {
        perror("sendto");
        close(socket_fd);
        return 1;
    }

How does the normal send stay bounded?

  • The first command-line argument becomes the message. The built-in default is hello over UDP.
  • The length check accepts payloads from 1 through MAX_PAYLOAD bytes.
  • The sendto() call sends the entire message as one datagram to server_address.
  • Save udp_client.c.
  • Confirm the send path closes the socket when sendto() fails.
  • Append the reply receive path below the send branch:
    char reply[MAX_PAYLOAD + 1];
    ssize_t received;
    do {
        received = recvfrom(socket_fd, reply, MAX_PAYLOAD, 0, NULL, NULL);
    } while (received < 0 && errno == EINTR);

    if (received < 0) {
        perror("recvfrom");
        close(socket_fd);
        return 1;
    }

    reply[received] = '\0';
    printf("UDP reply (%zd bytes): %s\n", received, reply);

    close(socket_fd);
    return 0;
}

How does the client receive its reply?

  • The client asks recvfrom() for up to MAX_PAYLOAD reply bytes.
  • The loop retries when an interrupted receive reports EINTR.
  • The client null-terminates the reply before printing the complete datagram.
  • Save udp_client.c.
  • Compile the UDP client in the second Terminal window from earlier by running:
clang udp_client.c -o udp_client

What does this command do?

The clang compiler builds udp_client.c and writes the executable to udp_client. A successful build returns you to the prompt without compiler errors.

The second Terminal prompt returns without an error when the client compiles successfully.

Is the UDP client failing to compile?

  • Confirm udp_client.c is saved inside the open socket-lab folder.
  • Check that the closing brace for the oversize branch appears before the normal message path.
  • Compare the parentheses around both sendto() calls with the full file below.

Help me diagnose my UDP client compilation error.

✔️ Awesome, I've got everything!

Your udp_client.c file is saved. The udp_client executable also compiles without errors.

ⓧ I'd like to double check the full code

Compare your client with the complete udp_client.c file:

#include "common.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in server_address;
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(UDP_PORT);
    if (inet_pton(AF_INET, LOOPBACK_ADDRESS,
                  &server_address.sin_addr) != 1) {
        fprintf(stderr, "Invalid loopback address\n");
        close(socket_fd);
        return 1;
    }

    if (argc > 1 && strcmp(argv[1], "oversize") == 0) {
        char oversized[MAX_PAYLOAD + 1];
        memset(oversized, 'X', sizeof(oversized));
        if (sendto(socket_fd, oversized, sizeof(oversized), 0,
                   (struct sockaddr *)&server_address,
                   sizeof(server_address)) < 0) {
            perror("sendto oversized datagram");
            close(socket_fd);
            return 1;
        }
        printf("Sent a %zu-byte UDP datagram\n", sizeof(oversized));
        close(socket_fd);
        return 0;
    }

    const char *message = argc > 1 ? argv[1] : "hello over UDP";
    size_t message_length = strlen(message);
    if (message_length == 0 || message_length > MAX_PAYLOAD) {
        fprintf(stderr, "Message must contain 1 to %d bytes\n", MAX_PAYLOAD);
        close(socket_fd);
        return 1;
    }

    if (sendto(socket_fd, message, message_length, 0,
               (struct sockaddr *)&server_address,
               sizeof(server_address)) < 0) {
        perror("sendto");
        close(socket_fd);
        return 1;
    }

    char reply[MAX_PAYLOAD + 1];
    ssize_t received;
    do {
        received = recvfrom(socket_fd, reply, MAX_PAYLOAD, 0, NULL, NULL);
    } while (received < 0 && errno == EINTR);

    if (received < 0) {
        perror("recvfrom");
        close(socket_fd);
        return 1;
    }

    reply[received] = '\0';
    printf("UDP reply (%zd bytes): %s\n", received, reply);

    close(socket_fd);
    return 0;
}
Run and compare the datagram tests

The server and client are ready to exchange one complete message per datagram. You will first prove the echo path works before testing the payload limit.

  • Start the server in the first Terminal window by running:
./udp_server

What should the server show?

You will see UDP server listening on 127.0.0.1:9091. This Terminal stays occupied while the receive loop waits for datagrams.

Before you run the client, do you expect the server to need a 4-byte length header to identify the end of this message?

  • Send the normal message from the second Terminal window by running:
./udp_client "hello over UDP"

What does the normal test prove?

The client prints UDP reply (14 bytes): hello over UDP. The server prints UDP datagram (14 bytes): hello over UDP.

The single recvfrom() call returns the complete datagram. No separate length header is needed to recover its boundary.

Did the normal client fail to receive a reply?

  • Confirm the first Terminal still shows the UDP server listening on port 9091.
  • Confirm both Terminal windows are using the socket-lab folder from earlier.
  • Stop any other local program that already occupies port 9091.

Help me debug the missing UDP reply.

Before you run the next test, do you expect the server to echo a 1025-byte datagram or reject it?

  • Send the oversized datagram from the second Terminal window by running:
./udp_client oversize

What does the oversized test prove?

The client prints Sent a 1025-byte UDP datagram. The server prints Rejected empty or oversized UDP datagram.

The server returns to recvfrom() after the rejection. It remains ready for another datagram.

How does this compare with TCP framing?

  • UDP delivers one datagram to each recvfrom() operation. The transport preserves that message boundary.
  • TCP exposes an ordered byte stream through recv(). Your framed protocol uses recv_exact() to collect the declared byte count.
  • A successful UDP send does not guarantee that a reply arrives. The current client waits in recvfrom() for that reply.

That completes the transport comparison. You now have a framed TCP service plus a UDP service that preserves each bounded datagram.

Secret mission

Add a UDP Timeout and Retry

A missing UDP reply currently leaves your client waiting forever. Add readiness polling with a 1,500 millisecond timeout. Retry the request once before reporting a visible failure.

Clean Up Your Resources

Clean Up Your Resources

Everything in this lab stays on your Mac with no ongoing service costs. Decide whether to keep the lab ready, pause its local processes, or delete its files.

Resources you used:

  • The socket-lab folder open in Visual Studio Code with your source files and compiled programs.
  • Any TCP or UDP process still running in macOS Terminal.

Keep everything running

No cleanup is required. Choose this option if you plan to repeat the framing tests or experiment with the UDP timeout.

  • Keep the socket-lab folder available for future tests.
  • Keep the source files with their compiled programs.
  • Leave a server process active only while you are using its local port.

Pause - I'll come back to this later

Pausing releases the local ports used by the servers. Your files remain inside socket-lab for another session.

  • Return to the first Terminal window from earlier.
  • Press Control+C if a server or client process is still running.
  • Return to the second Terminal window from earlier.
  • Press Control+C if a server or client process is still running.
  • Confirm that both Terminal windows show a shell prompt.

Both local server processes are now stopped. Your complete lab remains ready to compile or run again.

Delete - I don't want to use this again

Deletion removes the complete local lab after its processes stop. Files stored outside socket-lab remain safe.

  • Return to each Terminal window from earlier.
  • Press Control+C in any window that is still running a server or client.
  • Confirm that each Terminal window shows a shell prompt.
  • Return to a Terminal window whose current folder is socket-lab.
  • Move to the folder containing socket-lab by running this command:
cd ..

What Does This Command Do?

This moves the Terminal out of socket-lab into its containing folder. The project folder can now be removed as one unit.

  • Confirm that this location contains socket-lab by running this command:
ls

Check the Target Folder

You should see socket-lab in the output. This confirms that the deletion command targets the folder created for this project.

Deletion Is Permanent

The next command permanently removes every source file and compiled program inside socket-lab. Your Mac does not place these files in the Trash.

  • Delete the complete lab folder by running this command:
rm -rf socket-lab

What Did This Remove?

This removes the socket-lab folder with its TCP files and UDP files. It also removes the headers and compiled programs stored inside.

  • Verify that the folder is gone by running this command:
ls

Confirm the Cleanup

The output should no longer include socket-lab. That completes the cleanup: your local message lab files are gone.

Still See the Folder?

  • Confirm that the Terminal is in the folder containing socket-lab.
  • Check that the folder name uses the exact spelling socket-lab.
  • Ask for help diagnosing the remaining folder.

Nice Work!

Nice Work!

You did it! Your local programs now reveal the practical difference: TCP byte streams versus UDP datagrams.

What you learned:

  • Reproduce the TCP message-boundary bug with a deliberately split send. The naive server echoes only HELLO. This proves one recv() call cannot define a complete message.
  • Build a 4-byte length-prefixed TCP protocol that survives split transfers. Test truncated frames. Test oversized lengths. Confirm the sequential server keeps accepting clients.
  • Build a UDP echo service that receives one bounded datagram per recvfrom() call. Reject the 1,025-byte oversized datagram. Echo valid messages intact.
  • Secret Mission: Add a timed UDP receive with one retry. The client now exits after two timeouts instead of hanging when no reply arrives.

Ready to quiz yourself?