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");