banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

在 Socket 编程中分析 sockaddr 和 sockaddr_in 结构体

FwfiZF0acAAyLU8

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

觀察到,sockaddrsockaddr_insockaddr_in6 實際上長度是相同的,只是 sockaddr 將 ip 地址和端口號合併在一起,後兩者是前者的派生類型。

那為什麼我們不直接給其傳入 IP:Port 的方式呢?
因為 API 並沒有提供相關函數去解析 IPPort,並且原始的 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++ 的一種優化特性,指絕對不允許對一個和另一個類型不同的對象進行訪問,它能夠有效避免產生優化錯誤,保證操作的正確性。

這裡可以合理使用的原因:這兩個結構體的長度相同,強制轉換類型時不會丟失字節,也沒有多餘的字節。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。