Skip to content

Network Sockets in C

Time: 30 min | Prerequisites: Processes and IPC, Threads and Synchronization | Theory companion: Linux Fundamentals, Section 1.3


Learning Objectives

By the end of this tutorial you will be able to:

  • Explain the difference between TCP and UDP sockets
  • Build a TCP chat server and client using socket(), bind(), listen(), accept(), connect()
  • Map socket operations to the pipe and file descriptor patterns you already know
Before You Start

All exercises run on any Linux machine — your host laptop (Ubuntu, Fedora, Arch, etc.) or the Raspberry Pi via SSH. You need gcc installed:

gcc --version           # should print version info
mkdir -p ~/sockets
cd ~/sockets

This tutorial uses pthreads for the chat application. You should be comfortable with pthread_create() and pthread_join() from the Threads and Synchronization tutorial.


1. TCP vs UDP

Pipes and shared memory work between processes on the same machine. To communicate between machines (your laptop and the RPi, or two laptops), you need sockets.

A socket is a file descriptor — you read() and write() just like pipes, but the data travels over the network.

TCP UDP
Connection Yes — connect() first No — just send
Reliable Yes — retransmits lost packets No — fire and forget
Ordered Yes No
Use case SSH, HTTP, sensor logging Video streaming, DNS

For a chat application, TCP is the right choice — you don't want to lose messages.


2. TCP Chat — Server and Client

A minimal TCP chat: the server listens for a client, then both sides can type messages back and forth. Each side uses two threads — one for sending (reading from stdin) and one for receiving (reading from the socket).

chat_server.c — waits for a client, then chats

/* chat_server.c — TCP chat server
 *
 * Build:  gcc -Wall -pthread -o chat_server chat_server.c
 * Run:    ./chat_server 9000
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>

volatile int connected = 1;

void *receiver(void *arg)
{
    int sock = *(int *)arg;
    char buf[256];
    int n;

    while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
        buf[n] = '\0';
        printf("\r  [Remote] %s\n  [You] ", buf);
        fflush(stdout);
    }
    printf("\n  Connection closed by remote.\n");
    connected = 0;
    return NULL;
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        return 1;
    }
    int port = atoi(argv[1]);

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

    /* Allow port reuse (avoids "Address already in use" after restart) */
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Bind to port */
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(port),
    };
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }

    /* Listen for one client */
    listen(server_fd, 1);
    printf("Waiting for connection on port %d...\n", port);

    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &len);
    if (client_fd < 0) { perror("accept"); return 1; }

    printf("Connected from %s:%d\n\n",
           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    /* Receiver thread reads from socket */
    pthread_t t_recv;
    pthread_create(&t_recv, NULL, receiver, &client_fd);

    /* Main thread reads from stdin and sends */
    char line[256];
    printf("  [You] ");
    fflush(stdout);
    while (connected && fgets(line, sizeof(line), stdin)) {
        line[strcspn(line, "\n")] = '\0';
        if (strlen(line) == 0) continue;
        send(client_fd, line, strlen(line), 0);
        printf("  [You] ");
        fflush(stdout);
    }

    close(client_fd);
    close(server_fd);
    pthread_join(t_recv, NULL);
    return 0;
}

chat_client.c — connects to the server

