Socket System Calls bind() · listen() · accept() · connect() ·

 

Chapter 56 – Socket System Calls
Part 4 of 4  |  bind() · listen() · accept() · connect() · Full TCP Server-Client Example
Series
Linux IPC
Chapter
56 – TLPI
Level
Intermediate

What You Will Learn

This is the most hands-on part of Chapter 56. We cover every key socket system call in detail — what it does, why it is needed, the full function signature, error handling, and practical code. At the end there is a complete minimal TCP echo server and client you can compile and run.

Key Terms

bind() listen() accept() connect() Backlog Queue Passive Socket Active Socket socketcall() Well-Known Port Ephemeral Port

1. The Complete TCP Connection Flow

Before looking at each call individually, here is the complete sequence showing how server and client system calls interleave to create a TCP connection:

SERVER Step CLIENT
socket() — create socket fd 1 socket() — create socket fd
bind() — attach to well-known port 2 (optional bind)
listen() — mark socket as passive 3
blocks waiting… 4 connect() — initiate TCP handshake
accept() → new connected fd 5 ✔ connect() returns success
read() / write() 6 write() / read()
close() 7 close()

2. bind() — Assign an Address to a Socket

bind() ties a socket to a specific local address. For servers this is how they publish themselves at a known location.

#include <sys/socket.h>

int bind(int sockfd,
         const struct sockaddr *addr,
         socklen_t addrlen);
/*
 * sockfd  – file descriptor from socket()
 * addr    – pointer to address structure (sockaddr_in, sockaddr_un, etc.)
 * addrlen – sizeof(*addr)
 *
 * Returns 0 on success, -1 on error
 */

The struct sockaddr * is a generic wrapper. You always fill in the domain-specific structure (sockaddr_in, sockaddr_un) and cast its pointer when passing to bind().

Example — bind a TCP server to port 8080 on all interfaces:

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

int bind_server(void)
{
    int sockfd;
    struct sockaddr_in addr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); exit(1); }

    /* Allow immediate reuse of port after crash/restart */
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(8080);      /* port 8080 */
    addr.sin_addr.s_addr = INADDR_ANY;       /* all local interfaces */

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        exit(1);
    }

    printf("Socket bound to port 8080\n");
    return sockfd;
}

Key points about bind():

  • A client usually does not call bind() — the kernel picks an ephemeral (temporary) port automatically.
  • Well-known ports (0–1023) require root/CAP_NET_BIND_SERVICE.
  • SO_REUSEADDR is essential on servers so you can restart quickly after a crash without hitting “Address already in use”.

3. listen() — Make a Socket Passive (Accept-Ready)

After bind(), a stream socket is still an “active” socket — it can connect outward but cannot accept inbound connections. Calling listen() converts it into a passive socket that can receive incoming connection requests.

#include <sys/socket.h>

int listen(int sockfd, int backlog);
/*
 * sockfd  – bound socket fd
 * backlog – maximum number of pending (unaccepted) connections to queue
 *
 * Returns 0 on success, -1 on error
 */

The backlog parameter controls the size of the connection queue. When clients connect faster than the server calls accept(), completed connections are held in this queue. If the queue is full, new connection attempts are refused.

Connection Queue (backlog = 3) Server calls
Client A ✔ Client B ✔ Client C ✔ Client D ✘ refused accept() dequeues A → B → C
/* Typical usage — backlog of 5 to 10 is common for many servers */
if (listen(sockfd, 10) == -1) {
    perror("listen");
    exit(1);
}
printf("Listening for connections...\n");

4. accept() — Dequeue a Client Connection

accept() takes the first completed connection from the queue and returns a new file descriptor representing that individual client connection. The original listening socket remains open and keeps accepting future clients.

#include <sys/socket.h>

int accept(int sockfd,
           struct sockaddr *addr,
           socklen_t *addrlen);
/*
 * sockfd  – the listening socket (from listen())
 * addr    – filled with client's address (can be NULL if not needed)
 * addrlen – in: sizeof(*addr)   out: actual size written
 *
 * Returns: new connected socket fd on success, -1 on error
 * Blocks if no connection is pending (use O_NONBLOCK or select/poll to avoid)
 */

Important: accept() creates a brand-new socket fd for each client. The server uses this new fd to talk to that client, and continues listening on the original fd for the next client. This is why a typical server does:

