Sockets Introduction Stream Sockets: Complete Server and Client

 

Chapter 56: Sockets Introduction
Part 3 of 5 โ€” Stream Sockets: Complete Server and Client
๐Ÿ–ฅ๏ธ TCP Server
๐Ÿ’ป TCP Client
๐Ÿ“– read/write

Stream Sockets in Practice

SOCK_STREAM provides a reliable, ordered, bidirectional byte stream. Once two sockets are connected, data flows like water through a pipe โ€” what one end writes, the other end reads, byte for byte, in the correct order.

This is the model used by HTTP (web), SSH, FTP, SMTP (email), and most TCP-based protocols. In embedded systems it is also used for socket-based IPC between processes on the same device using AF_UNIX stream sockets.

This part shows a complete working server and client. The example is a simple echo server โ€” it reads whatever the client sends and writes it straight back.

Key Terms in This Part

SOCK_STREAM read() / write() half-close shutdown() partial read partial write EOF detection fork-per-client

Reading and Writing on Stream Sockets

Once a stream connection is established, both sides use standard read() and write() calls on the socket file descriptor. No special socket calls are needed for basic data transfer.

/* Writing data to a stream socket */
ssize_t write(int sockfd, const void *buf, size_t count);
/* Returns number of bytes written, or -1 on error */

/* Reading data from a stream socket */
ssize_t read(int sockfd, void *buf, size_t count);
/* Returns number of bytes read, 0 on EOF (peer closed), -1 on error */

Important behaviour of read() on stream sockets:

  • read() may return fewer bytes than requested (partial read). This is not an error โ€” stream sockets do not preserve message boundaries.
  • read() returning 0 means the peer has closed its end of the connection (EOF). You should stop reading.
  • read() returns -1 on error, with errno set.

Because of partial reads, a robust application wraps read() in a loop. The same applies to write() which may also write fewer bytes than requested on a non-blocking socket or when the send buffer is full.

/* Reliable read โ€” keeps reading until all n bytes are received */
ssize_t read_all(int fd, void *buf, size_t n)
{
    size_t total = 0;
    ssize_t nr;
    char *ptr = (char *)buf;

    while (total < n) {
        nr = read(fd, ptr + total, n - total);
        if (nr == 0)
            return total;   /* EOF โ€” connection closed by peer */
        if (nr == -1)
            return -1;      /* error */
        total += nr;
    }
    return total;
}

/* Reliable write โ€” keeps writing until all n bytes are sent */
ssize_t write_all(int fd, const void *buf, size_t n)
{
    size_t total = 0;
    ssize_t nw;
    const char *ptr = (const char *)buf;

    while (total < n) {
        nw = write(fd, ptr + total, n - total);
        if (nw == -1)
            return -1;      /* error */
        total += nw;
    }
    return total;
}

Closing a Connection: close() and shutdown()

When you close() a socket, the connection is terminated from your side. The peer will see read() return 0 (EOF). However, close() on a socket decrements the file descriptor reference count โ€” if the count is still positive (e.g., after fork()), the connection is NOT actually torn down yet.

shutdown() gives finer control. It shuts down part of a full-duplex connection without closing the file descriptor:

#include <sys/socket.h>

int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */

/* how can be: */
/* SHUT_RD   - stop receiving (reads return 0) */
/* SHUT_WR   - stop sending (peer sees EOF)    */
/* SHUT_RDWR - both directions (same as close but doesn't release fd) */

Half-Close with shutdown(SHUT_WR)
Client
Finishes sending all data
calls shutdown(SHUT_WR)
โ€” signals “done sending”
Still can READ responses
โ†’ data โ†’
โ† response โ†
โ†’ FIN (shutdown) โ†’
Server
Receives FIN
read() returns 0 (EOF)
Sends final response

The half-close (shutdown with SHUT_WR) is useful for protocols like HTTP/1.0 where the client sends a full request, signals “done”, and then waits for the complete response. The server can detect end-of-request by seeing EOF on the read side.

/* Example: client sends all data then half-closes the write side */
write_all(sockfd, request, request_len);
shutdown(sockfd, SHUT_WR);  /* signal to server that request is complete */

