萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 作業系統 >  >> Linux 教學

Linux Socket 程式設計核心:通用位址結構 sockaddr 與 sockaddr_in 深度解析

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 語言結構體首欄位位址等於結構體位址)。

二、 IPv4 專用結構 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))

四、 IPv6 結構 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) 的不同介面。

五、 Unix Domain Socket 結構 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。

六、 實戰範例:TCP Server 綁定 IPv4 位址


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

七、 常見陷阱與最佳實踐

  1. 忘記 htons/htonl:小端機器 (x86) 直接寫入主機序導致埠號/位址錯誤 (如 8080 變 0x1F90 → 36864)。**永遠使用轉換函數**。
  2. 未清零 sin_zero / 整個結構:殘留記憶體導致 bind 不定行為。習慣 memset(&addr, 0, sizeof(addr))。
  3. 傳遞錯誤的結構大小:bind(sockfd, &addr, sizeof(struct sockaddr)) 錯誤,必須傳實際結構大小 sizeof(servaddr) (16/28/110),核心依 sa_family 判斷實際長度。
  4. 使用過時 inet_addr/inet_ntoa:不支援 IPv6、inet_ntoa 回傳靜態緩衝區非執行緒安全。**全面改用 inet_pton/ntop**。
  5. IPv6 鏈路本地址未設 sin6_scope_id:ping6 fe80::1%eth0 中 %eth0 即 scope_id,程式中需 if_nametoindex("eth0") 取得。

八、 總結

sockaddr 族群是 Linux 網路程式設計的基石。理解「通用首部 + 具體載荷」的多型模式、網路位元組序轉換、結構大小對齊、轉型規則,是撰寫正確、可移植、支援 IPv4/IPv6 雙棧網路程式的前提。現代開發建議直接使用 getaddrinfo() 統一解析並填充 addrinfo 鏈表,避免手動填充結構體,進一步降低出錯機率。

copyright © 萬盛學電腦網 all rights reserved