Socket Addresses
IPv4 ยท IPv6 ยท Generic
TLPI ยง59.4
What Is a Socket Address?
A socket address is simply the combination of an IP address and a port number. Together they uniquely identify one endpoint of a network connection. When you call bind(), connect(), sendto(), or recvfrom(), you pass a socket address structure so the kernel knows where to send data or who to listen for.
The Linux networking API defines separate structures for IPv4 addresses, IPv6 addresses, and a generic “any size fits” structure. Understanding the differences is essential for writing portable network code.
| Property | IPv4 | IPv6 |
|---|---|---|
| Address size | 32 bits (4 bytes) | 128 bits (16 bytes) |
| Example address | 192.168.1.10 | 2001:db8::1 |
| C structure | struct sockaddr_in |
struct sockaddr_in6 |
| Address family | AF_INET |
AF_INET6 |
| Wildcard address | INADDR_ANY (0.0.0.0) |
in6addr_any (0::0) |
| Loopback address | INADDR_LOOPBACK (127.0.0.1) |
in6addr_loopback (::1) |
| Header | <netinet/in.h> |
<netinet/in.h> |
The sockaddr_in structure holds everything the kernel needs to identify an IPv4 endpoint:
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr; /* 32-bit IPv4 address, network byte order */
};
struct sockaddr_in {
sa_family_t sin_family; /* Always AF_INET */
in_port_t sin_port; /* 16-bit port, network byte order */
struct in_addr sin_addr; /* 32-bit IP address */
unsigned char __pad[8]; /* Padding to reach 16 bytes total */
};
| Field | Type | Size | Purpose | What to put here |
|---|---|---|---|---|
sin_family |
sa_family_t |
2 bytes | Identifies socket domain | Always AF_INET |
sin_port |
in_port_t |
2 bytes | Port number | htons(port_number) |
sin_addr.s_addr |
in_addr_t |
4 bytes | IP address | inet_pton() result or INADDR_ANY |
__pad |
unsigned char[] |
8 bytes | Alignment padding | Leave as zero (use memset) |
How to fill in sockaddr_in for a server that accepts any connection:
#include <netinet/in.h>
#include <string.h>
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); /* zero everything first */
addr.sin_family = AF_INET; /* IPv4 */
addr.sin_port = htons(8080); /* port 8080 in network byte order */
addr.sin_addr.s_addr = INADDR_ANY; /* accept on all interfaces */
/* Now use addr with bind() */
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
perror("bind");
How to fill in sockaddr_in for a client connecting to a specific IP:
#include <arpa/inet.h> /* for inet_pton() */
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.10", &server.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
perror("connect");
CPUs store multi-byte integers in either little-endian (x86) or big-endian (SPARC, some ARM) order. If two machines with different endianness communicate, they will misread each other’s integers.
The internet protocol suite mandates big-endian (network byte order) for all multi-byte fields in protocol headers. The C library provides conversion functions:
| Function | Converts | Use for |
|---|---|---|
htons(x) |
host โ network, 16-bit | Port numbers (sin_port) |
htonl(x) |
host โ network, 32-bit | IPv4 addresses in numeric form |
ntohs(x) |
network โ host, 16-bit | Reading port from received packet |
ntohl(x) |
network โ host, 32-bit | Reading IPv4 address from received packet |
/* On a little-endian machine, port 80 in memory: */
uint16_t port_host = 80; /* bytes: 0x50 0x00 (little-endian) */
uint16_t port_net = htons(80); /* bytes: 0x00 0x50 (big-endian / network) */
/* Always use htons() before storing into sin_port */
addr.sin_port = htons(80);
IPv6 uses a 128-bit address, so its structure is larger and has extra fields:
#include <netinet/in.h>
struct in6_addr {
uint8_t s6_addr[16]; /* 16 bytes = 128 bits */
};
struct sockaddr_in6 {
sa_family_t sin6_family; /* Always AF_INET6 */
in_port_t sin6_port; /* 16-bit port, network byte order */
uint32_t sin6_flowinfo; /* IPv6 flow info (set to 0) */
struct in6_addr sin6_addr; /* 128-bit IPv6 address */
uint32_t sin6_scope_id; /* Interface scope (set to 0) */
};
| Field | Size | Purpose | Typical value |
|---|---|---|---|
sin6_family |
2 bytes | Domain identifier | AF_INET6 |
sin6_port |
2 bytes | Port number | htons(port) |
sin6_flowinfo |
4 bytes | QoS flow label (advanced) | 0 |
sin6_addr |
16 bytes | The IPv6 address | inet_pton() result or in6addr_any |
sin6_scope_id |
4 bytes | Link-local scope identifier | 0 |
Setting up an IPv6 server to listen on all interfaces:
#include <netinet/in.h>
#include <string.h>
struct sockaddr_in6 addr6;
memset(&addr6, 0, sizeof(addr6));
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(8080);
addr6.sin6_addr = in6addr_any; /* listen on all IPv6 interfaces */
/* Note: sin6_flowinfo and sin6_scope_id are zero from memset */
if (bind(sockfd, (struct sockaddr *)&addr6, sizeof(addr6)) == -1)
perror("bind");
Connecting to a specific IPv6 address:
#include <arpa/inet.h>
struct sockaddr_in6 server6;
memset(&server6, 0, sizeof(server6));
server6.sin6_family = AF_INET6;
server6.sin6_port = htons(8080);
inet_pton(AF_INET6, "2001:db8::1", &server6.sin6_addr);
if (connect(sockfd, (struct sockaddr *)&server6, sizeof(server6)) == -1)
perror("connect");
For IPv4 the wildcard address is a simple integer constant: INADDR_ANY = 0. You can write it directly.
For IPv6 the address is a 16-byte array, not a scalar. You cannot write an array constant on the right-hand side of an assignment in C. Therefore the system provides:
- A macro for initializers:
IN6ADDR_ANY_INITโ only usable at declaration time. - A pre-initialized variable:
in6addr_anyโ usable anywhere.
/* Legal โ use macro at declaration */
const struct in6_addr my_any = IN6ADDR_ANY_INIT;
/* Legal โ use pre-initialized variable in assignment */
addr6.sin6_addr = in6addr_any;
/* ILLEGAL โ can't use macro in assignment */
/* addr6.sin6_addr = IN6ADDR_ANY_INIT; ERROR */
The same pattern applies for the loopback address:
/* ::1 (loopback) */
addr6.sin6_addr = in6addr_loopback;
On a dual-stack host (which runs both IPv4 and IPv6), both protocol families share a single port number space per transport protocol (TCP or UDP).
| What you bind | Result |
|---|---|
| IPv6 TCP socket to port 2000 with wildcard address | Port 2000 is now owned โ no other socket (IPv4 or IPv6) can bind TCP port 2000 |
| IPv4 TCP socket trying to bind port 2000 afterwards | Fails with EADDRINUSE |
This is why you should always use SO_REUSEADDR carefully and understand which address family your server socket is using.
A common problem: you want to write a function that accepts connections from both IPv4 and IPv6 clients. You need to store the client’s address after accept(), but you don’t know at compile time whether it will be a sockaddr_in (16 bytes) or a sockaddr_in6 (28 bytes).
Solution: struct sockaddr_storage. It is large enough and suitably aligned to hold any socket address type.
/* From <sys/socket.h> on Linux */
struct sockaddr_storage {
sa_family_t ss_family; /* address family */
/* ... padding to reach 128 bytes, suitably aligned ... */
};
Usage pattern with accept():
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
void print_peer_address(int connfd)
{
struct sockaddr_storage peer;
socklen_t peer_len = sizeof(peer);
char addr_str[INET6_ADDRSTRLEN];
if (getpeername(connfd, (struct sockaddr *)&peer, &peer_len) == -1) {
perror("getpeername");
return;
}
if (peer.ss_family == AF_INET) {
/* IPv4 peer */
struct sockaddr_in *p4 = (struct sockaddr_in *)&peer;
inet_ntop(AF_INET, &p4->sin_addr, addr_str, sizeof(addr_str));
printf("IPv4 peer: %s port %d\n", addr_str, ntohs(p4->sin_port));
} else if (peer.ss_family == AF_INET6) {
/* IPv6 peer */
struct sockaddr_in6 *p6 = (struct sockaddr_in6 *)&peer;
inet_ntop(AF_INET6, &p6->sin6_addr, addr_str, sizeof(addr_str));
printf("IPv6 peer: %s port %d\n", addr_str, ntohs(p6->sin6_port));
}
}
The key idiom: allocate a sockaddr_storage, pass it cast to (struct sockaddr *), then check ss_family afterwards to decide which concrete type to cast to.
All three structures compared side by side (approximate offsets):
| Offset | sockaddr_in (IPv4) | sockaddr_in6 (IPv6) | sockaddr_storage |
|---|---|---|---|
| 0โ1 | sin_family (2B) | sin6_family (2B) | ss_family (2B) |
| 2โ3 | sin_port (2B) | sin6_port (2B) | padding… |
| 4โ7 | sin_addr (4B) | sin6_flowinfo (4B) | padding… |
| 8โ23 | __pad (8B) | sin6_addr (16B) | padding… |
| 24โ27 | โ | sin6_scope_id (4B) | padding… |
| 28โ127 | โ | โ | padding (to 128B) |
| Total | 16 bytes | 28 bytes | 128 bytes |
Q1. What fields must you fill in a sockaddr_in before calling bind()?
You must set sin_family = AF_INET, sin_port (using htons()), and sin_addr.s_addr. Zero out the structure first with memset() to clear the padding field.
Q2. Why do we cast sockaddr_in to (struct sockaddr *) when calling bind()?
The kernel socket API predates C generics. All socket functions accept struct sockaddr * as their address parameter. The caller is responsible for passing the correct concrete type and its size, and the kernel uses the sa_family field to know how to interpret it.
Q3. What is the difference between INADDR_ANY and in6addr_any?
INADDR_ANY is a 32-bit integer constant (0) usable directly in an assignment. in6addr_any is a pre-initialized struct in6_addr variable because you cannot assign a 16-byte array literal in C.
Q4. Why is sockaddr_storage 128 bytes when sockaddr_in6 is only 28 bytes?
The extra space future-proofs the API. Unix domain socket addresses (sockaddr_un) can be up to 110 bytes. The 128-byte size guarantees that sockaddr_storage can hold any current or future socket address type on Linux.
Q5. If an IPv6 socket binds port 80 with the wildcard address, can an IPv4 socket also bind port 80?
No. IPv4 and IPv6 share a port namespace per transport protocol on the same host. The second bind will fail with EADDRINUSE.
Q6. Why must sin6_flowinfo and sin6_scope_id be set to 0 in most applications?
sin6_flowinfo is used for QoS flow labeling (rarely needed in application code). sin6_scope_id identifies link-local interfaces (relevant only for link-local addresses like fe80::). For global unicast addresses, both must be zero.
Q7. How do you check at runtime whether a received socket address is IPv4 or IPv6?
Store it in a sockaddr_storage and check the ss_family field. If it equals AF_INET, cast to sockaddr_in; if it equals AF_INET6, cast to sockaddr_in6.
Q8. What is the purpose of in_addr_t and in_port_t?
They are typedef aliases defined in <netinet/in.h>: in_addr_t is a 32-bit unsigned integer (for IPv4 addresses) and in_port_t is a 16-bit unsigned integer (for port numbers). Using these typedefs instead of raw uint32_t / uint16_t makes the code’s intent clearer.
Next: Part 3 โ Overview of Host and Service Conversion Functions