/* Now read the full server response */
while ((n = read(sockfd, buf, sizeof(buf))) > 0) {
    /* process response */
}
close(sockfd);

Complete TCP Echo Server

The server below listens on port 7777, accepts one client at a time, and echoes back whatever the client sends. Each line ending with ‘\n’ is read as a message.

/* tcp_echo_server.c โ€” Simple iterative TCP echo server */

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

#define PORT    7777
#define BACKLOG 5
#define BUFSIZE 1024

/* Echo all data received on client_fd back to the client */
static void handle_client(int client_fd)
{
    char buf[BUFSIZE];
    ssize_t nr;

    printf("Handling client...\n");

    while ((nr = read(client_fd, buf, sizeof(buf))) > 0) {
        /* Echo exactly what we received */
        if (write(client_fd, buf, nr) != nr) {
            perror("write");
            break;
        }
    }

    if (nr == -1)
        perror("read");

    printf("Client disconnected\n");
    close(client_fd);
}

int main(void)
{
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addrlen;
    int opt = 1;
    char client_ip[INET_ADDRSTRLEN];

    /* Step 1: Create a TCP socket */
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Allow address reuse (avoids EADDRINUSE on restart) */
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Step 3: Bind to port 7777 on all interfaces */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    /* Step 4: Start listening */
    if (listen(listen_fd, BACKLOG) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Echo server listening on port %d\n", PORT);

    /* Step 5: Accept loop */
    for (;;) {
        client_addrlen = sizeof(client_addr);
        client_fd = accept(listen_fd,
                           (struct sockaddr *)&client_addr,
                           &client_addrlen);
        if (client_fd == -1) {
            perror("accept");
            continue;  /* non-fatal, try next client */
        }

        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("Accepted connection from %s:%d\n",
               client_ip, ntohs(client_addr.sin_port));

        handle_client(client_fd);
    }

    /* Unreachable, but good practice */
    close(listen_fd);
    return 0;
}

Build and run:

gcc -Wall -o tcp_echo_server tcp_echo_server.c
./tcp_echo_server

Complete TCP Echo Client

The client below connects to the server, reads lines from stdin, sends them to the server, and prints the echoed response.

/* tcp_echo_client.c โ€” Simple TCP echo client */

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

#define SERVER_IP "127.0.0.1"
#define PORT      7777
#define BUFSIZE   1024

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;
    char send_buf[BUFSIZE];
    char recv_buf[BUFSIZE];
    ssize_t nr;

    /* Step 1: Create TCP socket */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Set up server address */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(PORT);

    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        exit(EXIT_FAILURE);
    }

    /* Step 3: Connect to the server */
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    printf("Connected to %s:%d\n", SERVER_IP, PORT);
    printf("Type messages (Ctrl+D to exit):\n");

    /* Step 4: Read from stdin, send to server, print echo */
    while (fgets(send_buf, sizeof(send_buf), stdin) != NULL) {
        size_t len = strlen(send_buf);

        /* Send the line to server */
        if (write(sockfd, send_buf, len) != (ssize_t)len) {
            perror("write");
            break;
        }

        /* Read the echo back */
        nr = read(sockfd, recv_buf, len);
        if (nr <= 0)
            break;

        recv_buf[nr] = '\0';
        printf("Echo: %s", recv_buf);
    }

    printf("Closing connection\n");
    close(sockfd);
    return 0;
}
/* Build and test both together */
gcc -Wall -o tcp_echo_server tcp_echo_server.c
gcc -Wall -o tcp_echo_client tcp_echo_client.c

/* Terminal 1 - start server */
./tcp_echo_server

/* Terminal 2 - connect client */
./tcp_echo_client
/* Type: hello world */
/* See: Echo: hello world */

Concurrent Server โ€” Handling Multiple Clients with fork()

The iterative server above handles one client at a time. To serve multiple clients simultaneously, the classic approach is to fork() a child process for each accepted connection. The parent closes its copy of client_fd and loops back to accept(). Each child handles one client independently.

/* Concurrent echo server โ€” fork per client */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT    7777
#define BACKLOG 10
#define BUFSIZE 1024