/* Loop: accept one client, fork/thread to handle it, accept next */
while (1) {
    struct sockaddr_in client_addr;
    socklen_t clen = sizeof(client_addr);

    int connfd = accept(listenfd,
                        (struct sockaddr *)&client_addr,
                        &clen);
    if (connfd == -1) { perror("accept"); continue; }

    /* At this point client_addr holds the client's IP and port */
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
    printf("Client connected from %s:%d\n",
           ip, ntohs(client_addr.sin_port));

    /* Handle the client — in a real server: fork() or pthread_create() */
    handle_client(connfd);

    close(connfd);   /* close this client's fd when done */
}
/* listenfd stays open for the next client */

5. connect() — Client Initiates the Connection

The client calls connect() to initiate a connection to a listening server. For TCP this triggers the three-way handshake. The call blocks until the connection is established or an error occurs.

#include <sys/socket.h>

int connect(int sockfd,
            const struct sockaddr *addr,
            socklen_t addrlen);
/*
 * sockfd  – the client's socket fd (from socket())
 * addr    – server's address (IP + port for AF_INET)
 * addrlen – sizeof(*addr)
 *
 * Returns 0 on success, -1 on error
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    int sockfd;
    struct sockaddr_in server;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); exit(1); }

    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port   = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);

    if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1) {
        perror("connect");
        exit(1);
    }

    printf("Connected to server!\n");
    /* Now read()/write() on sockfd */
    close(sockfd);
    return 0;
}

Common connect() errors:

  • ECONNREFUSED — No server listening on that address/port.
  • ETIMEDOUT — Server unreachable (firewall or wrong IP).
  • ENETUNREACH — No route to host.

6. Inside the Kernel — socketcall() Multiplexing

On most Linux architectures (x86-64, ARM, etc.) each socket-related function like bind(), listen(), and accept() is a genuine independent system call in the kernel. However, on some older architectures (notably 32-bit x86 and IA-64), all socket functions are actually multiplexed through a single kernel entry point called socketcall().

The socketcall() mechanism is a historical artefact from when the Linux socket layer was developed separately. It does not affect application code — you always use the standard function names. The C library wraps them transparently.

When you look at a system call trace with strace on modern x86-64 you will see the individual calls (e.g. bind, listen) directly. On 32-bit x86 you may see socketcall(SYS_BIND, ...) instead.

/* Run strace to see what system calls your socket code makes:
 *
 *   strace ./your_server 2>&1 | grep -E "socket|bind|listen|accept|connect"
 *
 * Typical output on x86-64:
 *   socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
 *   setsockopt(3, SOL_SOCKET, SO_REUSEADDR, ...) = 0
 *   bind(3, {sa_family=AF_INET, sin_port=htons(8080), ...}, 16) = 0
 *   listen(3, 10) = 0
 *   accept4(3, ...) = 4
 */

7. Complete Example — Minimal TCP Echo Server & Client

The following two programs together demonstrate all five system calls. The server echoes back whatever the client sends, then both close.

tcp_server.c:

/* tcp_server.c — minimal TCP echo server */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT    8080
#define BACKLOG 5
#define BUFSIZE 1024

int main(void)
{
    int                listenfd, connfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t          clen;
    char               buf[BUFSIZE];
    ssize_t            n;
    int                opt = 1;

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

    /* Allow port reuse after restart */
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Step 2: Bind to address */
    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(listenfd, (struct sockaddr *)&server_addr,
             sizeof(server_addr)) == -1) {
        perror("bind"); exit(1);
    }

    /* Step 3: Listen */
    if (listen(listenfd, BACKLOG) == -1) {
        perror("listen"); exit(1);
    }
    printf("Server listening on port %d\n", PORT);

    /* Step 4: Accept one client */
    clen    = sizeof(client_addr);
    connfd  = accept(listenfd, (struct sockaddr *)&client_addr, &clen);
    if (connfd == -1) { perror("accept"); exit(1); }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
    printf("Client connected: %s:%d\n", ip, ntohs(client_addr.sin_port));

    /* Step 5: Echo loop */
    while ((n = read(connfd, buf, BUFSIZE)) > 0) {
        printf("Received %zd bytes: %.*s", n, (int)n, buf);
        if (write(connfd, buf, n) != n) {
            perror("write"); break;
        }
    }

    printf("Client disconnected.\n");
    close(connfd);
    close(listenfd);
    return 0;
}

