1. 引言#
Socket プログラミングでは、私たちがよく使用する構造体 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
フィールドを直接使用して 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 アドレスとポート番号を組み合わせているのに対し、後者の 2 つは前者の派生型です。
では、なぜ IP:Port
の形式で直接渡さないのでしょうか?
API は IP
と Port
を解析するための関連関数を提供しておらず、元の sockaddr
は使い勝手が悪いため、後者の 2 つが存在します。
しかし、使用する際には、例えば:
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 つの構造体の長さは同じであり、強制的に型を変換してもバイトが失われず、余分なバイトもありません。