Fundamentals of TCP and UDP Network Protocols and Socket Programming

Network Architecture Models

Data transmission across networks is structured using layered reference models. The theoretical Open Systems Interconnection (OSI) framework defines seven distinct layers: Physical, Data Link, Network, Transport, Session, Presentation, and Application. During transmission, data moves downward, with each lower layer providing services to the layer directly above it. In practical deployments, the TCP/IP four-layer suite is the industry standard:

  • Application Layer: Handles high-level protocols and application-specific logic (e.g., HTTP, FTP, SSH, SMTP).
  • Transport Layer: Manages end-to-end communication, flow control, and reliability (TCP, UDP).
  • Internet (Network) Layer: Responsible for logical addressing, routing decisions, and packet forwarding across disparate networks (IPv4/IPv6, ICMP).
  • Link (Network Interface) Layer: Interfaces with hardware drivers and network interface cards, handling frame construction, MAC addressing, and physical bitstream conversion.

Protocol Encapsulation

As application payloads descend through the protocol stack, each layer performs encapsulation. The receiving layer appends its own header (and occasionally a trailer) to the payload from the upper layer. This metadata contains routing information, sequencing parameters, and error-checking values. The encapsulated units then passed to the lower layer until it reaches the physical medium for transmission. Upon receipt, the reverse process (decapsulation) strips each header to deliver the original payload to the target application.

Anatomy of a TCP Segment Header

Transmission Control Protocol (TCP) guarantees reliable, ordered, and error-checked delivery. Its header structure contains critical state management fields:

  • Source & Destination Ports: Combined with IP addresses, these 16-bit values uniquely identify a communication session.
  • Sequence Number: 32-bit value tracking the first byte of data in the segment.
  • Acknowledgment Number: 32-bit value indicating the next expected byte. Valid only when the ACK flag is set.
  • Data Offset (Header Length): 4-bit field specifying the header size in 32-bit words (maximum 60 bytes).
  • Control Flags:
    • SYN: Synchronize sequence numbers (connection initiation).
    • ACK: Acknowledgment of received data.
    • FIN: Connection termination.
    • RST: Reset the connection immediately.
    • PSH: Push buffered data to the receiving application.
    • URG: Indicates urgent pointer is active.
  • Window Size: 16-bit flow control field specifying the receiver's available buffer capacity (max 65,535 bytes).
  • Checksum: Mandatory 16-bit field verifying header and payload integrity. Calculated by the sender and validated by the receiver.
  • Urgent Pointer: 16-bit offset marking the end of urgent data.
  • Options & Padding: Variable-length fields aligned to 32 bits. The Maximum Segment Size (MSS) is commonly negotiated here to prevent IP fragmentation. Defaults to 536 bytes if omitted.

Connection Lifecycle

Three-Way Handshake (Establishment)

TCP connections follow a strict deterministic state machine to synchronize sequence numbers before data exchange:

  1. SYN: The client transmits a segment with SYN=1 and an initial sequence number seq=x. State transitions to SYN_SENT.
  2. SYN-ACK: The server acknowledges the request by replying with SYN=1 and ACK=1. It sets its sequence number seq=y and acknowledgment number ack=x+1. State shifts to SYN_RCVD.
  3. ACK: The client confirms receipt by sending a segment with ACK=1, seq=x+1, and ack=y+1. Both endpoints transition to ESTABLISHED, enabling bidirectional data flow. The monotonically increasing sequence numbers ensure segment ordering and packet loss detection.

Four-Way Termination

Because TCP connections are full-duplex, each direction must be closed independently:

  1. Active FIN: The initiating host sends a segment with FIN=1 and a sequence number seq=u. It enters FIN_WAIT_1.
  2. Passive ACK: The receiving host replies with ACK=1, ack=u+1, and moves to CLOSE_WAIT while draining remaining outbound buffers.
  3. Passive FIN: Once application data is fully transmitted, the receiver sends its own FIN=1 segment with seq=v and enters LAST_ACK.
  4. Final ACK: The initiator acknowledges with ACK=1, ack=v+1, transitions through TIME_WAIT to absorb delayed packets, and finally reaches CLOSED.

UDP Socket Implemantation Paradigm

User Datagram Protocol (UDP) operates as a connectionless, best-effort datagram service. It omits handshakes, sequencing, and retransmission mechanisms, resulting in lower latency and reduced overhead. This makes it suitable for real-time streaming, DNS queries, or local network environments where application-layer reliability suffices.

