Linux Sockets Stream Sockets โ€“ Connection Flow & listen()

 

Linux Sockets โ€“ Chapter 56
Part 4: Stream Sockets โ€“ Connection Flow & listen()
๐Ÿ“ฃ listen()
๐Ÿค connect()
โœ… accept()
Active vs Passive

Section 56.5 โ€“ Stream Sockets

Stream sockets work like a telephone call. Before two processes can exchange data, a connection must be established. This section covers the complete handshake: how a server listens for incoming calls, how a client dials in, and how the server picks up. Understanding this sequence is fundamental to all TCP network programming on Linux.

Key Terms in This Part

listen() connect() accept() backlog pending connection active socket passive socket active open passive open server client connection queue

Stream Sockets โ€“ Telephone Call Analogy

The entire stream socket connection sequence maps very naturally to a telephone call. Keep this analogy in mind as you read the technical details.

System Call Telephone Equivalent Who calls it
socket() Installing a telephone handset Both server and client
bind() Getting a phone number registered Server (usually)
listen() Turning the phone on so it can ring Server
connect() Dialing someone’s number Client
accept() Picking up the ringing phone Server
read() / write() Having a two-way conversation Both (bidirectional)
close() Hanging up the phone Either side

Active vs Passive Sockets

Every stream socket starts in the same state after socket(). Its role is determined by whether the application calls listen() or connect() next:

๐ŸŸฃ Active Socket
  • Default state after socket()
  • Used by the client
  • Calls connect() to initiate a connection
  • This is called active open
๐ŸŸ  Passive Socket (Listening Socket)
  • Created by calling listen()
  • Used by the server
  • Waits for connect() calls from clients
  • This is called passive open

In practice, the server always does the passive open (calls listen()) and the client always does the active open (calls connect()). This is the convention assumed throughout network programming literature.

listen() โ€“ Marking a Socket as Passive
#include <sys/socket.h>

int listen(int sockfd, int backlog);

/* Returns: 0 on success, -1 on error */

listen() marks the socket referred to by sockfd as a passive (listening) socket. After this call, the socket can accept incoming connections from clients.

The backlog Argument

Consider this scenario: the server is busy handling an existing client and a new client calls connect(). The connection cannot be immediately accepted. What happens?

The kernel maintains a connection queue for pending connections. The backlog argument sets the maximum length of this queue.

Clients connecting
Client A โ†’
Client B โ†’
Client C โ†’

โ†’

Pending Queue (backlog = 3)
Slot 1 โ€“ Client A
Slot 2 โ€“ Client B
Slot 3 โ€“ Client C

โ†’

Server
accept()
picks one by one

If a client tries to connect when the queue is full, its connect() call may block or fail with ECONNREFUSED depending on the implementation. A common value for backlog is 5 or higher. Since Linux 2.2, the backlog refers specifically to the fully established connections queue (not the half-open queue).

Important restriction: You cannot call listen() on a socket that has already been connected with connect(), or on a socket returned by accept().

Complete Stream Socket Connection Flow

This is the sequence of system calls in a typical client-server interaction.

SERVER (Passive)
1
socket()
Create socket fd
2
bind()
Assign address
3
listen()
Mark as passive
4
accept()
BLOCKS waiting for client
6
accept() returns
new connected fd
7
read() / write()
Exchange data
8
close()
Terminate connection

CLIENT (Active)
1
socket()
Create socket fd
(client does not call bind or listen)
5
connect()
Initiate connection to server
7
read() / write()
Exchange data
8
close()
Terminate connection

Notice that accept() returns a new file descriptor for the connected socket. The original listening socket (from step 3) remains open and continues to accept new clients. Servers typically fork a child process or start a thread to handle the new fd while the main process loops back to accept().

What if connect() is called before accept()?

This is very common. A client may call connect() before the server has called accept() โ€” for example, if the server is busy with another client.

The kernel handles this gracefully. It completes the TCP three-way handshake with the client and places the connection into the backlog queue. The client’s connect() returns successfully. When the server finally calls accept(), it removes the waiting connection from the queue.