/* chat_client.c — TCP chat client
 *
 * Build:  gcc -Wall -pthread -o chat_client chat_client.c
 * Run:    ./chat_client 127.0.0.1 9000        (same machine)
 *         ./chat_client 192.168.1.42 9000     (across network)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>

volatile int connected = 1;

void *receiver(void *arg)
{
    int sock = *(int *)arg;
    char buf[256];
    int n;

    while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0) {
        buf[n] = '\0';
        printf("\r  [Remote] %s\n  [You] ", buf);
        fflush(stdout);
    }
    printf("\n  Connection closed by server.\n");
    connected = 0;
    return NULL;
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <server-ip> <port>\n", argv[0]);
        return 1;
    }

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

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(atoi(argv[2])),
    };
    inet_pton(AF_INET, argv[1], &addr.sin_addr);

    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("connect"); return 1;
    }
    printf("Connected to %s:%s\n\n", argv[1], argv[2]);

    pthread_t t_recv;
    pthread_create(&t_recv, NULL, receiver, &sock);

    char line[256];
    printf("  [You] ");
    fflush(stdout);
    while (connected && fgets(line, sizeof(line), stdin)) {
        line[strcspn(line, "\n")] = '\0';
        if (strlen(line) == 0) continue;
        send(sock, line, strlen(line), 0);
        printf("  [You] ");
        fflush(stdout);
    }

    close(sock);
    pthread_join(t_recv, NULL);
    return 0;
}

Build and Test

Test on the same machine (two terminals):

# Terminal 1: start server
gcc -Wall -pthread -o chat_server chat_server.c
./chat_server 9000

# Terminal 2: connect client
gcc -Wall -pthread -o chat_client chat_client.c
./chat_client 127.0.0.1 9000

Test across the network (laptop to RPi, or laptop to laptop):

# On RPi (server):
./chat_server 9000

# On laptop (client) — use RPi's IP address:
./chat_client 192.168.1.42 9000

Type a message in either terminal — it appears on the other side.


3. How Sockets Map to What You Already Know

 Pipe:     fd[0] ◄─────────── fd[1]          (one direction, same machine)

 Socket:   sock_fd ◄─── TCP ───► sock_fd     (bidirectional, across network)
Already learned Socket equivalent
pipe() creates FDs socket() creates FD
read(fd, ...) recv(sock, ...)
write(fd, ...) send(sock, ...)
close(fd) close(sock)
Pipe connects parent-child Socket connects any two machines
Note

Sockets are file descriptors. This is "everything is a file" in action. You can even use read() and write() instead of recv() and send() — they work identically on TCP sockets. The socket API just adds connect(), bind(), listen(), and accept() for setting up the connection.


4. Extensions

Tip

Extension A: UDP sensor broadcast

Rewrite the server to use SOCK_DGRAM (UDP) and sendto() — broadcast sensor readings to all listeners on the network. No connect(), no accept(). This is how many IoT sensor networks work.

Hint: sendto(sock, buf, len, 0, (struct sockaddr *)&dest, sizeof(dest))

Tip

Extension B: Multi-client server

The current server handles one client. Modify it to accept() in a loop, creating a new thread for each client. Maintain a list of connected client FDs protected by a mutex. When one client sends a message, broadcast it to all others — a real chat room.


What Just Happened?

Concept What You Did Shell Equivalent Later in Course
socket() Created a network endpoint nc (netcat) IoT protocols, remote sensor access
bind() + listen() Set up a server on a port nc -l -p 9000 Embedded web servers
connect() Connected to a remote server nc host port Remote sensor clients
recv() / send() Exchanged data over TCP Read/write in nc Telemetry, control protocols

Forward references:

  • Sockets + threads → The SDL2 dashboard tutorials use socket-based data feeds
  • TCP server → Embedded web servers (lighttpd, custom) use the same bind/listen/accept pattern
  • UDP broadcast → Sensor networks, service discovery (mDNS, SSDP)

Deliverable

  • [ ] chat_server.c compiles and runs — waits for a connection on the specified port
  • [ ] chat_client.c compiles and runs — connects to the server
  • [ ] Chat works between two terminals on the same machine (localhost)
  • [ ] Chat works between two different machines (if available)
  • [ ] Can explain the difference between TCP and UDP
  • [ ] Can map socket()/recv()/send() to the pipe read()/write() model
  • [ ] (Optional) Extension A or B completed

Checkpoint

Question Your Answer
Can you chat between two terminals on the same machine?
Can you chat between your laptop and a classmate's?
What happens if you start the client before the server?
What does SO_REUSEADDR prevent?

Back to Course Overview