/* Reap zombie children automatically */
static void sigchld_handler(int sig)
{
    (void)sig;
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

static void handle_client(int client_fd)
{
    char buf[BUFSIZE];
    ssize_t nr;

    while ((nr = read(client_fd, buf, sizeof(buf))) > 0)
        write(client_fd, buf, nr);

    close(client_fd);
    exit(EXIT_SUCCESS);  /* child exits after serving client */
}

int main(void)
{
    int listen_fd, client_fd;
    struct sockaddr_in server_addr;
    int opt = 1;
    struct sigaction sa;

    /* Reap zombies */
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    listen(listen_fd, BACKLOG);

    printf("Concurrent server listening on port %d\n", PORT);

    for (;;) {
        client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd == -1) {
            perror("accept");
            continue;
        }

        switch (fork()) {
        case -1:
            perror("fork");
            close(client_fd);
            break;

        case 0:
            /* Child process: close listen_fd (not needed in child) */
            close(listen_fd);
            handle_client(client_fd);
            /* handle_client calls exit() โ€” never reaches here */

        default:
            /* Parent process: close client_fd (child has it) */
            close(client_fd);
            break;
        }
    }

    return 0;
}

Fork-per-Client Architecture
Parent Process
listen_fd open
loops on accept()
forks for each client
โ†’ fork() โ†’ child1 (client A)
โ†’ fork() โ†’ child2 (client B)
โ†’ fork() โ†’ child3 (client C)
child1
handles client A
exits when done
child2
handles client B
exits when done

Important: after fork(), the parent must close client_fd (it has its own copy after fork). The child must close listen_fd (it does not need to accept new connections). Failing to close these leads to file descriptor leaks and the server not properly detecting client disconnection.

Interview Questions โ€” Stream Sockets

Q1. Why might read() on a stream socket return fewer bytes than requested?

Stream sockets are byte-stream oriented โ€” they do not preserve message boundaries. The kernel delivers data as it arrives from the network. A single send() on the sender may result in multiple recv()/read() calls on the receiver, each returning a fragment of the data. The application must loop until all expected bytes are received.

Q2. How do you detect that the remote peer has closed its end of a TCP connection?

read() returns 0 (EOF). This happens when the peer calls close() or shutdown(SHUT_WR). The application should stop reading and close its own socket. A return of 0 is different from -1 (which indicates an error like ECONNRESET โ€” connection forcibly reset).

Q3. What is the difference between close() and shutdown() on a socket?

close() decrements the file descriptor reference count. The connection is terminated only when the count reaches zero. shutdown() immediately affects the connection regardless of the reference count, and can selectively shut down just the read or write direction. shutdown() is useful after fork() to ensure the peer sees EOF even though the parent still holds the fd.

Q4. In a fork-per-client server, why must the parent close client_fd after fork()?

After fork(), both parent and child have a copy of client_fd. The client’s connection will not see EOF (read returning 0) until all copies of the fd are closed. If the parent keeps client_fd open, even after the child exits and closes its copy, the client’s connection remains open. The parent must close client_fd immediately after fork() so only the child holds it.

Q5. What is SO_REUSEADDR and when is it necessary?

SO_REUSEADDR allows a new server to bind to a port even if that port is still in TIME_WAIT state from a previous server instance. Without it, restarting a server after a crash may fail with EADDRINUSE for up to 2 minutes (the TIME_WAIT duration). Always set it before bind() in server code.

Q6. What happens if write() returns fewer bytes than the size you passed?

For blocking sockets this is rare but possible when the socket send buffer is full. The fix is to loop write() until all bytes are sent (a write_all helper). For non-blocking sockets, a short write (or EAGAIN) is expected and the application must use an event loop to retry when the socket becomes writable again.

Q7. How does a stream socket differ from a Unix pipe?

Both provide a byte stream but sockets are bidirectional (both ends can read and write) while a pipe is unidirectional (one end writes, the other reads). Sockets can work across the network (AF_INET) while pipes are local-only. Sockets support explicit connection setup (connect/accept) while pipes connect two related processes through inheritance.

Next: Part 4 โ€” Datagram Sockets: sendto() and recvfrom()

Learn connectionless UDP-style communication with SOCK_DGRAM.

Leave a Reply

Your email address will not be published. Required fields are marked *