1. 引言#
在 Socket 編程中,我們時常使用的結構體 sockaddr_in
來構建 socket 信息。
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(ip);
serv_addr.sin_port = htons(port);
我們來看看 sockaddr_in
結構體的源碼:
struct sockaddr_in {
short sin_family; // 地址族(Address Family),AF_INET
u_short sin_port; // 16位TCP/UDP端口號,網絡字節序(Network Byte Order)
struct in_addr sin_addr; // 32位IP地址,網絡字節序(Network Byte Order)
char sin_zero[8]; // 暫時沒有使用,可以用來填充
};
我們注意到,第 4 行中,我們並沒有直接使用 s_addr
字段來表示一個 ip 地址,而是其中嵌套了一個結構體 sin_addr
。
那麼這樣有什麼好處呢?
2. 分析#
在 Unix 平台上,in_addr
結構體被定義為:
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; // 32位的IPV4地址,網絡字節序
};
在 Windows 平台上,in_addr
結構體被定義為:
struct in_addr {
union {
struct {
u_char s_b1, s_b2, s_b3, s_b4;
} S_un_b;
struct {
u_short s_w1, s_w2;
} S_un_w;
u_long S_addr;
} S_un;
};
可以看到,在不同的平台上,對於 s_addr
字段的處理方式是不一樣的,所以,這樣設計保證了平台兼容性。
這裡就解釋了為什麼我們在 sockaddr_in
結構體中看到的 s_addr
字段使用 in_addr
結構體包裹而不是直接使用這個字段了。
3. in_addr 中 Union 的分析#
在 Windows 平台上,in_addr
結構體中使用了一個 Union 類型來表示 s_addr
字段,分別表示以 4 個字節、2 個 16 位整數或 1 個 32 位整數來解釋 IPV4 地址的不同部分。
那麼當我們初始化 in_addr
字段後:
serv_addr.sin_addr.s_addr = inet_addr(ip);
我們可以使用上述的 3 種 Union 類型來對 IPV4
地址進行解釋。
4. sockaddr 結構體#
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
char sa_data[14]; //IP地址和端口號
};
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
uint16_t sin_port; //16位的端口號
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址類型,取值為AF_INET6
in_port_t sin6_port; //(2)16位端口號
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具體的IPv6地址
uint32_t sin6_scope_id; //(4)接口範圍ID
};
觀察到,sockaddr
、 sockaddr_in
和 sockaddr_in6
實際上長度是相同的,只是 sockaddr
將 ip 地址和端口號合併在一起,後兩者是前者的派生類型。
那為什麼我們不直接給其傳入 IP:Port
的方式呢?
因為 API 並沒有提供相關函數去解析 IP
和 Port
,並且原始的 sockaddr
使用起來有諸多不便,這才有了後兩者。
但在使用的時候,比如:
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(server_addr)))
我們通過 type punning 的方式(即強制類型轉換),來調用上述函數,這樣不管是 sockaddr_in
還是 sockaddr_in6
都可以兼容使用了。
type punning: 指在 C/C++ 中使用不同類型訪問同一段存儲空間的技巧,從而可以變相的改變存儲空間的類型,即通過改變變量的類型獲取一定的位模式。
type punning 的方式有很多,比如通過 Union 和 強制類型轉換,以及 officially sanctioned 方式,如 memcpy 等。
但是在使用 type punning 的時候可能會導致 strict aliasing 的問題,所以需要慎重使用。
strict aliasing: 是指 C/C++ 的一種優化特性,指絕對不允許對一個和另一個類型不同的對象進行訪問,它能夠有效避免產生優化錯誤,保證操作的正確性。
這裡可以合理使用的原因:這兩個結構體的長度相同,強制轉換類型時不會丟失字節,也沒有多餘的字節。