IPv6 UDP Sockets
Intermediate
TLPI Ch. 59
What This Section Covers
In this section, we look at a complete real-world example: a UDP datagram server and client using IPv6 (AF_INET6). The server listens on all interfaces, receives a message, converts it to uppercase, and sends it back. The client sends a message and prints the response.
This is a direct adaptation of the UNIX domain datagram example from earlier chapters (Section 57.3), now rewritten for the internet domain with IPv6. The concepts are the same — the key differences are in the socket address structure and the address family used.
You will also see how inet_pton() and inet_ntop() from Part 1 are used in a real program.
Key Terms
For IPv4, we use struct sockaddr_in. For IPv6, we use struct sockaddr_in6, defined in <netinet/in.h>.
#include <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; /* Always AF_INET6 */
in_port_t sin6_port; /* Port in network byte order */
uint32_t sin6_flowinfo; /* IPv6 flow info (usually 0) */
struct in6_addr sin6_addr; /* 128-bit IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (usually 0) */
};
struct in6_addr {
uint8_t s6_addr[16]; /* 128 bits = 16 bytes */
};
| Field | IPv4 (sockaddr_in) | IPv6 (sockaddr_in6) |
|---|---|---|
| Family field | sin_family = AF_INET |
sin6_family = AF_INET6 |
| Port field | sin_port |
sin6_port |
| Address field | sin_addr (struct in_addr, 4 bytes) |
sin6_addr (struct in6_addr, 16 bytes) |
| Wildcard address | INADDR_ANY |
in6addr_any |
| Loopback address | 127.0.0.1 |
::1 |
| Address string size | INET_ADDRSTRLEN = 16 |
INET6_ADDRSTRLEN = 46 |
Important: in6addr_any is a global constant (not a macro) of type struct in6_addr. It represents the IPv6 wildcard address :: (all zeros), meaning the socket will accept packets on any network interface.
/* Wildcard address constants */
extern const struct in6_addr in6addr_any; /* :: */
extern const struct in6_addr in6addr_loopback; /* ::1 */
Both the server and client share a common header that defines the port number and the maximum message size. This is a clean practice: if you change the port, you change it in one place.
/* i6d_ucase.h - shared by server and client */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <ctype.h>
#define BUF_SIZE 10 /* Maximum message size in bytes */
#define PORT_NUM 50002 /* UDP port the server listens on */
Note: BUF_SIZE 10 is intentionally small to demonstrate message size limits. In a real application, this would be much larger (e.g., 4096 or 65507 for UDP).
The server: creates a UDP socket, binds to any interface on port 50002, then loops forever receiving messages, converting them to uppercase, and sending them back.
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUF_SIZE 10
#define PORT_NUM 50002
int main(void)
{
struct sockaddr_in6 svaddr; /* Server's address structure */
struct sockaddr_in6 claddr; /* Client's address (filled by recvfrom) */
int sfd;
ssize_t numBytes;
socklen_t len;
char buf[BUF_SIZE];
char claddrStr[INET6_ADDRSTRLEN]; /* For printing client IP */
int j;
/* Step 1: Create UDP socket in the IPv6 domain */
sfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
/* Step 2: Fill in server address structure */
memset(&svaddr, 0, sizeof(struct sockaddr_in6));
svaddr.sin6_family = AF_INET6;
svaddr.sin6_addr = in6addr_any; /* Accept on ALL interfaces */
svaddr.sin6_port = htons(PORT_NUM); /* htons: host-to-network byte order */
/* Step 3: Bind socket to the address */
if (bind(sfd,
(struct sockaddr *) &svaddr,
sizeof(struct sockaddr_in6)) == -1) {
perror("bind");
return 1;
}
printf("Server listening on UDP port %d ...\n", PORT_NUM);
/* Step 4: Receive messages in a loop */
for (;;) {
len = sizeof(struct sockaddr_in6);
/*
* recvfrom() blocks until a datagram arrives.
* It fills claddr with the SENDER's address and port.
* We don't need to bind() on the client side for this to work --
* the kernel assigns an ephemeral port to unbound clients.
*/
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numBytes == -1) {
perror("recvfrom");
return 1;
}
/* Step 5: Convert client's binary IPv6 address to string for logging */
if (inet_ntop(AF_INET6,
&claddr.sin6_addr,
claddrStr,
INET6_ADDRSTRLEN) == NULL) {
printf("Couldn't convert client address to string\n");
} else {
printf("Server received %ld bytes from (%s, %u)\n",
(long) numBytes,
claddrStr,
ntohs(claddr.sin6_port)); /* ntohs: network-to-host byte order */
}
/* Step 6: Convert message to uppercase in-place */
for (j = 0; j < numBytes; j++)
buf[j] = toupper((unsigned char) buf[j]);
/* Step 7: Send the uppercase message back to the client */
if (sendto(sfd, buf, numBytes, 0,
(struct sockaddr *) &claddr, len) != numBytes) {
perror("sendto");
return 1;
}
}
return 0; /* Never reached */
}
Key Points Explained
| Point | Explanation |
|---|---|
in6addr_any |
Wildcard: server accepts datagrams on any network interface (all IPs on the machine) |
htons(PORT_NUM) |
Converts port from host byte order to network byte order (big-endian) |
recvfrom(..., &claddr, &len) |
Fills claddr with the sender’s address — the server needs this to reply |
inet_ntop(AF_INET6, ...) |
Converts binary IPv6 address to printable string for logging — no DNS needed |
ntohs(claddr.sin6_port) |
Converts port from network byte order back to host order for display |
toupper() |
Converts each byte to uppercase — this is the server’s “business logic” |
sendto(..., &claddr, len) |
Sends reply back to the client using the address captured by recvfrom |
The client takes the server’s IPv6 address as a command-line argument and sends each remaining argument as a separate datagram. It prints the uppercase response from the server.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUF_SIZE 10
#define PORT_NUM 50002
int main(int argc, char *argv[])
{
struct sockaddr_in6 svaddr;
int sfd;
ssize_t numBytes;
size_t msgLen;
char resp[BUF_SIZE];
int j;
if (argc < 3) {
fprintf(stderr, "Usage: %s server-IPv6-addr msg...\n", argv[0]);
return 1;
}
/* Step 1: Create UDP socket in IPv6 domain */
sfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sfd == -1) {
perror("socket");
return 1;
}
/*
* IMPORTANT: The client does NOT call bind().
* When sendto() is first called on an unbound UDP socket,
* the kernel automatically assigns an ephemeral port.
* The server sees this ephemeral port via recvfrom() and
* uses it to send the reply back.
*/
/* Step 2: Fill in server address using inet_pton() to convert argv[1] */
memset(&svaddr, 0, sizeof(struct sockaddr_in6));
svaddr.sin6_family = AF_INET6;
svaddr.sin6_port = htons(PORT_NUM);
/*
* inet_pton() converts the IPv6 string from command line
* into binary form and stores it in svaddr.sin6_addr
*/
if (inet_pton(AF_INET6, argv[1], &svaddr.sin6_addr) != 1) {
fprintf(stderr, "inet_pton: invalid IPv6 address: %s\n", argv[1]);
return 1;
}
/* Step 3: Send each remaining argument as a separate datagram */
for (j = 2; j < argc; j++) {
msgLen = strlen(argv[j]);
/* Send the message to the server */
if (sendto(sfd, argv[j], msgLen, 0,
(struct sockaddr *) &svaddr,
sizeof(struct sockaddr_in6)) != (ssize_t) msgLen) {
perror("sendto");
return 1;
}
/* Step 4: Wait for and receive the uppercase response */
numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
/*
* We pass NULL for the sender address in recvfrom() because
* we don't care about the server's address (we already know it).
*/
if (numBytes == -1) {
perror("recvfrom");
return 1;
}
printf("Response %d: %.*s\n", j - 1, (int) numBytes, resp);
}
return 0;
}
| SERVER | CLIENT | |
|---|---|---|
| 1. socket(AF_INET6, SOCK_DGRAM, 0) | ||
| 2. bind(sfd, &svaddr, …) — port 50002 | ||
| 3. recvfrom() — blocks, waiting… | 1. socket(AF_INET6, SOCK_DGRAM, 0) | |
| → | 2. inet_pton(AF_INET6, “::1”, &svaddr.sin6_addr) | |
| → | 3. sendto(sfd, “ciao”, …) — kernel assigns ephemeral port 32770 | |
| 4. recvfrom() returns — claddr = (::1, port 32770) | 4. recvfrom() — blocks, waiting for response… | |
| 5. inet_ntop() → prints “Server received 4 bytes from (::1, 32770)” | ||
| 6. toupper(“ciao”) → “CIAO” | ||
| 7. sendto(sfd, “CIAO”, …, &claddr) → sends to port 32770 | ← | |
| 8. recvfrom() — blocks again for next datagram | 5. recvfrom() returns “CIAO” → prints “Response 1: CIAO” |
Shell Session Output
$ ./i6d_ucase_sv &
[1] 31047
$ ./i6d_ucase_cl ::1 ciao
Server received 4 bytes from (::1, 32770)
Response 1: CIAO
The ::1 argument is the IPv6 loopback address (equivalent to IPv4’s 127.0.0.1). It means “send to myself” — both server and client run on the same machine.
In the UNIX domain version of this example (Section 57.3), the client had to call bind() to a path in the filesystem so the server could reply to it. In the Internet domain, this is not necessary.
| UNIX Domain Client
Must call |
vs | Internet Domain Client
Does NOT need to call |
An ephemeral port is a temporary port number assigned by the kernel from the ephemeral port range (typically 32768–60999 on Linux). It exists only for the lifetime of the socket. You can check the range on your system:
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
| Aspect | IPv4 (AF_INET) | IPv6 (AF_INET6) |
|---|---|---|
| Socket creation | socket(AF_INET, SOCK_DGRAM, 0) |
socket(AF_INET6, SOCK_DGRAM, 0) |
| Address structure | struct sockaddr_in |
struct sockaddr_in6 |
| Address size | sizeof(struct sockaddr_in) |
sizeof(struct sockaddr_in6) |
| Wildcard address | INADDR_ANY |
in6addr_any |
| Loopback address string | “127.0.0.1” | “::1” |
| Address printing | inet_ntop(AF_INET, ...) |
inet_ntop(AF_INET6, ...) |
| Address buffer size | INET_ADDRSTRLEN (16) |
INET6_ADDRSTRLEN (46) |
| Address assignment field | sin_addr (in_addr) |
sin6_addr (in6_addr) |
The SOCK_DGRAM, bind(), sendto(), recvfrom(), htons(), and ntohs() calls are identical between IPv4 and IPv6 — only the address family and structures differ.
When you don’t know at compile time whether you’ll be dealing with IPv4 or IPv6, use struct sockaddr_storage. It is large enough to hold either sockaddr_in or sockaddr_in6. It is particularly useful in servers that want to accept both.
#include <sys/socket.h>
struct sockaddr_storage ss; /* Big enough for any socket address */
socklen_t len = sizeof(ss);
/* Use in recvfrom() - works for both IPv4 and IPv6 clients */
recvfrom(sfd, buf, BUF_SIZE, 0, (struct sockaddr *) &ss, &len);
/* Then inspect ss.ss_family to decide which type it is */
if (ss.ss_family == AF_INET) {
struct sockaddr_in *p4 = (struct sockaddr_in *) &ss;
/* use p4->sin_addr etc. */
} else if (ss.ss_family == AF_INET6) {
struct sockaddr_in6 *p6 = (struct sockaddr_in6 *) &ss;
/* use p6->sin6_addr etc. */
}
| Structure | Size | Use Case |
|---|---|---|
struct sockaddr_in |
16 bytes | IPv4 only programs |
struct sockaddr_in6 |
28 bytes | IPv6 only programs |
struct sockaddr_storage |
128 bytes | Dual-stack (IPv4 + IPv6) programs |
A: The IPv6 equivalent is in6addr_any, which represents the address :: (all zeros, 128 bits). When a server binds to this address, it accepts incoming datagrams/connections on any of the machine’s network interfaces and any of its IP addresses.
A: The client does not call bind() because the Linux kernel automatically assigns an ephemeral port to the client socket when sendto() is first called. The server’s recvfrom() call captures the client’s socket address (IP + ephemeral port) in the claddr structure. The server then uses this address in sendto() to reply.
A: sockaddr_in is the IPv4 socket address structure with a 32-bit sin_addr field. sockaddr_in6 is the IPv6 structure with a 128-bit sin6_addr field (plus sin6_flowinfo and sin6_scope_id). You must use sockaddr_in6 when creating AF_INET6 sockets.
A: ::1 is the IPv6 loopback address. It is the IPv6 equivalent of IPv4’s 127.0.0.1. When you send a packet to ::1, it loops back to the same machine without going onto the network. It is used for testing network programs locally.
A: An ephemeral port is a temporary port number assigned by the kernel to a socket that has not explicitly called bind(). On Linux, they are drawn from the range 32768–60999 by default (configurable via /proc/sys/net/ipv4/ip_local_port_range). They are released when the socket is closed.
A: The sockaddr_in6 structure has padding fields and sin6_flowinfo / sin6_scope_id that are not always explicitly set. If they contain garbage values from the stack, bind() may fail or behave unexpectedly. Using memset(&svaddr, 0, sizeof(...)) guarantees all fields start at zero, which is the correct default for these optional fields.
A: struct sockaddr_storage is a generic, large socket address structure (128 bytes) that is big enough to hold any socket address type — sockaddr_in, sockaddr_in6, or sockaddr_un. Use it when writing code that needs to handle both IPv4 and IPv6 clients, or when the address family is not known at compile time. You inspect ss.ss_family to determine the actual type and cast appropriately.
A: htons() converts a 16-bit value from host byte order to network byte order (big-endian). ntohs() does the reverse. They are needed because x86 systems are little-endian (store low byte first) while the network protocol requires big-endian (high byte first). Port numbers must always be stored in network byte order in socket address structures. Not using these functions on little-endian machines (like x86) will silently produce wrong port numbers.
A: The kernel would assign an ephemeral port to the server socket automatically (just as it does for unbound clients). But clients would have no way to know which port to send to, since the server port is supposed to be well-known and pre-agreed. This is why servers always call bind() explicitly to a fixed, known port number.
A: Yes, passing NULL for the sender address (and corresponding length pointer) in recvfrom() is safe and valid. It simply means the caller does not want to know the sender’s address. The client already knows the server’s address (it used it in sendto()), so there is no need to capture it again.
| Topic | Function/Constant | Key Point |
|---|---|---|
| String → Binary | inet_pton() |
Returns 1/0/-1; works for IPv4 and IPv6 |
| Binary → String | inet_ntop() |
Thread-safe; use INET6_ADDRSTRLEN buffer |
| Hostname resolution | getaddrinfo() |
Modern, IPv4+IPv6, thread-safe; must freeaddrinfo() |
| Reverse resolution | getnameinfo() |
Binary address → hostname + service name |
| Obsolete hostname | gethostbyname() |
IPv4 only, not thread-safe — do not use in new code |
| Obsolete service | getservbyname() |
Not thread-safe — do not use in new code |
| IPv6 address struct | sockaddr_in6 |
Use instead of sockaddr_in for AF_INET6 sockets |
| IPv6 wildcard | in6addr_any |
Accepts on all interfaces; equivalent to INADDR_ANY |
| Dual-stack buffer | sockaddr_storage |
128 bytes; fits any address type |
| Ephemeral port | (kernel assigned) | Auto-assigned to unbound sockets on first sendto() |
Chapter 59 Complete!
You have covered address conversion, name resolution APIs, and a full IPv6 UDP socket example.
← Part 2: Name Resolution Next Chapter → embeddedpathashala.com
