Server Code
Iterative Server
is_seqnum_sv.c
What Does This Server Do?
The server in this chapter is called a sequence number server (is_seqnum_sv). It maintains a counter. Each time a client connects and asks for N numbers, the server tells the client the starting number and advances the counter by N. So if client A asks for 1 number and client B asks for 10, client A gets 0, client B gets 1, and the next client would start from 11.
It is an iterative server — it handles one client fully before moving to the next. It does not fork a child or create a thread per client.
This server demonstrates the complete server socket lifecycle using getaddrinfo(), which makes it work with both IPv4 and IPv6.
Every TCP server follows the same lifecycle. Here it is mapped to the actual function calls used in this program:
| Step | Function | What Happens |
|---|---|---|
| 1 | getaddrinfo() |
Get list of addresses to bind to (wildcard, port 50000) |
| 2 | socket() |
Create a socket file descriptor |
| 3 | setsockopt(SO_REUSEADDR) |
Allow reuse of port after server restarts |
| 4 | bind() |
Attach socket to the port (50000) on all interfaces |
| 5 | listen() |
Mark socket as passive, set backlog queue size |
| 6 | accept() |
Block until a client connects, get new socket for that client |
| 7 | read() / write() |
Communicate with the client |
| 8 | close(cfd) |
Close client connection, loop back to accept() |
The very first thing the server does after parsing arguments is:
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
errExit("signal");
Why? When a server tries to write() data to a client that has already closed its end of the connection, the kernel sends a SIGPIPE signal to the server process. The default behavior of SIGPIPE is to terminate the process. That would kill the whole server just because one client disconnected!
By setting SIG_IGN (ignore), the server says: “Don’t kill me for this. Just let the write() call return -1 with errno == EPIPE and I’ll handle it myself.”
| SIGPIPE Handling | What happens on write() to closed client |
|---|---|
| Default (SIG_DFL) | Server process is terminated — bad! |
| Ignored (SIG_IGN) | write() returns -1, errno = EPIPE — server can handle gracefully |
The server needs to bind to all network interfaces so any client can connect. This is done by passing NULL as the node to getaddrinfo() together with the AI_PASSIVE flag:
struct addrinfo hints;
struct addrinfo *result, *rp;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_socktype = SOCK_STREAM; /* TCP */
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
/*
* AI_PASSIVE → use wildcard address (all interfaces)
* AI_NUMERICSERV → PORT_NUM is "50000", a number not a name
*/
if (getaddrinfo(NULL, PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
Passing NULL as the first argument (node) combined with AI_PASSIVE is the standard way to say: “I’m a server, give me the wildcard address so I can accept connections from any network interface.”
getaddrinfo() may return multiple address structures (one for IPv4, one for IPv6). The server loops through them until one succeeds:
int optval = 1;
for (rp = result; rp != NULL; rp = rp->ai_next) {
/* Try to create a socket with this address family */
lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (lfd == -1)
continue; /* socket() failed, try next address */
/* SO_REUSEADDR: lets us restart server without waiting for
TIME_WAIT to expire on the old port binding */
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval)) == -1)
errExit("setsockopt");
/* Try to bind this socket to the port */
if (bind(lfd, rp->ai_addr, rp->ai_addrlen) == 0)
break; /* bind() succeeded! Stop the loop */
/* bind() failed: close this socket and try next address */
close(lfd);
}
if (rp == NULL)
fatal("Could not bind socket to any address");
SO_REUSEADDR explained: When a server stops and restarts, the old TCP connections may still be in TIME_WAIT state on the OS for up to 2 minutes. Without SO_REUSEADDR, the new server can’t bind the same port during that time and will get “Address already in use”. Setting SO_REUSEADDR before bind() bypasses this restriction.
#define BACKLOG 50
/* Convert socket from active (connecting) to passive (listening) */
if (listen(lfd, BACKLOG) == -1)
errExit("listen");
/* Done with the addrinfo list — free it */
freeaddrinfo(result);
/* Main loop: handle clients one at a time */
for (;;) {
struct sockaddr_storage claddr;
socklen_t addrlen = sizeof(struct sockaddr_storage);
/* accept() blocks until a client connects */
/* Returns a NEW socket fd for this specific client */
cfd = accept(lfd, (struct sockaddr *) &claddr, &addrlen);
if (cfd == -1) {
errMsg("accept");
continue; /* On error, just try to accept the next one */
}
/* Now communicate with the client using cfd ... */
/* lfd is still listening for new connections */
}
Key points about listen() and accept():
| Concept | Explanation |
|---|---|
| BACKLOG = 50 | Up to 50 pending connections can queue up waiting for accept() |
| lfd (listening fd) | Stays open the whole server lifetime. Never written to. |
| cfd (client fd) | New socket for each client. Closed after that client is done. |
| sockaddr_storage | Buffer large enough for IPv4 or IPv6 client address |
After accept() returns a client fd, the server does four things:
char host[NI_MAXHOST];
char service[NI_MAXSERV];
char addrStr[NI_MAXHOST + NI_MAXSERV + 10];
/* 1. Identify the client: turn their address into a readable string */
if (getnameinfo((struct sockaddr *) &claddr, addrlen,
host, NI_MAXHOST,
service, NI_MAXSERV, 0) == 0)
snprintf(addrStr, sizeof(addrStr), "(%s, %s)", host, service);
else
snprintf(addrStr, sizeof(addrStr), "(?UNKNOWN?)");
printf("Connection from %s\n", addrStr);
/* 2. Read how many sequence numbers the client wants */
char reqLenStr[INT_LEN];
if (readLine(cfd, reqLenStr, INT_LEN) <= 0) {
close(cfd);
continue; /* Read failed or client sent nothing */
}
int reqLen = atoi(reqLenStr);
if (reqLen <= 0) { /* Sanity check: reject bad requests */
close(cfd);
continue;
}
/* 3. Send back the current sequence number as a string */
char seqNumStr[INT_LEN];
snprintf(seqNumStr, INT_LEN, "%d\n", seqNum);
if (write(cfd, seqNumStr, strlen(seqNumStr)) != strlen(seqNumStr))
fprintf(stderr, "Error on write");
/* 4. Advance the counter by how many the client requested */
seqNum += reqLen;
/* 5. Close this client's connection */
close(cfd);
Notice that data is exchanged as newline-terminated strings, not as raw binary integers. This makes it easy to test with tools like telnet and avoids byte-order issues.
One great advantage of using newline-terminated strings for the protocol is that you can test the server manually using telnet without writing any client code:
/* Start the server in background */
$ ./is_seqnum_sv &
[1] 4075
/* Connect using telnet to port 50000 on localhost */
$ telnet localhost 50000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1 <-- You type: request 1 sequence number
12 <-- Server replies with sequence number 12
Connection closed by foreign host. <-- Server closed connection
This works because telnet sends text over a TCP connection, which is exactly what our simple protocol uses. Always design simple TCP protocols around text lines — it makes debugging much easier.
#define _BSD_SOURCE /* For NI_MAXHOST, NI_MAXSERV from netdb.h */
#include <netdb.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "is_seqnum.h" /* defines PORT_NUM, INT_LEN etc. */
#define BACKLOG 50 /* Max pending connections in listen queue */
int main(int argc, char *argv[])
{
uint32_t seqNum; /* Current sequence number */
char reqLenStr[INT_LEN]; /* Client's requested length as string */
char seqNumStr[INT_LEN]; /* Sequence number to send as string */
struct sockaddr_storage claddr; /* Client address buffer */
int lfd, cfd, optval, reqLen;
socklen_t addrlen;
struct addrinfo hints, *result, *rp;
char host[NI_MAXHOST], service[NI_MAXSERV];
char addrStr[NI_MAXHOST + NI_MAXSERV + 10];
/* Optional: start sequence at a specific number */
seqNum = (argc > 1) ? atoi(argv[1]) : 0;
/* Ignore SIGPIPE so write() to disconnected client returns -1
instead of killing the whole server */
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
errExit("signal");
/* Prepare hints for getaddrinfo */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; /* Wildcard + numeric port */
if (getaddrinfo(NULL, PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
optval = 1;
/* Try each address until bind() succeeds */
for (rp = result; rp != NULL; rp = rp->ai_next) {
lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (lfd == -1) continue;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof(optval)) == -1)
errExit("setsockopt");
if (bind(lfd, rp->ai_addr, rp->ai_addrlen) == 0)
break;
close(lfd);
}
if (rp == NULL) fatal("Could not bind socket to any address");
if (listen(lfd, BACKLOG) == -1)
errExit("listen");
freeaddrinfo(result); /* Done with address list */
/* Main server loop */
for (;;) {
addrlen = sizeof(struct sockaddr_storage);
cfd = accept(lfd, (struct sockaddr *) &claddr, &addrlen);
if (cfd == -1) { errMsg("accept"); continue; }
/* Log client address */
if (getnameinfo((struct sockaddr *) &claddr, addrlen,
host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0)
snprintf(addrStr, sizeof(addrStr), "(%s, %s)", host, service);
else
snprintf(addrStr, sizeof(addrStr), "(?UNKNOWN?)");
printf("Connection from %s\n", addrStr);
/* Read request */
if (readLine(cfd, reqLenStr, INT_LEN) <= 0) { close(cfd); continue; }
reqLen = atoi(reqLenStr);
if (reqLen <= 0) { close(cfd); continue; }
/* Send response */
snprintf(seqNumStr, INT_LEN, "%d\n", seqNum);
if (write(cfd, seqNumStr, strlen(seqNumStr)) != strlen(seqNumStr))
fprintf(stderr, "Error on write");
seqNum += reqLen; /* Advance counter */
close(cfd); /* Done with this client */
}
}
Q1. Why does the server ignore SIGPIPE?
The default action for SIGPIPE is to terminate the process. If a client disconnects while the server is writing to it, a SIGPIPE would kill the entire server. By ignoring it, the write() call returns -1 with errno == EPIPE instead, which the server can check and handle gracefully.
Q2. What is the difference between lfd and cfd in a TCP server?
lfd is the listening socket — it stays open for the lifetime of the server and is never used for data transfer. cfd is the connected socket returned by accept() — one is created per client connection and is closed when that client session ends.
Q3. What does SO_REUSEADDR do and why is it important for servers?
It allows a socket to bind to a port that still has connections in TIME_WAIT state from a previous server instance. Without it, restarting a server within 2 minutes of shutdown would fail with “Address already in use”. Setting it before bind() is standard practice for all TCP servers.
Q4. What does the BACKLOG parameter in listen() control?
It sets the maximum number of client connection requests that can be queued (pending) while the server hasn’t called accept() yet. Connections beyond this limit are refused by the kernel. A value of 50 is reasonable for most servers.
Q5. Why does the server use newline-terminated strings instead of binary integers?
Text-based protocols are easier to debug — you can test them with tools like telnet or nc without writing a client. They also avoid byte-order issues (big-endian vs little-endian) that affect binary integer transfer.
Q6. What is an iterative server? What is its limitation?
An iterative server handles one client completely before accepting the next. It is simple but has a key limitation: if one client is slow or misbehaves, all other clients are kept waiting. For production servers, a concurrent design (fork a process or create a thread per client, or use an event loop) is needed to serve multiple clients simultaneously.
Q7. What happens if getaddrinfo() returns multiple results and the first bind() fails?
The server’s loop closes the failed socket, moves to the next addrinfo in the linked list, and tries socket() and bind() again with the next address. This continues until one succeeds or the list is exhausted, in which case the server exits with an error.
← Part 1: Overview Next: Client Program → EmbeddedPathashala Home