Key differences from TCP sockets:

  • Socket type: SOCK_DGRAM replaces SOCK_STREAM.
  • Addressing: Functions like sendto() and recvfrom() explicitly specify destination and source addresses per packet.
  • Workflow: Bypasses listen(), accept(), and explicit connection binding.

Server Implementation (Datagram Handler)

The server binds to a specific port, enters an infinite loop to ingest datagrams, processes the payload (converting text to uppercase), and routes the response back to the originating client address.

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

#define SERVICE_PORT 8800
#define MAX_PAYLOAD 1024

void transform_to_upper(char *data) {
    for (int idx = 0; data[idx] != '\0'; idx++) {
        data[idx] = toupper(data[idx]);
    }
}

int main(void) {
    int udp_listener = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_listener < 0) {
        perror("Failed to create datagram socket");
        return EXIT_FAILURE;
    }

    struct sockaddr_in bind_config = {0};
    bind_config.sin_family = AF_INET;
    bind_config.sin_addr.s_addr = htonl(INADDR_ANY);
    bind_config.sin_port = htons(SERVICE_PORT);

    if (bind(udp_listener, (struct sockaddr*)&bind_config, sizeof(bind_config)) < 0) {
        perror("Port binding failed");
        close(udp_listener);
        return EXIT_FAILURE;
    }

    printf("UDP endpoint listening on port %d\n", SERVICE_PORT);

    struct sockaddr_in peer_info;
    socklen_t addr_size = sizeof(peer_info);
    char buffer[MAX_PAYLOAD];

    while (1) {
        ssize_t rx_count = recvfrom(udp_listener, buffer, sizeof(buffer), 0,
                                    (struct sockaddr*)&peer_info, &addr_size);
        
        if (rx_count <= 0) {
            if (rx_count < 0) perror("Datagram reception failed");
            continue;
        }

        buffer[rx_count] = '\0';
        printf("Received %zd bytes from %s: %s\n", rx_count, 
               inet_ntoa(peer_info.sin_addr), buffer);

        transform_to_upper(buffer);

        ssize_t tx_count = sendto(udp_listener, buffer, strlen(buffer) + 1, 0,
                                  (struct sockaddr*)&peer_info, addr_size);
        if (tx_count < 0) {
            perror("Datagram transmission failed");
        }
    }

    close(udp_listener);
    return 0;
}

Client Implementation (Request/Response)

The client constructs a destination endpoint, dispatches a string payload to the server, and blocks until the echoed response arrives. The response is validated and printed to standard output.

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

#define TARGET_PORT 8800
#define TARGET_IP "127.0.0.1"
#define RX_BUFFER_SIZE 1024

int main(void) {
    int comm_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (comm_fd < 0) {
        perror("Socket initialization error");
        return EXIT_FAILURE;
    }

    struct sockaddr_in remote_endpoint = {0};
    remote_endpoint.sin_family = AF_INET;
    remote_endpoint.sin_port = htons(TARGET_PORT);
    remote_endpoint.sin_addr.s_addr = inet_addr(TARGET_IP);

    char request_payload[] = "socket communication test";
    char response_payload[RX_BUFFER_SIZE];
    socklen_t endpoint_len = sizeof(remote_endpoint);

    ssize_t dispatch_bytes = sendto(comm_fd, request_payload, 
                                    strlen(request_payload) + 1, 0,
                                    (struct sockaddr*)&remote_endpoint, 
                                    sizeof(remote_endpoint));
    if (dispatch_bytes < 0) {
        perror("Data dispatch failed");
        close(comm_fd);
        return EXIT_FAILURE;
    }
    printf("Dispatched %zd bytes to %s:%d\n", dispatch_bytes, TARGET_IP, TARGET_PORT);

    ssize_t receive_bytes = recvfrom(comm_fd, response_payload, 
                                     sizeof(response_payload), 0,
                                     (struct sockaddr*)&remote_endpoint, 
                                     &endpoint_len);
    if (receive_bytes < 0) {
        perror("Data retrieval failed");
    } else {
        response_payload[receive_bytes] = '\0';
        printf("Server response [%zd bytes]: %s\n", receive_bytes, response_payload);
    }

    close(comm_fd);
    return 0;
}

Tags: transport-layer tcp-handshake udp-datagrams c-sockets network-protocols

Posted on Fri, 15 May 2026 18:54:34 +0000 by Gath