Chapter 59 covers the full picture of how to write networked programs using Internet domain sockets in C on Linux. It answers three big questions:
How are addresses structured?
How do you resolve names?
Which socket domain?
An Internet domain socket address has two components: an IP address and a port number. Together they uniquely identify an endpoint on the network.
/* Filling in an IPv4 socket address */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); /* Port: host to network byte order */
addr.sin_addr.s_addr = INADDR_ANY; /* 0.0.0.0 — all local interfaces */
/* Or a specific IP: */
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
| Property | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) |
|---|---|---|
| Connection | Connection-oriented (connect/accept) | Connectionless |
| Reliability | Reliable — retransmits lost data | Unreliable — no retransmit |
| Order | Ordered delivery guaranteed | May arrive out of order |
| Communication model | Byte stream (no message boundaries) | Message-oriented (preserves boundaries) |
| Use cases | HTTP, SSH, FTP, email | DNS, VoIP, video streaming, games |
Different CPU architectures store multi-byte integers differently. For example, x86 uses little-endian (least significant byte first), while network byte order is big-endian (most significant byte first). This causes problems when two machines with different architectures communicate.
Solution — always convert port and address values:
/* Host-to-network and network-to-host conversion functions */
/* For 16-bit values (port numbers): */
uint16_t htons(uint16_t host_short); /* host to network */
uint16_t ntohs(uint16_t net_short); /* network to host */
/* For 32-bit values (IPv4 addresses): */
uint32_t htonl(uint32_t host_long); /* host to network */
uint32_t ntohl(uint32_t net_long); /* network to host */
/* Example: */
struct sockaddr_in addr;
addr.sin_port = htons(8080); /* ALWAYS use htons() for ports */
addr.sin_addr.s_addr = htonl(INADDR_ANY); /* or use INADDR_ANY directly */
Data marshalling: For application-level data (not just addresses), a common and simple approach is to encode all transmitted data as text (e.g., newline-delimited fields). This avoids byte-order and struct-packing problems between different architectures.
You need to convert between human-readable IP strings and binary form for use in socket addresses:
#include <arpa/inet.h>
/* String (dotted-decimal or hex) → binary */
int inet_pton(int af, const char *src, void *dst);
/* af: AF_INET or AF_INET6
src: "192.168.1.1" (IPv4) or "2001:db8::1" (IPv6)
dst: pointer to in_addr or in6_addr
Returns: 1 on success, 0 if string is not valid, -1 on error */
/* Binary → string */
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/* Returns: pointer to dst string on success, NULL on error */
/* Use these buffer size macros: */
char buf4[INET_ADDRSTRLEN]; /* 16 bytes, enough for "xxx.xxx.xxx.xxx\0" */
char buf6[INET6_ADDRSTRLEN]; /* 46 bytes, enough for full IPv6 string */
/* Examples: */
struct in_addr addr4;
inet_pton(AF_INET, "10.0.0.1", &addr4); /* string to binary */
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr4, str, sizeof(str)); /* binary to string */
printf("Address: %s\n", str); /* "10.0.0.1" */
inet_aton(), inet_addr(), and inet_ntoa() only work with IPv4. inet_pton() and inet_ntop() work with both IPv4 and IPv6 — use them for new code.DNS (Domain Name System) is how hostnames like www.google.com get resolved to IP addresses. It is a distributed, hierarchical system:
Each level delegates responsibility to the next. Local zone administrators manage only their portion of the namespace. DNS servers talk to each other to resolve names outside their zones. This distributed design means no single organization controls the whole internet namespace.
Resolution order on Linux:
/* Linux checks in this order (configured in /etc/nsswitch.conf): */
/* 1. /etc/hosts — local static mappings (fastest) */
/* 2. DNS servers — listed in /etc/resolv.conf */
/* /etc/hosts example: */
/* 127.0.0.1 localhost */
/* 192.168.1.10 mydev.local */
/* /etc/resolv.conf example: */
/* nameserver 8.8.8.8 <- Google's DNS */
/* nameserver 8.8.4.4 <- Google's backup DNS */
The modern chapter recommendation is to use getaddrinfo() instead of gethostbyname() and getservbyname(). It resolves both host and service in one call, is thread-safe, and is protocol-independent.
#include <netdb.h>
#include <sys/socket.h>
/* getaddrinfo: translate hostname + service into a list of socket addresses */
int getaddrinfo(const char *host, /* hostname or IP string */
const char *service, /* service name or port string */
const struct addrinfo *hints, /* filter criteria */
struct addrinfo **result); /* output: linked list */
/* Always free the result when done: */
void freeaddrinfo(struct addrinfo *result);
/* The returned addrinfo structure: */
struct addrinfo {
int ai_flags; /* Input flags */
int ai_family; /* AF_INET, AF_INET6, or AF_UNSPEC */
int ai_socktype; /* SOCK_STREAM or SOCK_DGRAM */
int ai_protocol; /* Protocol (usually 0) */
socklen_t ai_addrlen; /* Length of ai_addr */
struct sockaddr *ai_addr; /* Socket address (cast to use) */
char *ai_canonname; /* Canonical name (if requested) */
struct addrinfo *ai_next; /* Next entry in linked list */
};
/* Example: connect to www.example.com on port 80 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
int connect_to_web_server(const char *host)
{
struct addrinfo hints, *result, *rp;
int sfd, ret;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* Accept IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM; /* We want TCP */
ret = getaddrinfo(host, "http", &hints, &result);
if (ret != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return -1;
}
/* Try each address in the returned list */
for (rp = result; rp != NULL; rp = rp->ai_next) {
sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sfd == -1)
continue;
if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
break; /* Connected successfully */
close(sfd);
}
freeaddrinfo(result); /* Must free the list when done */
if (rp == NULL) {
fprintf(stderr, "Could not connect to %s\n", host);
return -1;
}
return sfd; /* Return connected socket */
}
getaddrinfo() returns 0 on success and a non-zero error code on failure. Use gai_strerror(ret) to get the error string — not strerror(errno) and not hstrerror(h_errno).The exercise in section 59.17 points out that the basic readLine() function — which reads one character at a time with a system call per character — is very inefficient. The challenge is to build a buffered version.
The idea is to read a large block into an internal buffer, then extract one line at a time from the buffer without system calls. Only when the buffer is empty do you call read() again.
/* Concept: Buffered line reader */
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define RLBUF_SIZE 4096
typedef struct {
int fd;
char buf[RLBUF_SIZE];
int buf_pos; /* Next unread position in buf */
int buf_end; /* One past last valid byte in buf */
} RlBuf;
/* Initialize the buffered reader for file descriptor fd */
void readLineBufInit(int fd, RlBuf *rlbuf)
{
rlbuf->fd = fd;
rlbuf->buf_pos = 0;
rlbuf->buf_end = 0;
}
/* Read one line into 'line' (up to maxlen-1 chars + '\0').
Returns number of bytes in line (including '\n' if present),
0 on EOF, -1 on error. */
ssize_t readLineBuffered(RlBuf *rlbuf, char *line, size_t maxlen)
{
size_t i = 0;
for (;;) {
/* If buffer is empty, refill it */
if (rlbuf->buf_pos >= rlbuf->buf_end) {
ssize_t n = read(rlbuf->fd, rlbuf->buf, RLBUF_SIZE);
if (n == 0)
return (i == 0) ? 0 : (ssize_t)i; /* EOF */
if (n == -1)
return -1;
rlbuf->buf_pos = 0;
rlbuf->buf_end = (int)n;
}
/* Copy next character from buffer to output */
char c = rlbuf->buf[rlbuf->buf_pos++];
if (i < maxlen - 1)
line[i++] = c;
if (c == '\n')
break;
}
line[i] = '\0';
return (ssize_t)i;
}
Why this matters: The naive one-byte-at-a-time approach requires one read() system call per character. A system call involves switching from user mode to kernel mode and back — a significant overhead. With a buffer of 4096 bytes, you amortize that cost across up to 4096 characters, making it orders of magnitude faster for large inputs.
htons(), ntohs(), htonl(), ntohl() for port and address values.inet_pton() and inet_ntop() for converting between binary and string forms. They support both IPv4 and IPv6.getaddrinfo() for new code. Historical functions gethostbyname() and getservbyname() are obsolete (not thread-safe, IPv4-centric).An IP address (32-bit for IPv4, 128-bit for IPv6) and a port number (16-bit). Together they uniquely identify a communication endpoint on a network. They are stored in struct sockaddr_in (IPv4) or struct sockaddr_in6 (IPv6).
TCP (SOCK_STREAM) provides a reliable, ordered, bidirectional byte-stream. Data is guaranteed to arrive in order and retransmitted if lost. UDP (SOCK_DGRAM) is connectionless and unreliable — datagrams may be lost, reordered, or duplicated. UDP preserves message boundaries; TCP does not (it is a stream).
Different CPU architectures store multi-byte integers in different byte orders (big-endian or little-endian). Network protocols define big-endian as the standard (network byte order). Functions like htons() convert a 16-bit value from host byte order to network byte order, ensuring that the value is interpreted correctly on any architecture.
inet_pton() converts a presentation (string) IP address to binary network form — “p” stands for presentation, “n” for network. inet_ntop() does the reverse — network binary to presentation string. Both support IPv4 and IPv6. Older functions like inet_aton() only support IPv4.
DNS (Domain Name System) is a system for mapping hostnames to IP addresses. It is distributed because no single server stores the entire database. Instead, the namespace is divided into zones, each managed by a local administrator. DNS servers communicate with each other to resolve names outside their own zone, and the hierarchy (root → TLD → domain) distributes management responsibility.
getaddrinfo() is thread-safe (allocates result dynamically, not in a static buffer), resolves both host and service in a single call, is protocol-independent (supports IPv4, IPv6, and any future protocol), and returns a linked list of results covering all matching addresses and socket types. The older functions are not thread-safe and are IPv4-centric.
(1) Performance — no TCP/IP stack overhead; data is copied directly through kernel buffers. (2) Access control — the socket appears as a file, so standard Unix file permissions restrict who can connect. (3) Advanced features — support for passing open file descriptors (SCM_RIGHTS) and verified peer credentials (SCM_CREDENTIALS / SO_PEERCRED) between processes.
Encoding all transmitted data as text with delimiter characters (usually newlines). Text is unambiguous across architectures — there is no question of byte order for characters. Fields are delimited by a known character so both sides can parse the data. This is simpler than implementing a binary marshalling scheme and handles data type size differences as well.
gai_strerror() converts an error code returned by getaddrinfo() into a human-readable string. Unlike most system calls which set errno, getaddrinfo() returns its own error codes (like EAI_NONAME, EAI_AGAIN, EAI_FAIL). You must use gai_strerror(ret) to decode them, not strerror(errno).
Call freeaddrinfo(result) to free the dynamically allocated linked list of struct addrinfo records. Unlike gethostbyname() which used a static buffer, getaddrinfo() allocates heap memory for each call. Failing to free it causes a memory leak.
The TLPI text (Section 59.15) recommends these resources for deeper study on TCP/IP and socket programming:
socket(7), ip(7), tcp(7), udp(7), raw(7), packet(7). Read these with man 7 tcp etc. They are very detailed and cover Linux-specific behavior.https://www.gnu.org/. Has an extensive chapter on the sockets API covering all the functions discussed in Chapter 59.You have covered host resolution, service resolution, the UNIX vs Internet socket choice, and all major Chapter 59 concepts. Keep practicing with the exercises.
