Why a Helper Library?
Writing raw socket code for a TCP server involves many repetitive steps: create socket, set socket options, fill in the address structure, bind, listen. Every server you write repeats these same 10-15 lines of boilerplate. A helper library wraps all of this into two simple functions: inetListen() and inetConnect().
These functions also use the modern getaddrinfo() API, which handles both IPv4 and IPv6 automatically and does DNS resolution for you. This is how real production servers are written.
What the Library Provides
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
getaddrinfo(NULL, port, …);
sockfd = socket(…);
setsockopt(SO_REUSEADDR);
bind(sockfd, …);
listen(sockfd, backlog);
freeaddrinfo(…);
~15 lines, repeated everywhere
fd = inetListen(port, backlog, &addrlen);
1 line, clean and reusable
inetListen() — Server Side
inetListen() creates a listening TCP socket bound to the given port on all available network interfaces. It handles IPv4, sets SO_REUSEADDR, and returns a ready-to-accept() file descriptor.
/*
* inetListen() - Create a listening TCP socket on the given port.
*
* service - port number as string, e.g. "54321" or service name like "http"
* backlog - listen() backlog (queue size for pending connections)
* addrlen - if non-NULL, filled with address length (useful for accept())
*
* Returns a file descriptor ready for accept(), or -1 on error.
*/
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
int inetListen(const char *service, int backlog, socklen_t *addrlen)
{
struct addrinfo hints, *result, *rp;
int sockfd, optval, s;
memset(&hints, 0, sizeof(hints));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_flags = AI_PASSIVE; /* Bind to all interfaces */
/* Resolve address for listening */
s = getaddrinfo(NULL, service, &hints, &result);
if (s != 0) {
errno = ENOSYS; /* Use a generic error */
return -1;
}
/* Try each address returned by getaddrinfo */
optval = 1;
for (rp = result; rp != NULL; rp = rp->ai_next) {
sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sockfd == -1)
continue; /* Try next address */
/* Allow reuse of the port quickly after server restart */
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval)) == -1) {
close(sockfd);
freeaddrinfo(result);
return -1;
}
if (bind(sockfd, rp->ai_addr, rp->ai_addrlen) == 0)
break; /* bind() succeeded — use this address */
/* bind() failed — try next address */
close(sockfd);
}
if (rp == NULL) {
freeaddrinfo(result);
return -1; /* No address succeeded */
}
if (addrlen != NULL)
*addrlen = rp->ai_addrlen; /* Return address size to caller */
freeaddrinfo(result);
if (listen(sockfd, backlog) == -1) {
close(sockfd);
return -1;
}
return sockfd; /* Ready to accept() */
}
inetConnect() — Client Side
inetConnect() creates a TCP socket and connects it to the given host and port. It handles DNS resolution automatically — pass a hostname like “localhost” or “192.168.1.1” and it does the right thing.
/*
* inetConnect() - Connect to a TCP server.
*
* host - hostname or IP address string (e.g. "localhost", "192.168.1.5")
* service - port/service name (e.g. "54321" or "http")
* type - SOCK_STREAM for TCP, SOCK_DGRAM for UDP
*
* Returns a connected file descriptor, or -1 on error.
*/
int inetConnect(const char *host, const char *service, int type)
{
struct addrinfo hints, *result, *rp;
int sockfd, s;
memset(&hints, 0, sizeof(hints));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_socktype = type; /* SOCK_STREAM or SOCK_DGRAM */
/* Resolve hostname + port to a list of addresses */
s = getaddrinfo(host, service, &hints, &result);
if (s != 0) {
errno = ENOSYS;
return -1;
}
/* Try each address until one connects */
for (rp = result; rp != NULL; rp = rp->ai_next) {
sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sockfd == -1)
continue; /* Try next */
if (connect(sockfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* connect() succeeded */
close(sockfd); /* connect() failed — try next */
}
freeaddrinfo(result);
if (rp == NULL)
return -1; /* No address worked */
return sockfd; /* Connected and ready */
}
Sequence Number Server — Using the Library
The TLPI book uses a sequence number server (seqnum server) as the example application. The server keeps a counter; each client sends a request with a number N, and the server responds with N sequential numbers starting from the current counter value.
is_seqnum_sv.c — Server Using inetListen()
/* is_seqnum_sv.c — Sequence Number Server */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include "rlbuf.h"
#include "inet_sockets.h" /* provides inetListen() */
#define PORT_NUM "54321"
#define BACKLOG 50
#define MAXLINE 128
int main(int argc, char *argv[])
{
int listenfd, connfd;
socklen_t addrlen, len;
struct sockaddr_storage claddr;
char line[MAXLINE];
char addr_str[INET6_ADDRSTRLEN];
RlBuf rlbuf;
int seq_num = 0; /* Global sequence counter */
/* Use inetListen() — one call instead of 15 lines */
listenfd = inetListen(PORT_NUM, BACKLOG, &addrlen);
if (listenfd == -1) {
perror("inetListen"); exit(EXIT_FAILURE);
}
printf("Server ready on port %s\n", PORT_NUM);
for (;;) {
len = sizeof(claddr);
connfd = accept(listenfd, (struct sockaddr *)&claddr, &len);
if (connfd == -1) { perror("accept"); continue; }
initRlBuf(connfd, &rlbuf);
/* Read request: "N\n" where N = how many sequence numbers wanted */
if (readLineBuf(&rlbuf, line, sizeof(line)) <= 0) {
close(connfd); continue;
}
int req_len = atoi(line); /* Client wants req_len numbers */
if (req_len <= 0) {
close(connfd); continue;
}
/* Send the starting sequence number */
snprintf(line, sizeof(line), "%d\n", seq_num);
if (write(connfd, line, strlen(line)) != (ssize_t)strlen(line))
perror("write");
seq_num += req_len; /* Advance counter */
close(connfd);
}
return 0;
}
is_seqnum_cl.c — Client Using inetConnect()
/* is_seqnum_cl.c — Sequence Number Client */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "rlbuf.h"
#include "inet_sockets.h"
#define PORT_NUM "54321"
#define MAXLINE 128
int main(int argc, char *argv[])
{
int sockfd;
char line[MAXLINE];
RlBuf rlbuf;
int req_len;
if (argc != 3) {
fprintf(stderr, "Usage: %s host len\n", argv[0]);
exit(EXIT_FAILURE);
}
req_len = atoi(argv[2]);
if (req_len <= 0) {
fprintf(stderr, "len must be positive\n");
exit(EXIT_FAILURE);
}
/* Connect to server — one call instead of socket+fill+connect */
sockfd = inetConnect(argv[1], PORT_NUM, SOCK_STREAM);
if (sockfd == -1) {
perror("inetConnect"); exit(EXIT_FAILURE);
}
/* Send request: "N\n" */
snprintf(line, sizeof(line), "%d\n", req_len);
if (write(sockfd, line, strlen(line)) != (ssize_t)strlen(line)) {
perror("write"); exit(EXIT_FAILURE);
}
initRlBuf(sockfd, &rlbuf);
/* Read response */
if (readLineBuf(&rlbuf, line, sizeof(line)) <= 0) {
fprintf(stderr, "Server error or EOF\n");
exit(EXIT_FAILURE);
}
printf("Sequence number: %s", line);
printf("(reserved %d numbers: %d to %d)\n",
req_len, atoi(line), atoi(line) + req_len - 1);
close(sockfd);
return 0;
}
Build and Test
# Header file: inet_sockets.h
# int inetListen(const char *service, int backlog, socklen_t *addrlen);
# int inetConnect(const char *host, const char *service, int type);
gcc -Wall -o seqnum_sv is_seqnum_sv.c inet_sockets.c rlbuf.c
gcc -Wall -o seqnum_cl is_seqnum_cl.c inet_sockets.c rlbuf.c
# Terminal 1
./seqnum_sv
# Terminal 2 — request 5 numbers
./seqnum_cl localhost 5
Sequence number: 0
(reserved 5 numbers: 0 to 4)
# Terminal 2 — request 3 more numbers
./seqnum_cl localhost 3
Sequence number: 5
(reserved 3 numbers: 5 to 7)
Understanding getaddrinfo()
getaddrinfo() is the modern API that replaces gethostbyname(), getservbyname(), and manual struct filling. It resolves both hostname and service name, supports IPv4 and IPv6, and is thread-safe.
#include <netdb.h>
int getaddrinfo(
const char *node, /* hostname, IP string, or NULL (for servers) */
const char *service, /* port number as string, or service name */
const struct addrinfo *hints, /* constraints on result */
struct addrinfo **res /* OUTPUT: linked list of results */
);
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, etc. */
int ai_family; /* AF_INET, AF_INET6, AF_UNSPEC */
int ai_socktype; /* SOCK_STREAM, SOCK_DGRAM */
int ai_protocol; /* IPPROTO_TCP, IPPROTO_UDP, 0 */
socklen_t ai_addrlen; /* Size of ai_addr */
struct sockaddr *ai_addr; /* The address to use with socket APIs */
char *ai_canonname; /* Canonical hostname (if requested) */
struct addrinfo *ai_next; /* Next result in linked list */
};
/*
* IMPORTANT: Always call freeaddrinfo() when done — it's a linked list
* and getaddrinfo() allocates memory for it.
*/
/* Example: resolve "localhost:80" */
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* Accept IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM; /* TCP only */
int rc = getaddrinfo("localhost", "80", &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
/* NOTE: use gai_strerror(), not strerror(), for getaddrinfo errors */
}
/* Use res->ai_addr, res->ai_addrlen, res->ai_family etc. */
freeaddrinfo(res); /* Always free! */
| Flag | Meaning | Used By |
|---|---|---|
| AI_PASSIVE | node=NULL means “all interfaces” (INADDR_ANY) | Servers |
| AI_CANONNAME | Fill ai_canonname with the canonical hostname | Diagnostics |
| AI_NUMERICHOST | node must be a numeric IP string (no DNS lookup) | Performance |
| AI_NUMERICSERV | service must be a port number string | Performance |
Why SO_REUSEADDR Is Essential
When a TCP server closes, the OS keeps the port in a TIME_WAIT state for about 60-120 seconds (to handle any late-arriving packets). Without SO_REUSEADDR, if you restart your server during that window, bind() fails with EADDRINUSE — “Address already in use”. This is extremely annoying during development.
/* Without SO_REUSEADDR — this happens during development: */
$ ./seqnum_sv
Server ready on port 54321
^C
$ ./seqnum_sv
bind: Address already in use <-- Frustrating!
/* With SO_REUSEADDR (as in inetListen): */
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
/* Now restart works immediately */
/* SO_REUSEADDR allows: */
/* 1. Binding to a port in TIME_WAIT state */
/* 2. Multiple sockets bound to same port (with SO_REUSEPORT on Linux) */
/* NOTE: SO_REUSEADDR does NOT bypass "port already in use" if another */
/* live process is actively listening on that port. */
(server running)
(closing)
60-120s
SO_REUSEADDR: bind() fails while in TIME_WAIT.With
SO_REUSEADDR: bind() succeeds immediately.Interview Questions & Answers
getaddrinfo() resolves a hostname and service name into a list of socket addresses ready to use with socket() and connect()/bind(). It supports both IPv4 and IPv6 (unlike gethostbyname() which is IPv4-only), handles both name resolution and service lookup in one call, and is thread-safe (gethostbyname uses global state).
AI_PASSIVE tells getaddrinfo() that you’re setting up a server socket. When used with node=NULL, it returns an address with INADDR_ANY (0.0.0.0 for IPv4), meaning “accept connections on all network interfaces”. Without AI_PASSIVE, NULL resolves to loopback (127.0.0.1), which only accepts local connections.
getaddrinfo() can return multiple addresses — for example, both an IPv4 and an IPv6 address for a hostname, or addresses from multiple DNS records. The library tries each one with socket() and bind() until one succeeds. This makes the code robust across different network environments without hardcoding address families.
When a TCP server closes a connection, the port enters TIME_WAIT state for 60-120 seconds. Without SO_REUSEADDR, restarting the server during this window fails with EADDRINUSE. Setting SO_REUSEADDR before bind() allows reuse of a port in TIME_WAIT. It does NOT let you steal a port that another active server is using.
getaddrinfo() dynamically allocates a linked list of struct addrinfo results. You must call freeaddrinfo(result) when done to release this memory. Forgetting it causes a memory leak. Common mistake: calling freeaddrinfo() before finishing with the address data.
getaddrinfo() returns its own error codes (EAI_NONAME, EAI_FAIL, etc.) which are NOT errno values. strerror() only understands errno codes. Use gai_strerror(rc) to get human-readable error strings for getaddrinfo failures.
The backlog specifies the maximum number of pending (not yet accepted) connections the kernel will queue for a listening socket. If a client connects and the server hasn’t called accept() yet, the kernel holds it in this queue. Once the queue is full, new connection attempts are silently dropped or get a RST. Typical values: 5 to 128. On Linux, the actual limit is also capped by /proc/sys/net/core/somaxconn.
Yes. By setting hints.ai_family = AF_UNSPEC, getaddrinfo() returns both IPv4 and IPv6 addresses if available. The loop tries each address in order until one connects successfully. This makes the client automatically prefer IPv6 when available (since it’s typically returned first) without any code changes.
