Sockets: Internet Domains Chapter Summary, Key Concepts, and Practice Exercises

 

Chapter 59 – Sockets: Internet Domains
Part 4: Chapter Summary, Key Concepts, and Practice Exercises
59.16
Summary
59.17
Exercises
DNS
+ TCP/UDP

Chapter 59 Series — Complete
Part 1: gethostbyname() Part 2: getservbyname() Part 3: UNIX vs Internet Sockets ▶ Part 4: Summary

Everything Covered in Chapter 59
Internet domain sockets IPv4 / IPv6 addressing TCP stream sockets UDP datagram sockets byte order (endianness) data marshalling IP address conversion DNS getaddrinfo() gethostbyname() getservbyname() UNIX vs Internet sockets

What Chapter 59 Is About (Big Picture)

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?

IPv4 (32-bit), IPv6 (128-bit), port numbers, byte order
🔍

How do you resolve names?

DNS, /etc/hosts, getaddrinfo(), gethostbyname()
⚖️

Which socket domain?

UNIX vs Internet for local IPC

Concept 1: Internet Domain Socket Addresses

An Internet domain socket address has two components: an IP address and a port number. Together they uniquely identify an endpoint on the network.

IPv4 Address (struct sockaddr_in)
sin_family = AF_INET
sin_port = htons(80)
sin_addr = 192.168.1.5
(32-bit, 4 bytes)
vs
IPv6 Address (struct sockaddr_in6)
sin6_family = AF_INET6
sin6_port = htons(80)
sin6_addr = 2001:db8::1
(128-bit, 16 bytes)
/* 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);

Concept 2: TCP Stream vs UDP Datagram Sockets
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

Concept 3: Byte Order and Data Marshalling

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.

Integer 0x12345678 stored in memory:
Little-endian (x86, ARM)
0x78
addr+0
0x56
addr+1
0x34
addr+2
0x12
addr+3
LSB (least significant byte) first
Big-endian (Network byte order)
0x12
addr+0
0x34
addr+1
0x56
addr+2
0x78
addr+3
MSB (most significant byte) first

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.

Concept 4: IP Address Conversion Functions

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"       */
Prefer inet_pton / inet_ntop over older functions: The older 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.

Concept 5: DNS — The Distributed Hostname Database

DNS (Domain Name System) is how hostnames like www.google.com get resolved to IP addresses. It is a distributed, hierarchical system:

Root DNS Servers (“.”)
.com TLD server
.org TLD server
.in TLD server
google.com zone
example.com zone

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                  */

Concept 6: Modern API — getaddrinfo() and getnameinfo()

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 */
}
Note on error handling: 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).

Exercise 59-1: Buffered readline() — Concept and Approach

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.

Chapter 59 — Complete Concept Summary
Internet domain socket address = IP address + port number. IPv4 = 32-bit address. IPv6 = 128-bit address.
Internet domain datagram sockets use UDP — connectionless, unreliable, message-oriented.
Internet domain stream sockets use TCP — reliable, bidirectional, byte-stream communication.
Byte order problem: Different architectures use different byte orderings. Network byte order is big-endian. Always use htons(), ntohs(), htonl(), ntohl() for port and address values.
Data marshalling: For application data, text encoding with delimiter characters (often newline) is a simple and portable solution.
IP address conversion: Use inet_pton() and inet_ntop() for converting between binary and string forms. They support both IPv4 and IPv6.
Hostname resolution: Prefer getaddrinfo() for new code. Historical functions gethostbyname() and getservbyname() are obsolete (not thread-safe, IPv4-centric).
DNS: Distributed hierarchical database. Local zone administrators manage their portion. DNS servers communicate to resolve hostnames outside their zone.
UNIX vs Internet sockets for local IPC: Internet sockets work everywhere (simpler). UNIX sockets offer better performance, file-permission-based access control, file descriptor passing, and credential passing for local-only use cases.

Chapter 59 — All Interview Questions
Q1: What two pieces of information make up an Internet domain socket address?

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

Q2: What is the difference between TCP and UDP in the context of Internet domain sockets?

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

Q3: Why do we need byte-order conversion functions like htons() and htonl()?

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.

Q4: What is the difference between inet_pton() and inet_ntop()?

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.

Q5: What is DNS and why is it described as “distributed”?

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.

Q6: Why is getaddrinfo() preferred over gethostbyname() and getservbyname()?

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.

Q7: What are the three main advantages of UNIX domain sockets over Internet domain sockets for local IPC?

(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.

Q8: What simple strategy do many applications use to handle byte-order differences in application data (not addresses)?

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.

Q9: What does gai_strerror() do and when would you use it?

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

Q10: After calling getaddrinfo(), what must you always do with the result before returning from a function?

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.

Further Study Resources

The TLPI text (Section 59.15) recommends these resources for deeper study on TCP/IP and socket programming:

Linux manual pagessocket(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.
GNU C Library manual — Available at https://www.gnu.org/. Has an extensive chapter on the sockets API covering all the functions discussed in Chapter 59.
RFC documents — The definitive specifications. Key RFCs: RFC 791 (IPv4), RFC 793 (TCP), RFC 768 (UDP), RFC 1034/1035 (DNS). See Section 58.7 of TLPI for a complete RFC list.

Chapter 59 Series Complete!

You have covered host resolution, service resolution, the UNIX vs Internet socket choice, and all major Chapter 59 concepts. Keep practicing with the exercises.

← Back to Part 1 ← Part 3

Leave a Reply

Your email address will not be published. Required fields are marked *