WinSock での IP アドレスとポートの扱い方

WinSock を学ぶときに障害となりやすいのはデータ構造のバリエーションの多さにあると思います。 WSADATA からはじまり、HOSTENT、sockaddr とさまざまな型の構造体が定義されています。これらはさまざまなプロトコルに対応できることのトレードオフとして強いられる複雑さなのですが、 TCP/IP(v4) に限って言えば非常に簡単にまとめることができます。

まず、アドレスは sockaddr_in がすべてです。

(sockaddr_in) = (IP アドレス) + (ポート)

左図のように sockaddr_in 構造体ひとつでポートと IP アドレスを表現します。もうひとつのフィールド sin_family には、 TCP/IP をあらわす AF_INET 値が常に設定されます。常に同じ値ですから、このフィールドは問題となることはありません。

struct sockaddr_in {
    short    sin_family;
    u_short  sin_port;
    struct   in_addr sin_addr;
    char     sin_zero[8];
};

ここで struct 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;
#define s_addr  S_un.S_addr            /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2     /* host on imp */
#define s_net   S_un.S_un_b.s_b1     /* network */
#define s_imp   S_un.S_un_w.s_w2   /* imp */
#define s_impno S_un.S_un_b.s_b4   /* imp # */
#define s_lh    S_un.S_un_b.s_b3      /* logical host */
};

union があるのでちょっと見にくいけど、要は ULONG 程度の大きさのデータ構造です。

ポート番号

注意点としては、ポートと IP アドレス値どちらもネットワークバイトオーダーで設定する必要がある、という点です。ではネットワークバイトオーダーとは何か?についてご説明します。
始めに、short 型の値をホストバイトオーダーからネットワークバイトオーダーに変換するプログラムの実際の動作を見てください。これを行うためには WinSock で提供される htons() を使います (関数名は Host To Network Short の意です)。

#include "stdafx.h"
#pragma comment(lib, "Ws2_32.lib")

short g_sPort = 80; //g_sPort をネットワークバイトオーダーに変換します

int main()
{
    g_sPort = htons(g_sPort);

    return 0;
}

これをデバッガで確認します。htons() で変換作業が行われるので ws2_32!htons にブレークポイントをおいて引っかかるのを待ちます。

0:000> bp ws2_32!htons
0:000> g
Breakpoint 0 hit  ←htons が呼び出されるところでストップ
eax=cccc0050 ebx=7ffdf000 ecx=00000000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=719e1746 esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000202
WS2_32!ntohs:
719e1746 0fb7442404       movzx  eax,word ptr [esp+0x4] ss:0023:0012ff30=0050
0:000> kv
*** WARNING: Unable to verify checksum for ByteOrderTest.exe
ChildEBP RetAddr  Args to Child
0012ff28 0040d3f7 cccc0050 77f82402 77f754f8 WS2_32!ntohs (FPO: [1,0,0])
0012ff80 00401199 00000001 00390e20 00390eb8 ByteOrderTest!main+0x27
0012ffc0 77e3eb69 77f82402 77f754f8 7ffdf000 ByteOrderTest!mainCRTStartup+0xe9
0012fff0 00000000 004010b0 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])
0:000> dc 00424b04 l1  ←00424b04 は g_sPort のアドレス.
00424b04  00000050                             P...
0:000> g 0040d3f7  ←ntohs を実行して戻るところでブレーク
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=0040d3f7 esp=0012ff34 ebp=0012ff80 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000206
ByteOrderTest!main+27:
0040d3f7 3bf4             cmp     esi,esp
0:000> dc 00424b04 l1
00424b04  00000050                             P...
0:000> u eip
ByteOrderTest!main+27:
0040d3f7 3bf4             cmp     esi,esp
0040d3f9 e8723cffff       call    ByteOrderTest!_chkesp (00401070)
0040d3fe 66a3044b4200     mov     [ByteOrderTest!g_sPort (00424b04)],ax
0040d404 33c0             xor     eax,eax
0040d406 5f               pop     edi
0040d407 5e               pop     esi
0040d408 5b               pop     ebx
0040d409 83c440           add     esp,0x40
0:000> t
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=0040d3f9 esp=0012ff34 ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038 gs=0000             efl=00000246
ByteOrderTest!main+29:
0040d3f9 e8723cffff       call    ByteOrderTest!_chkesp (00401070)
0:000>
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=00401070 esp=0012ff30 ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ByteOrderTest!_chkesp:
00401070 7501             jnz     ByteOrderTest!_chkesp+0x3 (00401073) [br=0]
0:000>
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=00401072 esp=0012ff30 ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ByteOrderTest!_chkesp+2:
00401072 c3               ret
0:000>
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=0040d3fe esp=0012ff34 ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ByteOrderTest!main+2e:
0040d3fe 66a3044b4200 mov [ByteOrderTest!g_sPort (00424b04)],ax ds:0023:00424b04=0050←ここで代入
0:000>
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390eb8 esi=0012ff34 edi=0012ff80
eip=0040d404 esp=0012ff34 ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ByteOrderTest!main+34:
0040d404 33c0             xor     eax,eax
0:000> dc 00424b04 l1
00424b04  00005000                             .P..
0:000>

