What You Will Learn
Part 1 explained what each function does and how to use it. This part goes deeper — into how each function is actually implemented. Understanding the internals gives you two things: you can write similar helper libraries yourself, and you understand exactly what is happening inside those single-line calls.
The key insight in this implementation is the private helper function inetPassiveSocket(). Both inetListen() and inetBind() share most of their steps. Instead of duplicating code, both call this private helper.
Read through the entire source first, then the sections below explain each function in detail. The code uses _BSD_SOURCE to enable NI_MAXHOST and NI_MAXSERV which are BSD extensions to the POSIX standard.
/* inet_sockets.c */
#define _BSD_SOURCE /* Enables NI_MAXHOST and NI_MAXSERV from <netdb.h> */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include "inet_sockets.h" /* Our own header */
#include "tlpi_hdr.h" /* TLPI error-handling utilities */
/* --------------------------------------------------------
* inetConnect() - Client: create socket and connect to server
* -------------------------------------------------------- */
int
inetConnect(const char *host, const char *service, int type)
{
struct addrinfo hints;
struct addrinfo *result, *rp;
int sfd, s;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_family = AF_UNSPEC; /* Accept IPv4 or IPv6 */
hints.ai_socktype = type; /* SOCK_STREAM or SOCK_DGRAM */
s = getaddrinfo(host, service, &hints, &result);
if (s != 0) {
errno = ENOSYS; /* Map getaddrinfo error to an errno value */
return -1;
}
/* Walk the linked list of address results.
Try each one until a socket + connect() succeeds. */
for (rp = result; rp != NULL; rp = rp->ai_next) {
sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sfd == -1)
continue; /* socket() failed for this entry, try next */
if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* connect() succeeded! exit loop */
/* connect() failed, close this socket and try the next address */
close(sfd);
}
freeaddrinfo(result); /* Always free the list when done */
return (rp == NULL) ? -1 : sfd;
/* rp == NULL means the loop exhausted all addresses without success */
}
/* --------------------------------------------------------
* inetPassiveSocket() - PRIVATE HELPER
* Used internally by both inetListen() and inetBind()
* -------------------------------------------------------- */
static int
inetPassiveSocket(const char *service, int type, socklen_t *addrlen,
int doListen, int backlog)
{
struct addrinfo hints;
struct addrinfo *result, *rp;
int sfd, optval, s;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
hints.ai_socktype = type;
hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
hints.ai_flags = AI_PASSIVE; /* Wildcard IP address (0.0.0.0 or ::) */
/* NULL as first arg + AI_PASSIVE = bind to all interfaces */
s = getaddrinfo(NULL, service, &hints, &result);
if (s != 0)
return -1;
/* Walk the list; bind (and optionally listen) on first that works */
optval = 1;
for (rp = result; rp != NULL; rp = rp->ai_next) {
sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sfd == -1)
continue;
/* Allow rapid server restart without "Address already in use" */
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
close(sfd);
freeaddrinfo(result);
return -1;
}
if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
break; /* bind() succeeded */
/* bind() failed for this address, try next */
close(sfd);
}
if (rp == NULL) {
freeaddrinfo(result);
return -1; /* No address worked */
}
if (doListen) {
/* Only inetListen() wants us to call listen() */
if (listen(sfd, backlog) == -1) {
freeaddrinfo(result);
close(sfd);
return -1;
}
}
if (addrlen != NULL)
*addrlen = rp->ai_addrlen; /* Return address structure size */
freeaddrinfo(result);
return sfd;
}
/* --------------------------------------------------------
* inetListen() - TCP Server: listening socket
* -------------------------------------------------------- */
int
inetListen(const char *service, int backlog, socklen_t *addrlen)
{
/* doListen = 1 means the helper will call listen() */
return inetPassiveSocket(service, SOCK_STREAM, addrlen, 1, backlog);
}
/* --------------------------------------------------------
* inetBind() - UDP server/client: bound socket, no listen()
* -------------------------------------------------------- */
int
inetBind(const char *service, int type, socklen_t *addrlen)
{
/* doListen = 0 means no listen() call */
return inetPassiveSocket(service, type, addrlen, 0, 0);
}
/* --------------------------------------------------------
* inetAddressStr() - Convert sockaddr to readable string
* -------------------------------------------------------- */
char *
inetAddressStr(const struct sockaddr *addr, socklen_t addrlen,
char *addrStr, int addrStrLen)
{
char host[NI_MAXHOST], service[NI_MAXSERV];
/* getnameinfo() is the reverse of getaddrinfo():
it converts a binary address into hostname + service strings */
if (getnameinfo(addr, addrlen,
host, NI_MAXHOST,
service, NI_MAXSERV,
NI_NUMERICSERV) == 0) { /* NI_NUMERICSERV = port as number */
/* Format: "(hostname, port)" */
snprintf(addrStr, addrStrLen, "(%s, %s)", host, service);
} else {
snprintf(addrStr, addrStrLen, "(?UNKNOWN?)");
}
return addrStr; /* Pointer to the caller's buffer */
}
Let’s break down exactly what happens when you call inetConnect("example.com", "http", SOCK_STREAM):
| Step | What Happens | Details |
|---|---|---|
| 1 | Zero the hints struct | memset(&hints, 0, sizeof(struct addrinfo)) — clears all fields. Then specific fields are set. This is necessary because the struct has reserved fields that must be zero. |
| 2 | Set AF_UNSPEC | hints.ai_family = AF_UNSPEC — tells getaddrinfo() to return both IPv4 and IPv6 results. The function tries each in order, so if IPv6 is preferred by the OS, it appears first in the list. |
| 3 | Call getaddrinfo() | Resolves "example.com" + "http" (port 80) into a linked list of struct addrinfo entries. Each entry has a complete, ready-to-use socket address. On failure, returns a non-zero error code (not an errno value — that’s why we manually set errno = ENOSYS). |
| 4 | Loop: socket() + connect() | For each address result: create a socket, attempt connect(). If connect() fails (e.g. the host has an IPv6 address but the network doesn’t support it), close that socket and try the next entry. Break out of the loop on the first successful connect(). |
| 5 | Free the list | freeaddrinfo(result) must always be called after getaddrinfo(), whether or not the loop succeeded. This releases memory allocated by the resolver. Note: the pointer rp points into the now-freed list — but we only check if it’s NULL or not; we never dereference it after this point. |
| 6 | Return result | If rp == NULL, the loop ran out of addresses with no success → return -1. Otherwise return the connected socket file descriptor. |
Why does the function set errno = ENOSYS on getaddrinfo failure?
getaddrinfo() does not use errno. It returns its own error codes (EAI_NONAME, EAI_FAIL, etc.) via its return value. Since inetConnect() returns -1 to signal failure (following UNIX convention), callers who inspect errno after a -1 return would get whatever was in errno from before — potentially misleading. Setting errno = ENOSYS (Function not implemented) is a reasonable sentinel to indicate the failure came from address resolution, not from a system call.
static — it is not visible outside of inet_sockets.c. It is a private implementation detail, not part of the public API. Both inetListen() and inetBind() are thin wrappers around it.The doListen flag controls the difference:
/* How inetListen() calls the helper */
inetPassiveSocket(service, SOCK_STREAM, addrlen, 1, backlog);
/* ^ ^
doListen=1 means call listen()
backlog = caller's value */
/* How inetBind() calls the helper */
inetPassiveSocket(service, type, addrlen, 0, 0);
/* ^ ^
doListen=0 = no listen() call
backlog not used */
The AI_PASSIVE flag:
Notice that hints.ai_flags = AI_PASSIVE is set here, but not in inetConnect(). When getaddrinfo() is called with a NULL hostname and AI_PASSIVE, it returns the wildcard address:
- IPv4:
0.0.0.0— accept connections on all local interfaces - IPv6:
::(double colon) — same, but IPv6
This is exactly what a server wants: bind to all available network interfaces so clients can reach it from anywhere.
SO_REUSEADDR — Why it matters:
int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
When a server closes a TCP socket, the port enters a TIME_WAIT state for up to 2 minutes. During this time, trying to restart the server on the same port fails with EADDRINUSE (“Address already in use”). Setting SO_REUSEADDR before bind() tells the kernel to allow reuse of the port even if it’s in TIME_WAIT. This is standard practice for all server programs.
/* Without SO_REUSEADDR: */
/* Kill server → restart immediately → bind() fails: Address already in use */
/* With SO_REUSEADDR: */
/* Kill server → restart immediately → bind() succeeds */
Why bind to the first successful address, not loop through all?
A server only needs one bound socket per port. Unlike inetConnect() where we need to try multiple addresses because only one might work for connection, here we just need any one address that supports binding. The first one that succeeds is fine. (On dual-stack systems, the kernel typically handles both IPv4 and IPv6 clients through a single IPv6 wildcard socket.)
This function uses getnameinfo(), which is the reverse of getaddrinfo(). It takes a binary socket address and produces human-readable hostname and service strings.
char host[NI_MAXHOST]; /* Max hostname length (1025 on Linux) */
char service[NI_MAXSERV]; /* Max service name length (32 on Linux) */
int result = getnameinfo(
addr, /* Input: binary sockaddr structure */
addrlen, /* Input: size of that structure */
host, /* Output: hostname string buffer */
NI_MAXHOST, /* Size of hostname buffer */
service, /* Output: service/port string buffer */
NI_MAXSERV, /* Size of service buffer */
NI_NUMERICSERV /* Flag: return port as a number, not service name */
);
What the NI_NUMERICSERV flag does:
service = "http"
service = "ssh"
service = "domain"
service = "80"
service = "22"
service = "53"
Using NI_NUMERICSERV is preferred for logging because it always produces a numeric port, which is unambiguous. Service name lookup can fail or return unexpected names for custom ports.
What about NI_NUMERICHOST?
inetAddressStr() does not pass NI_NUMERICHOST, which means getnameinfo() will attempt a reverse DNS lookup to get the hostname. This can be slow (a network round-trip) if DNS is involved. If speed matters, you should add NI_NUMERICHOST to always get the IP address without DNS lookup:
/* Fast version: always returns IP address, no DNS lookup */
getnameinfo(addr, addrlen,
host, NI_MAXHOST,
service, NI_MAXSERV,
NI_NUMERICHOST | NI_NUMERICSERV);
/* Output: (192.168.1.10, 8080) instead of (myhost.local, 8080) */
The snprintf and truncation:
snprintf(addrStr, addrStrLen, "(%s, %s)", host, service);
snprintf() guarantees it will never write more than addrStrLen bytes (including the null terminator). If the formatted string would be longer, it is silently truncated. That is why the documentation says to always use IS_ADDR_STR_LEN (4096) as the buffer size — to avoid truncation.
This example shows how to build a full TCP echo server and client using the library. The server accepts connections, logs the client address, reads a line, and echoes it back.
Echo Server:
/* echo_server.c — uses the inet_sockets library */
#include "inet_sockets.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT "5000"
#define BACKLOG 10
#define BUFSIZE 256
int main(void)
{
socklen_t addrlen;
/* Step 1: Create listening TCP socket on port 5000 */
int lfd = inetListen(PORT, BACKLOG, &addrlen);
if (lfd == -1) {
perror("inetListen");
return 1;
}
printf("Server listening on port %s\n", PORT);
/* Allocate correctly-sized buffer for client address */
struct sockaddr *claddr = malloc(addrlen);
if (claddr == NULL) { perror("malloc"); return 1; }
for (;;) {
socklen_t len = addrlen;
/* Step 2: Accept an incoming connection */
int cfd = accept(lfd, claddr, &len);
if (cfd == -1) {
perror("accept");
continue;
}
/* Step 3: Log the client's address */
char addrStr[IS_ADDR_STR_LEN];
inetAddressStr(claddr, len, addrStr, IS_ADDR_STR_LEN);
printf("Connection from: %s\n", addrStr);
/* Step 4: Read from client and echo back */
char buf[BUFSIZE];
ssize_t n;
while ((n = read(cfd, buf, BUFSIZE)) > 0) {
if (write(cfd, buf, n) != n) {
perror("write");
break;
}
}
/* Step 5: Close client socket */
close(cfd);
printf("Connection from %s closed\n", addrStr);
}
free(claddr);
close(lfd);
return 0;
}
Echo Client:
/* echo_client.c — uses the inet_sockets library */
#include "inet_sockets.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define PORT "5000"
#define BUFSIZE 256
int main(int argc, char *argv[])
{
const char *host = (argc > 1) ? argv[1] : "localhost";
/* Step 1: Connect to the echo server */
int sfd = inetConnect(host, PORT, SOCK_STREAM);
if (sfd == -1) {
perror("inetConnect");
return 1;
}
printf("Connected to %s:%s\n", host, PORT);
/* Step 2: Read lines from stdin, send to server, print echo */
char buf[BUFSIZE];
while (fgets(buf, BUFSIZE, stdin) != NULL) {
size_t len = strlen(buf);
/* Send to server */
if (write(sfd, buf, len) != (ssize_t)len) {
perror("write");
break;
}
/* Read the echo */
char echo[BUFSIZE];
ssize_t n = read(sfd, echo, len);
if (n <= 0) break;
echo[n] = '\0';
printf("Echo: %s", echo);
}
close(sfd);
return 0;
}
Build and run:
# Compile (assuming inet_sockets.c and tlpi_hdr.h are available)
gcc -o echo_server echo_server.c inet_sockets.c -I.
gcc -o echo_client echo_client.c inet_sockets.c -I.
# Terminal 1: Start server
./echo_server
# Terminal 2: Connect and type messages
./echo_client localhost
Hello World! # you type this
Echo: Hello World! # server echoes back
static on a function definition limits its visibility to the current translation unit (i.e., to inet_sockets.c only). This enforces encapsulation — callers using the library cannot accidentally call inetPassiveSocket() directly since it is not declared in the header and not visible as a symbol. If it were not static, it would be exported as a global symbol, potentially causing name collisions with other code that might define a function with the same name.getaddrinfo() result list: (a) A hostname that has both IPv4 and IPv6 DNS records (dual-stack) — the list will contain both a struct sockaddr_in and a struct sockaddr_in6 entry. (b) A hostname that resolves to multiple IP addresses for load balancing (e.g., large web services often have many A records). (c) Round-robin DNS where each query returns a different ordering. The loop ensures the client tries each available address before giving up, which makes the connection attempt more robust.TIME_WAIT state for 2×MSL (Maximum Segment Lifetime, typically 60 seconds) to ensure any delayed packets from the old connection are discarded before a new connection reuses the same port+address combination. SO_REUSEADDR allows bind() to succeed even if the port is in TIME_WAIT, bypassing this wait for server processes. It is safe because the new server connection will have different sequence numbers, so the kernel can distinguish it from any delayed packets from the old connection. It does not allow two sockets to bind the same port simultaneously — SO_REUSEPORT does that (and requires more care).getaddrinfo() returns its own error codes (like EAI_NONAME, EAI_AGAIN, EAI_FAIL) via its return value, not through errno. To print a descriptive error message, you use gai_strerror(retval). The library sets errno = ENOSYS as a way to leave some indicator in errno that the failure was from address resolution, since callers may check errno after getting -1. In production code, you would typically want to preserve the original getaddrinfo() error code and use gai_strerror() for user-facing messages. The choice of ENOSYS here is a simplification in the TLPI example.memset(&hints, 0, ...), all fields are already zero/NULL. Setting these fields explicitly to NULL is defensive programming — it makes the intent explicit and documents that these fields are intentionally not being used. The ai_canonname field would be populated if AI_CANONNAME were set in ai_flags, which it isn’t here. The explicit NULL assignments are also a guard against future changes where someone might add code between the memset and getaddrinfo() that accidentally sets these fields.freeaddrinfo(result), rp does point to freed memory. However, the only use of rp after that point is the null-check: return (rp == NULL) ? -1 : sfd. Since rp is a local variable holding an address value (a number), checking whether it is NULL does not dereference it and is perfectly safe — the value of a pointer variable is not affected by freeing the memory it points to. Dereferencing rp after freeaddrinfo() would be undefined behavior, but the code does not do that.socket() requires you to already know the address family (AF_INET vs AF_INET6) before you can create the socket. To know the address family you must first resolve the hostname. If you hardcode AF_INET, your code only works with IPv4. The library’s approach — using getaddrinfo() first with AF_UNSPEC, then creating the socket using the family returned by getaddrinfo — is the correct protocol-independent pattern. You cannot skip the resolution step without losing IPv6 support or the ability to handle hostnames (as opposed to raw IP addresses).