Socket 程式設計中,位址結構是連接應用層與傳輸層的關鍵資料介面。Linux 採用「通用位址結構 + 具體協定結構」的多型設計,確保 bind()、connect()、accept()、getsockname()、getpeername() 等系統呼叫能統一處理 IPv4、IPv6、Unix Domain、Netlink 等多種協定族。本文深入剖析 struct sockaddr、struct sockaddr_in、struct in_addr 的記憶體佈局、轉型規則與網路位元組序轉換。
struct sockaddr
struct sockaddr {
sa_family_t sa_family; // 協定族 (Address Family) : AF_INET, AF_INET6, AF_UNIX, AF_PACKET...
char sa_data[14]; // 14 位元組協定相關位址資料 (不透明)
};
sa_family_t 通常為 unsigned short (16 位元)。sa_data 固定 14 位元組,總大小 16 位元組。僅作為統一函數參數類型,絕不直接存取。sockaddr_in, sockaddr_in6, sockaddr_un...) **大小均 ≥ 16 位元組**,且首欄位均為 sa_family,保證記憶體佈局相容 (C 語言結構體首欄位位址等於結構體位址)。struct sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; // 必須為 AF_INET
in_port_t sin_port; // 16 位元埠號 (網路位元組序)
struct in_addr sin_addr; // 32 位元 IPv4 位址
unsigned char sin_zero[8]; // 填充至 16 位元組,與 sockaddr 等長,**必須清零**
};
struct in_addr 定義:
struct in_addr {
in_addr_t s_addr; // 32 位元 IPv4 位址 (網路位元組序)
};
in_addr_t 為 uint32_t。總大小:2 + 2 + 4 + 8 = 16 位元組,與 sockaddr 完全對齊。
| 函數/巨集 | 用途 | 範例 |
|---|---|---|
htons()/htonl() | 主機序 → 網路序 (16/32 位) | sin_port = htons(8080) |
ntohs()/ntohl() | 網路序 → 主機序 | port = ntohs(sin_port) |
inet_pton(AF_INET, "192.168.1.1", &sin_addr) | 字串 → 二進制位址 (支援 IPv6) | 取代 inet_addr |
inet_ntop(AF_INET, &sin_addr, buf, sizeof(buf)) | 二進制位址 → 字串 (支援 IPv6) | 取代 inet_ntoa (非執行緒安全) |
memset(&addr, 0, sizeof(addr)) | 清零整個結構 (含 sin_zero) | 必做步驟 |
(struct sockaddr *)&addr_in | 向下轉型傳給系統呼叫 | bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) |
(struct sockaddr_in *)&addr | 向上轉型取得具體欄位 | printf("%d", ntohs(((struct sockaddr_in*)&addr)->sin_port)) |
struct sockaddr_in6 (對比)
struct sockaddr_in6 {
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port; // 埠號
uint32_t sin6_flowinfo; // 流標籤 + 流量類別
struct in6_addr sin6_addr; // 128 位 IPv6 位址
uint32_t sin6_scope_id; // 介面索引 (鏈路本地址必填)
};
struct in6_addr {
unsigned char s6_addr[16]; // 16 位元組 = 128 位
};
大小 28 位元組 (>16),sin6_scope_id 用於區分同一鏈路本地址 (fe80::/10) 的不同介面。
struct sockaddr_un
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX / AF_LOCAL
char sun_path[108]; // 路徑名 (Linux 抽象命名空間 @ 開頭不佔用檔案系統)
};
大小約 110 位元組,sun_path 長度限制為 sizeof(sun_path) - 1。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 監聽所有介面
servaddr.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
perror("bind failed");
listen(sockfd, SOMAXCONN);
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
printf("Client: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
htons/htonl:小端機器 (x86) 直接寫入主機序導致埠號/位址錯誤 (如 8080 變 0x1F90 → 36864)。**永遠使用轉換函數**。sin_zero / 整個結構:殘留記憶體導致 bind 不定行為。習慣 memset(&addr, 0, sizeof(addr))。bind(sockfd, &addr, sizeof(struct sockaddr)) 錯誤,必須傳實際結構大小 sizeof(servaddr) (16/28/110),核心依 sa_family 判斷實際長度。inet_addr/inet_ntoa:不支援 IPv6、inet_ntoa 回傳靜態緩衝區非執行緒安全。**全面改用 inet_pton/ntop**。sin6_scope_id:ping6 fe80::1%eth0 中 %eth0 即 scope_id,程式中需 if_nametoindex("eth0") 取得。sockaddr 族群是 Linux 網路程式設計的基石。理解「通用首部 + 具體載荷」的多型模式、網路位元組序轉換、結構大小對齊、轉型規則,是撰寫正確、可移植、支援 IPv4/IPv6 雙棧網路程式的前提。現代開發建議直接使用 getaddrinfo() 統一解析並填充 addrinfo 鏈表,避免手動填充結構體,進一步降低出錯機率。