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.
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;
}
| 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).
