banner
Rick Sanchez

Rick Sanchez

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

Socket プログラミングにおける sockaddr および sockaddr_in 構造体の解析

FwfiZF0acAAyLU8

1. イントロダクション#

ソケットプログラミングでは、ソケット情報を構築するためによく使用される sockaddr_in 構造体を使用します。

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 フィールドを使用せずに、その中に 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 構造体では、s_addr フィールドを表すために Union 型が使用されており、異なる部分で IPv4 アドレスを解釈するために 4 バイト、2 つの 16 ビット整数、または 1 つの 32 ビット整数を使用しています。

したがって、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 アドレスとポート番号を組み合わせています。後の 2 つは前者の派生型です。

では、なぜ直接 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 や強制的な型変換、memcpy などの公式に認められた方法があります。

ただし、type punning を使用する際には、strict aliasing の問題が発生する可能性があるため、注意が必要です。

strict aliasing: C/C++ の最適化機能で、異なる型のオブジェクトにアクセスすることを絶対に許可しないものであり、最適化エラーを回避し、操作の正確性を保証します。

ここでは、これら 2 つの構造体の長さが同じであり、型変換時にバイトが失われず、余分なバイトもありません ので、適切に使用できる理由です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。