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
};
sockaddr
、sockaddr_in
、sockaddr_in6
を観察すると、実際には長さが同じであることがわかりますが、sockaddr
は IP アドレスとポート番号を組み合わせています。後の 2 つは前者の派生型です。
では、なぜ直接 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 や強制的な型変換、memcpy などの公式に認められた方法があります。
ただし、type punning を使用する際には、strict aliasing の問題が発生する可能性があるため、注意が必要です。
strict aliasing: C/C++ の最適化機能で、異なる型のオブジェクトにアクセスすることを絶対に許可しないものであり、最適化エラーを回避し、操作の正確性を保証します。
ここでは、これら 2 つの構造体の長さが同じであり、型変換時にバイトが失われず、余分なバイトもありません ので、適切に使用できる理由です。