このようにホストバイトオーダーで 0x00000050 だったものがネットワークバイトオーダーへ変換することで、0x00005000 に変わりました。少し正確に言うと short はここでは 2 byte データですから、0x0050 が 0x5000 に換わったことになります。

ここで、このテストは PentiumIII 上で行われましたが Pentium などの IA プロセッサは 「リトル・エンディアン」マシンです。したがって、ホストバイトオーダーはリトル・エンディアンです。一方、ネットワークバイトオーダーはビッグ・エンディアンです。

リトルなバイトがエンドになるのでアドレスの小さい番地から大きな番地へと 0x00 0x50 の順で並ぶことになります。そうです、この並びがデバッガの並びと同一であるために 0x0050 が、デバッガで見えたわけです。補足するとデバッガではダブルワードごとに表示されるために 0 がパディングされ 0x00000050 というわけです。
ではこれを「ビッグ・エンディアン」バイトオーダーにするとどうなるか、というとビッグなバイトがエンドになるので、0x50 0x00 の順で並び、0x5000 となります。これはデバッガで確認した結果と同一です。

WinSock にはこのようなバイトオーダー変換関数が多数用意されています。WinSock に渡されるデータはネットワークバイトオーダー (=ビッグ・エンディアン. つまり IA アーキテクチャでのリトル・エンディアンではない) であることに注意してください。これは通信先のマシンのアーキテクチャを隠蔽する役割を担います。マシンのアーキテクチャに依存しないネットワークオーダーを定義することによって、WinSock プログラマがマシンアーキテクチャを意識する必要がなくなるのです。


Coffee Break
では、ここでちょっとだけ脱線して htons() を自分で実装してることにしましょう。そのために htons の動作をちょっとのぞいて見ましょう。

0:000> g
Breakpoint 0 hit
eax=cccc0050 ebx=7ffdf000 ecx=00000000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e1746 esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000202
WS2_32!htons:
719e1746 0fb7442404       movzx  eax,word ptr [esp+0x4] ss:0023:0012ff30=0050

[esp+0x4] は EBP フレームでは一つ目の引数をあらわすので、ここでは引数の short 型値 80 をさしています. eax に 80 をセット.

0:000> t
eax=00000050 ebx=7ffdf000 ecx=00000000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e174b esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000202
WS2_32!htons+5:
719e174b 33c9             xor     ecx,ecx
0:000>
eax=00000050 ebx=7ffdf000 ecx=00000000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e174d esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
WS2_32!htons+7:
719e174d 8ae8             mov     ch,al
al, ch はそれぞれ ax, cx の low (下位) バイト及び high (上位) バイトを表す
0:000>
eax=00000050 ebx=7ffdf000 ecx=00005000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e174f esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
WS2_32!htons+9:
719e174f c1e808           shr     eax,0x8
shr eax を 8 ビット右へシフト
0:000>
eax=00000000 ebx=7ffdf000 ecx=00005000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e1752 esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
WS2_32!htons+c:
719e1752 0bc8             or      ecx,eax
0:000>
eax=00000000 ebx=7ffdf000 ecx=00005000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e1754 esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000206
WS2_32!htons+e:
719e1754 668bc1           mov     ax,cx
これでアキュムレータ (eax) にデータがセットされた
0:000>
eax=00005000 ebx=7ffdf000 ecx=00005000 edx=00390ed8 esi=0012ff34 edi=0012ff80
eip=719e1757 esp=0012ff2c ebp=0012ff80 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000206
WS2_32!htons+11:
719e1757 c20400           ret     0x4

ということで、要は ah と al をスワップするだけですから、xchg (EXCHANGE) 命令を使って次のようにすれば OK でしょう。
short sPort = 80;

_asm {
    mov ax, sPort
    xchg al, ah
    mov sPort, ax
}

尚、このような「リトル・エンディアン」から「ビッグ・エンディアン」の変換のためには、ダブルワードの場合は同様に bswap 命令を用いることができます。

IP アドレス

ポート番号の話題と同様ネットワークバイトオーダーに関する話ですが、IP アドレスについても見ておきましょう。違いはバイト数です。ポートはワードデータでしたが、IP アドレスはダブルワードです。この違いを抜かせばあとは問題ありません。
192.168.0.1 という IP アドレスを例にすると 192 168 0 1 は 16 進数で C0 A8 00 01 ですから、通常ですと Intel アーキテクチャでは、リトルエンディアンであるために 0100A8C0 として格納されます。これはネットワークバイトオーダーで表現すると C0A80001 ですが、つまりこの形式でデータを格納できれば良いことになります。
通常 IP アドレスは文字列として "192.168.0.1" のような形式を扱うのが普通です。WinSock では、この文字列を数値に変換し、なおかつネットワークバイトオーダーにしてくれる便利な関数が提供されています。inet_addr です。使い方は次のように簡単です。

ULONG ulAddr = inet_addr("192.168.0.1");


関連書籍

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説