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
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 |
Every stream socket starts in the same state after socket(). Its role is determined by whether the application calls listen() or connect() next:
- Default state after
socket() - Used by the client
- Calls
connect()to initiate a connection - This is called active open
- 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.
#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.
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().
This is the sequence of system calls in a typical client-server interaction.
socket()Create socket fd
bind()Assign address
listen()Mark as passive
accept()BLOCKS waiting for client
accept() returnsnew connected fd
read() / write()Exchange data
close()Terminate connection
socket()Create socket fd
connect()Initiate connection to server
read() / write()Exchange data
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().
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.
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;
}
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().
| 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 |
You now have a solid foundation in Linux sockets. Next: UNIX Domain Sockets (Ch. 57) and TCP/IP Sockets (Ch. 58).