If accept() is called before any client has connected, it blocks (waits) until a connection arrives โ€” like sitting by the telephone waiting for it to ring.

Coding Examples

Example 1: Minimal TCP Server

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

#define PORT    8080
#define BACKLOG 5

int main(void)
{
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buf[256];
    ssize_t n;

    /* 1. Create listening socket */
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) { perror("socket"); exit(1); }

    /* 2. Bind to well-known 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(listen_fd,
             (struct sockaddr *) &server_addr,
             sizeof(server_addr)) == -1) {
        perror("bind"); exit(1);
    }

    /* 3. Mark socket as passive (listening) */
    if (listen(listen_fd, BACKLOG) == -1) { perror("listen"); exit(1); }

    printf("Server listening on port %d...\n", PORT);

    /* 4. Accept one client connection */
    client_len = sizeof(client_addr);
    conn_fd = accept(listen_fd,
                     (struct sockaddr *) &client_addr,
                     &client_len);
    if (conn_fd == -1) { perror("accept"); exit(1); }

    printf("Client connected\n");

    /* 5. Read data from client */
    n = read(conn_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("Received: %s\n", buf);
    }

    /* 6. Send response */
    write(conn_fd, "Hello from server!\n", 19);

    /* 7. Close connections */
    close(conn_fd);
    close(listen_fd);
    return 0;
}

Example 2: Minimal TCP 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 SERVER_PORT 8080

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;
    char buf[256];
    ssize_t n;

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

    /* 2. Fill in server address */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(SERVER_PORT);

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

    /* 3. Connect (active open) โ€“ no bind() needed for client */
    if (connect(sockfd,
                (struct sockaddr *) &server_addr,
                sizeof(server_addr)) == -1) {
        perror("connect"); exit(1);
    }

    printf("Connected to server\n");

    /* 4. Send data */
    write(sockfd, "Hello from client!\n", 19);

    /* 5. Read server response */
    n = read(sockfd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("Server says: %s\n", buf);
    }

    close(sockfd);
    return 0;
}

Example 3: Iterative server โ€“ handle multiple clients in sequence

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

#define PORT    9000
#define BACKLOG 10

int main(void)
{
    int listen_fd, conn_fd;
    struct sockaddr_in saddr;
    char msg[] = "Welcome!\n";

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

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

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

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

    printf("Iterative server on port %d. Waiting...\n", PORT);

    /*
     * Loop forever: accept a client, serve it, close, repeat.
     * Only one client is served at a time (iterative, not concurrent).
     * For concurrent serving, fork() or threads would be used.
     */
    for (;;) {
        conn_fd = accept(listen_fd, NULL, NULL);
        if (conn_fd == -1) { perror("accept"); continue; }

        write(conn_fd, msg, sizeof(msg) - 1);
        close(conn_fd);   /* done with this client */
    }

    close(listen_fd);
    return 0;
}

Example 4: Concurrent server using fork()

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

#define PORT    9001
#define BACKLOG 10

/* Simple child process: handle one client then exit */
static void handle_client(int conn_fd)
{
    char buf[512];
    ssize_t n;

    n = read(conn_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("[child %d] Received: %s\n", (int)getpid(), buf);
        write(conn_fd, "ACK\n", 4);
    }
    close(conn_fd);
}

int main(void)
{
    int listen_fd, conn_fd;
    struct sockaddr_in saddr;
    pid_t pid;

    /* Prevent zombie children */
    signal(SIGCHLD, SIG_IGN);

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

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

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

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

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

        pid = fork();
        if (pid == -1) {
            perror("fork");
            close(conn_fd);
            continue;
        }

        if (pid == 0) {
            /* Child: close the listening fd, handle client, exit */
            close(listen_fd);
            handle_client(conn_fd);
            exit(0);
        }

        /* Parent: close the connected fd, loop back to accept() */
        close(conn_fd);
    }

    return 0;
}