tcp_client.c:

/* tcp_client.c — connects to echo server and sends a message */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT    8080
#define BUFSIZE 1024

int main(void)
{
    int                sockfd;
    struct sockaddr_in server_addr;
    char               buf[BUFSIZE];
    ssize_t            n;
    const char        *msg = "Hello from client!\n";

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

    /* Step 2: Fill server address */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

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

    /* Step 4: Send data */
    if (write(sockfd, msg, strlen(msg)) == -1) {
        perror("write"); exit(1);
    }

    /* Step 5: Read echo */
    n = read(sockfd, buf, BUFSIZE - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("Echo from server: %s", buf);
    }

    close(sockfd);
    return 0;
}

Build and run:

# Terminal 1 — compile and run server
gcc -o tcp_server tcp_server.c
./tcp_server
# Output: Server listening on port 8080

# Terminal 2 — compile and run client
gcc -o tcp_client tcp_client.c
./tcp_client
# Output: Connected to server
#         Echo from server: Hello from client!

8. Quick Reference — All Socket System Calls
Call Who calls it Purpose Returns
socket() Both Create a new socket fd or -1
bind() Server (usually) Assign local address/port 0 or -1
listen() Server only Mark socket as passive, set backlog 0 or -1
accept() Server only Dequeue a client; get new connected fd new fd or -1
connect() Client only Initiate connection to server 0 or -1
read()/write() Both Transfer data on connected socket bytes or -1
send()/recv() Both Like read/write but with socket-specific flags bytes or -1
sendto()/recvfrom() Both (UDP) Send/receive datagram with address bytes or -1
close() Both Close socket fd, initiate TCP FIN 0 or -1

Interview Questions — Part 4

Q1. What does listen() do and what is the backlog parameter?

listen() transitions a socket from active (client-capable) to passive (server-capable), allowing it to receive incoming connections. The backlog parameter sets the maximum number of completed but not-yet-accepted connections the kernel will queue. New connections arriving when the queue is full are rejected.

Q2. Why does accept() return a new file descriptor rather than reusing the listening fd?

Because a server must handle multiple clients simultaneously. The listening fd represents the server’s well-known address and must remain open to accept future connections. The new fd returned by accept() is specific to one client and is used only for that client’s data exchange. When that client disconnects, the new fd is closed while the listening fd stays open.

Q3. What happens if a client calls connect() on a port where no server is listening?

The kernel on the server machine sends a TCP RST (reset) packet back, and connect() returns -1 with errno set to ECONNREFUSED.

Q4. Why should servers set SO_REUSEADDR before bind()?

After a server crashes or is killed, the TCP stack keeps the port in a TIME_WAIT state for up to 2 minutes. Without SO_REUSEADDR, a restarted server would fail bind() with EADDRINUSE during that window. SO_REUSEADDR tells the kernel to allow reuse of the address immediately.

Q5. What is the difference between write() and send() on a socket?

They both transmit data. send() adds a flags parameter (e.g. MSG_DONTWAIT for non-blocking, MSG_NOSIGNAL to suppress SIGPIPE). write(fd, buf, n) is equivalent to send(fd, buf, n, 0).

Q6. What is socketcall() and why does it exist?

On some Linux architectures (32-bit x86, IA-64), all socket functions are multiplexed through a single kernel entry point called socketcall() that takes a subcommand (SYS_BIND, SYS_LISTEN, etc.) as its first argument. This was an artifact of the original Linux socket implementation being a separate project. Modern 64-bit Linux kernels provide each socket function as its own system call.

Q7. At what point does the TCP three-way handshake happen in the socket API?

When the client calls connect(). The kernel sends a SYN, receives SYN-ACK from the server, and sends ACK — all transparently. connect() returns only after the handshake is complete (or fails). On the server side, accept() retrieves a connection from the queue that the kernel already fully established.

Q8. Can you call listen() on a SOCK_DGRAM socket? Why or why not?

No. listen() is only valid on stream sockets (SOCK_STREAM). Datagram sockets are connectionless — there is no concept of an incoming connection request to queue. A UDP server simply calls bind() and then recvfrom() directly.

Chapter 56 Complete!

You have covered all the fundamentals: what sockets are, the three domains, stream vs datagram types, and every key system call with working code.

← Back to Part 1 More Tutorials

Leave a Reply

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