๐ŸŽฏ Interview Questions โ€“ Stream Sockets & Connection Flow
Q1. What is the sequence of system calls a TCP server must make?socket() โ†’ bind() โ†’ listen() โ†’ accept() โ†’ read()/write() โ†’ close(). The server creates a socket, assigns it an address, marks it as passive, then loops on accept() to receive incoming client connections. Each accept() call returns a new connected socket fd for communicating with that specific client.

Q2. What is the difference between an active socket and a passive socket?An active socket is the default state of a socket after socket(). It is used by the client to initiate connections via connect() โ€” this is called an active open. A passive socket (also called a listening socket) is created when the server calls listen(). It waits for incoming connections โ€” this is called a passive open. A socket can be one or the other, never both.

Q3. What does the backlog argument to listen() control?The backlog sets the maximum number of pending (unaccepted) connections that the kernel will queue for this socket. If a client calls connect() while the server is busy and has not yet called accept(), the connection waits in this queue. If the queue is full, new connection attempts may be refused. A common value is 5 to 128.

Q4. What does accept() return?accept() returns a brand new file descriptor for the newly established connection with one specific client. The original listening socket fd remains unchanged and continues to accept future connections. The new fd is what you use for read()/write() with that particular client. If no client is waiting, accept() blocks until one arrives.

Q5. Can connect() be called before the server calls accept()? What happens?Yes, this is normal. When a client calls connect(), the kernel performs the TCP three-way handshake with the server’s listening socket and queues the connection in the backlog. The client’s connect() returns successfully. The connection waits in the queue until the server calls accept(), which dequeues it and returns a new fd.

Q6. Why does a concurrent server close(conn_fd) in the parent after fork()?After fork(), both parent and child have a copy of conn_fd. The child uses conn_fd to communicate with the client. The parent should close its copy immediately because: (1) the parent does not need it โ€” it goes back to accept(); (2) if the parent keeps it open, the connection’s reference count stays at 2, and when the child closes it, the connection is not actually terminated (file descriptors are reference-counted).

Q7. What is SO_REUSEADDR and why is it used on server sockets?SO_REUSEADDR is a socket option set via setsockopt(). It allows a server to bind to a port that is still in the TIME_WAIT state from a previous socket. Without it, if you restart a server quickly after stopping it, bind() fails with EADDRINUSE because the OS is still holding the port in TIME_WAIT. Setting SO_REUSEADDR before bind() avoids this problem during development and restarts.

Q8. What is the difference between an iterative server and a concurrent server?An iterative server handles one client at a time: accept, serve, close, then accept the next. Simple to implement but clients queue up. A concurrent server forks a child process (or creates a thread) for each accepted connection, allowing multiple clients to be served simultaneously. The parent immediately loops back to accept() while the child handles the client.

Q9. Can you call listen() on a socket that was returned by accept()?No. The POSIX specification and Linux explicitly disallow calling listen() on a connected socket โ€” i.e., a socket on which connect() has been successfully called, or a socket returned by accept(). Doing so returns an error.

Q10. What happens if accept() is called on a listening socket before any client connects?accept() blocks (sleeps) until a client connection arrives. Once a connection is available in the backlog queue, accept() returns immediately with the new connected socket fd. To avoid blocking, use a non-blocking socket (O_NONBLOCK) or use select()/poll()/epoll() to wait for the socket to become readable before calling accept().

๐Ÿ“‹ Chapter 56 Summary โ€“ All Parts
Part Topic Key System Calls / Concepts
Part 1 Socket Overview Domains, Types, IPC vs Network, FIONREAD
Part 2 Creating a Socket socket(), SOCK_CLOEXEC, SOCK_NONBLOCK
Part 3 Binding & Address Structures bind(), struct sockaddr, sockaddr_in, well-known port, ephemeral port
Part 4 Stream Socket Connection Flow listen(), connect(), accept(), backlog queue, active/passive

Chapter 56 Complete!

You now have a solid foundation in Linux sockets. Next: UNIX Domain Sockets (Ch. 57) and TCP/IP Sockets (Ch. 58).

โ† Back to Part 1 EmbeddedPathashala

Leave a Reply

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