[BlueLeaf1336]> PROGRAM> WinSock勉強会>

WinSockの基本 -- その2

historyTOP

2002/11/29:作成
2002/11/30:更新
2002/12/01:更新
2004/02/09:整理

IPアドレスTOP

ネットワークにあるコンピュータは、通常IPアドレスを持っている。 たとえば、192.168.0.1とか、127.0.0.1とか。 両方とも、実は、インターネットでは使われていないはずのアドレスだが、 とりあえず、XXX.XXX.XXX.XXXという形であらわされる、コンピュータの住所を持つ。

じゃあ、コンピュータは、どんな形で扱っているのかどうかが気になる。 まさか、このままじゃあないだろうと思った人は正解。 実際は、TInAddrという型で扱う。

type
  PInAddr = ^TInAddr;
  {$EXTERNALSYM in_addr}
  in_addr = record
    case integer of
      0: (S_un_b: SunB);
      1: (S_un_w: SunW);
      2: (S_addr: u_long);
  end;
  TInAddr = in_addr;
SunBとかSunW って何だ。と思ったらちゃんとあった。
type
  {$EXTERNALSYM SunB}
  SunB = packed record
    s_b1, s_b2, s_b3, s_b4: u_char;
  end;

  {$EXTERNALSYM SunW}
  SunW = packed record
    s_w1, s_w2: u_short;
  end;

それに、u_longとかu_charとかu_shortはと思ったらこれもある。ついでに、u_intも持ってきた。

type
  {$EXTERNALSYM u_char}
  u_char = Char;
  {$EXTERNALSYM u_short}
  u_short = Word;
  {$EXTERNALSYM u_int}
  u_int = Integer;
  {$EXTERNALSYM u_long}
  u_long = Longint;

全部見たことのある型になった。

つまり、TInAddrは、共用体で、SunBとかSunWとかu_longとかなっているが、SunB = u_char * 4SunW = u_short * 2で、さらに言い換えると、SunB = Char * 4SunW = Word * 2u_long = Longintといえる。よって、4バイトを3種類の方法で表してるだけで、結局4バイトの整数ということだ。

実は上位下位が逆に表現されていることに注意する必要がある。 「インターネットではビッグエンディアン」というのに関係あるが、よくは知らない。 そのときに考えよう。今は関係ないらしいし。

inet_addrTOP

でも、普通見ているIPアドレスは、XXX.XXX.XXX.XXXなわけで、どうにかしてコンピュータで扱える4バイトの整数に直す必要がある。とはいえ、関数がある。

{$EXTERNALSYM inet_addr}
function inet_addr(cp: PChar): u_long; stdcall; {PInAddr;}  { TInAddr }
cp: PChar
XXX.XXX.XXX.XXXタイプのIPアドレス文字列
戻り値: u_long
IPアドレスを表す4バイト整数

inet_addrテストTOP

procedure TForm1.Button3Click(Sender: TObject);
begin
    ShowMessage(IntToStr(inet_addr('127.0.0.1')));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage(IntToStr(inet_addr('192.168.0.1')));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage(IntToStr(inet_addr('dum.168.0.1')));
    ShowMessage(SysErrorMessage(WSAGetLastError));
end;

IPアドレスinet_addr(IP)SysErrorMessage(WSAGetLastError)
127.0.0.116777343この操作を正しく終了しました。
192.168.0.116820416この操作を正しく終了しました。
dum.168.0.1-1この操作を正しく終了しました。

結果は上のようになった。しかも、ソケットオープンしてない状態で。でもあたりまえか。 単なる計算だ。逆につながってた状態で、誰に何を聞くのか考えるほうが難しい。

それから興味深いのは、最後の嘘IPアドレスに対して、-1が返り、エラーなしの点だ。 これは、次の定数宣言に関係しているということだ。

const
  {$EXTERNALSYM INADDR_ANY}
  INADDR_ANY       = $00000000;     //誰でもOK
  {$EXTERNALSYM INADDR_LOOPBACK}
  INADDR_LOOPBACK  = $7F000001;     //ループバック(自分自身)
  {$EXTERNALSYM INADDR_BROADCAST}
  INADDR_BROADCAST = -1;            //ブロードキャスト
  {$EXTERNALSYM INADDR_NONE}
  INADDR_NONE      = -1;            //ありえないアドレス
2つの-1があるが、間違いなくINADDR_NONEだ。 でも、-1 = 255.255.255.255をあらわすらしいので、確かにエラーではない。奥深い。

INADDR_LOOPBACK = $7F000001というのがあるが、これって、127.0.0.1を あらわしているはず。ちょっと、電卓で計算してみよう。 16進モードにして、貼り付けて、10進モードに変換したら、ぬ。2130706433になった。 テストでは、16777343だったのに。なんかあるんだろうけど不明なので保留する。

inet_ntoaTOP

当然、inet_addrの逆、つまり、整数表示のIPアドレスを、文字列表示に変換する関数もある。

{$EXTERNALSYM inet_ntoa}
function inet_ntoa(inaddr: TInAddr): PChar; stdcall;
TInAddrについては、さっき勉強したので速攻で、テストしてみよう。こんな感じで。 と思ったら、コンパイル通らん。
ShowMessage(inet_ntoa(inet_addr('127.0.0.1')));
よく見てみると、inet_addr()は、u_long を返す。inet_ntoa()は、TInAddrを求める。 確かに違う。ううむ。キャストしてみよう。
ShowMessage(inet_ntoa(TInAddr(inet_addr('127.0.0.1'))));
あ、うまくいった。でも、こんなキャストしていいのか。 TInAddr.S_addr: u_longだけども、TInAddr≠u_longじゃないのか。 構造体のメンバが、u_long 型1個(ホントは共用体)とはいえ。

ま。いっか。 ということで、

inet_ntoaテストTOP

procedure TForm1.Button4Click(Sender: TObject);
begin
    ShowMessage(inet_ntoa(TInAddr(inet_addr('127.0.0.1'))));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage(inet_ntoa(TInAddr(inet_addr('192.168.0.1'))));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage(inet_ntoa(TInAddr(inet_addr('dum.168.0.1'))));
    ShowMessage(SysErrorMessage(WSAGetLastError));
end;

inet_ntoa(TInAddr(inet_addr('127.0.0.1')))127.0.0.1
inet_ntoa(TInAddr(inet_addr('192.168.0.1')))192.168.0.1
inet_ntoa(TInAddr(inet_addr('dum.168.0.1')))255.255.255.255

ちゃんといけてる。ソケットオープンは相変わらず不要。 それに、嘘っぱちIPアドレスも確かに、255.255.255.255と解釈されている。 賢いのー。

PHostEntTOP

いうたら、ここまでは単なる計算だ。もうちょっとよい感じの関数を使おう。

さて、IPアドレスからそのコンピュータの情報を取得する関数がある。 それが、gethostbyaddrで、例によって、WinSock.pasでは次のように。

{$EXTERNALSYM gethostbyaddr}
function gethostbyaddr(addr: Pointer;
                       len, Struct: Integer): PHostEnt; stdcall;

それから、似たような感じだが、コンピュータ名(ホスト名)から、そのコンピュータの情報を取得する関数もある。 こっちは、gethostbynameという。

{$EXTERNALSYM gethostbyname}
function gethostbyname(name: PChar): PHostEnt; stdcall;

とりあえず、この2つの関数を使うことを目的にする。

で、気づくのが、両方とも、PHostEntという型のデータを戻り値としている。 このPHostEntは、こんなもの。

type
  PHostEnt = ^THostEnt;
  {$EXTERNALSYM hostent}
  hostent = record
    h_name: PChar;
    h_aliases: ^PChar;
    h_addrtype: Smallint;
    h_length: Smallint;
    case Byte of
      0: (h_addr_list: ^PChar);
      1: (h_addr: ^PChar)
  end;
  THostEnt = hostent;

説明のために、var HostEntry:PHostEntを宣言したことにする。

h_name: PChar;
ホスト正式名称。HostEntry^.h_nameで取り出す。
h_aliases: ^PChar;
別名リスト。HostEntry^.h_aliases。注1参照。
h_addrtype: Smallint;
ホストアドレスタイプ
h_length: Smallint;
ホストアドレス長
h_addr_list: ^PChar
IPアドレス。HostEntry^.h_addr_list。注2参照。
h_addr: ^PChar
これもIPアドレス。(for backward compat らしい。意味不明。)

わかるというかパクれるところを説明する。

※注1(h_aliases)

h_aliasesとなってるので、複数ありえる。 で、それを一発で書いてある。取り出し方は、教えてもらうことにする。

function HostAliases(HostEntry: PHostEnt): string;
type
    PPChar=^PChar;
var
    ppc: PPChar;
begin
    Result := '';
    ppc := PPChar(HostEntry^.h_aliases);
    while assigned(ppc^) do
    begin
        // ppc は、PPChar = ^PChar なので、ppc^ = PChar になる。
        Result := Result + ',' + ppc^;
        inc(ppc);
    end;
    //1文字目が、絶対に','になってるのでごみ掃除
    Result := Copy(Result, 2, MaxInt);
end;

※注2(h_addr_list)

凶悪。こんな風にして使う。
PInAddr(HostEntry^.h_addr_list^)^
こういう解釈になるんだろうか。
  1. HostEntry^.h_addr_list
  2. (HostEntry^.h_addr_list)^
  3. PInAddr((HostEntry^.h_addr_list)^)
  4. (PInAddr((HostEntry^.h_addr_list)^))^
ちょっと考えてみよう。深く考えずに。検算程度で。
  1. HostEntry^.h_addr_list = ^PChar
  2. (HostEntry^.h_addr_list)^ = (^PChar)^ = PChar
  3. PInAddr((HostEntry^.h_addr_list)^) = PInAddr(PChar) = PInAddr
  4. (PInAddr((HostEntry^.h_addr_list)^))^ = (PInAddr)^ = TInAddr
なんかなりそうな気がしてきた。というか、なるに決まってるが。

gethostbyaddrTOP

やっと準備が終わった。いよいよ使ってみよう。

{$EXTERNALSYM gethostbyaddr}
function gethostbyaddr(addr: Pointer;
                       len, Struct: Integer): PHostEnt; stdcall;
addr: Pointer;
TInAddr型の変数へのポインタ
len: Integer
4(IPアドレスのサイズ)
Struct: Integer
AF_INET(addrの種類)

を入れるそうだが、最初のaddrはあれとして、残りの2つは何だろう。

とりあえず、AF_INETは定数値なので、WinSock.pasに見つかった。

{$EXTERNALSYM AF_INET}
AF_INET         = 2;               { internetwork: UDP, TCP, etc. }
まあ、そういうもんだということにした。

gethostbyaddrテストTOP

2002/11/30

こんな感じで。

procedure TForm1.Button5Click(Sender: TObject);
var
    HostEntry: PHostEnt;
    ip:    u_long;
begin
    ip := inet_addr('127.0.0.1');
    HostEntry := gethostbyaddr(@ip, 4, AF_INET);
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_name:' + HostEntry^.h_name);
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_aliases:' + HostAliases(HostEntry));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addrtype:' + IntToStr(HostEntry^.h_addrtype));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_length:' + IntToStr(HostEntry^.h_length));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addr_list:' + inet_ntoa(PInAddr(HostEntry^.h_addr_list^)^));
    ShowMessage(SysErrorMessage(WSAGetLastError));
end;

結果はこんな感じ。

127.0.0.1
h_namesotec-m360このコンピュータの名前
h_aliases''(空文字列)別名なんてつけ方知らないので
h_addrtype2おそらくAF-INET
h_length4アドレスの長さ(関数に渡した)
h_addr_list127.0.0.1関数に渡した

大体予想通り。他のIPアドレスも試してみよう。

IPh_nameh_aliasesh_addrtypeh_lengthh_addr_list
0.0.0.0sotec-m360''24AAA.AAA.AAA.AAA
BBB.BBB.BBB.BBBsotec-m360''24AAA.AAA.AAA.AAA
127.0.0.0localnet''24127.0.0.0
192.168.0.0エラー1----
255.255.255.255エラー1----
CCC.CCC.CCC.CCCCCC.dion.ne.jp''24CCC.CCC.CCC.CCC

※AAA.AAA.AAA.AAA

今、このコンピュータは、ADSLで接続されている。 で、NTT西日本のフレッツ接続ツールを使っている。 なので、コマンドプロンプトで、ipconfigを入力すると、2種類のアダプタ情報が表示される。 1つは、実際のLANカードに対応したアダプタ。もう1つは、PPPoE接続用の実体のないアダプタ。 ともに、DHCPから、IPアドレスを取得する設定にしてある。 で、IPアドレスを、0.0.0.0として、gethostbyaddr()を呼び出した場合の、h_addr_list には、PPPoE接続用アダプタのIPアドレスが返ってきている。

※BBB.BBB.BBB.BBB

実際のLANカードにどっかの誰かがつけた、IPアドレス。

※エラー1

「要求した名前は有効で、データベースにありますが、解決された正しい関連データがありません。」 と表示され、エラー対処していないので、不正アドレス読み込みでエラーになる。

※CCC.CCC.CCC.CCC

dionのDNSサーバ(プライマリ/セカンダリ共テスト)のアドレス。 こんなところで。続く。

2002/12/01

ふと気づく。 h_addr_listと名乗っておるくせに、たった1つしか IPアドレスを返さないわけがない。 けども、参考文献(こけるWired)にはそれっぽいことが書いてないのでそんなもんかと 思っていたが。 たまたまダウンロードした、WinSock系のOCXの説明書の中の親切説明に、 h_aliases、h_addr_listの構造がともに、似たようなもんだと書いてあるのを発見。 上の方に書いた、HostAliasesを参考にして、HostAddressListを作成 してみよう。

function HostAddressList(HostEntry: PHostEnt): string;
type
    PPChar=^PChar;
var
    ppc: PPChar;
begin
    Result := '';
    ppc := PPChar(HostEntry^.h_addr_list);
    while assigned(ppc^) do
    begin
        // ppc は、PPChar = ^PChar なので、ppc^ = PChar になる。
        Result := Result + ',' + inet_ntoa(PInAddr(ppc^)^);
        inc(ppc);
    end;
    //1文字目が、絶対に','になってるのでごみ掃除
    Result := Copy(Result, 2, MaxInt);
end;

で、これを使って、昨日のButton5Clickを書き換えてテストしてみる。

procedure TForm1.Button5Click(Sender: TObject);
var
    HostEntry: PHostEnt;
    ip:    u_long;
begin
    ip := inet_addr('0.0.0.0');
    HostEntry := gethostbyaddr(@ip, 4, AF_INET);
    ShowMessage(SysErrorMessage(WSAGetLastError));

    ShowMessage('h_name:' + HostEntry^.h_name);
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_aliases:' + HostAliases(HostEntry));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addrtype:' + IntToStr(HostEntry^.h_addrtype));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_length:' + IntToStr(HostEntry^.h_length));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addr_list:' + HostAddressList(HostEntry));
    ShowMessage(SysErrorMessage(WSAGetLastError));
end;

結果はこんな感じ。

0.0.0.0
h_namesotec-m360このコンピュータの名前
h_aliases''(空文字列)別名なんてつけ方知らないので
h_addrtype2おそらくAF-INET
h_length4アドレスの長さ(関数に渡した)
h_addr_listPPPoE用,LANカード用ちゃんと2種類!!

よしよし。こんな感じでよさそう。エラーをあまり試せなかったけど、 思いつかんし、次にいこう。

gethostbynameTOP

今度は、IPアドレスではなく、ホスト名からコンピュータの情報を取得するための関数だ。

{$EXTERNALSYM gethostbyname}
function gethostbyname(name: PChar): PHostEnt; stdcall;
今度は引数がとっても簡単。
name: PChar
ホスト名
即刻テストしてみよう。

gethostbynameテストTOP

こんな感じで、beginの次行のPChar()をいろいろ変化させてみよう。

procedure TForm1.Button6Click(Sender: TObject);
var
    HostEntry: PHostEnt;
begin
    HostEntry := gethostbyname(PChar('localhost'));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_name:' + HostEntry^.h_name);
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_aliases:' + HostAliases(HostEntry));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addrtype:' + IntToStr(HostEntry^.h_addrtype));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_length:' + IntToStr(HostEntry^.h_length));
    ShowMessage(SysErrorMessage(WSAGetLastError));
    ShowMessage('h_addr_list:' + HostAddressList(HostEntry));
    ShowMessage(SysErrorMessage(WSAGetLastError));
end;

結果はこんな感じに。

ホスト名h_nameh_aliasesh_addrtypeh_lengthh_addr_list
localhostsotec-m360''24127.0.0.1
sotec-m360sotec-m360''24PPPoE用,LANカード用
www.microsoft.co.jpreroute.microsoft.comwww.microsoft.co.jp24207.46.248.234
www.google.co.jpwww.google.comwww.google.co.jp24216.239.33.101
www.samurai.com ※注1home.samurai.comwww.samurai.com24205.207.28.66
www.okusama.com ※注1www.okusama.com-24207.82.103.197
www.harapeko.com ※注2そのようなホストは不明です。

※注1

ありえないホスト名の例としてテストしたが、実在した。びっくり。

※注2

ありえないホスト名の例としてテスト。エラー処理をしていないので、nilが入っている(とどっかで見た)THostEntに アクセスしているため、このエラー表示が出た後でAccessViolationがおきるが、ちゃんとないもんはないと返ってくる。 注1のテストのときに、ひょっとしてなんでもかんでも適当に返ってくるんじゃないのかと疑ったが、そんなことはなかった。

こんなところで、このページを終わることにする。 ただし、タイトルのWinSockの基本関数が何かという定義づけもしていないし、 基本関数が2個だけということもありえないが、そこそこ長くなったので、別のページに切り替える ことにする。ただ、しばらくは、WinSockの基本関数をタイトルにすることになる。

ここまでで、勉強した大体の関数や構造体など。

エラーコードTOP

また、gethostbynamegethostbyaddrが返すエラーコードとして、 こんな感じで宣言されている。(それなりに訳してみた。辞書なしで。)

{ resolverを使っているときに、gethostbyname() と gethostbyaddr() から返される エラーコード。これらのエラーは、WSAGetLastError() を通して、retrievedされる ことに注意すること。なので、特殊な実装や language run-time systems からのエラーコードによるクラッシュを避けるためのルールに従わなければならない ことに注意すること。 この理由により、エラーコードは、WSABASEERR+1001 を基準にしている。 また、[WSA]NO_ADDRESSが、互換性の目的のためだけに定義されていることも注意する こと。}

{ Authoritativeなエラーコード: Host not found }

  {$EXTERNALSYM WSAHOST_NOT_FOUND}
  WSAHOST_NOT_FOUND       = (WSABASEERR+1001);
  {$EXTERNALSYM HOST_NOT_FOUND}
  HOST_NOT_FOUND          = WSAHOST_NOT_FOUND;

{ Authoritativeでないエラーコード: Host not found, or SERVERFAIL }

  {$EXTERNALSYM WSATRY_AGAIN}
  WSATRY_AGAIN            = (WSABASEERR+1002);
  {$EXTERNALSYM TRY_AGAIN}
  TRY_AGAIN               = WSATRY_AGAIN;

{ どうしようもないエラー, FORMERR, REFUSED, NOTIMP }

  {$EXTERNALSYM WSANO_RECOVERY}
  WSANO_RECOVERY          = (WSABASEERR+1003);
  {$EXTERNALSYM NO_RECOVERY}
  NO_RECOVERY             = WSANO_RECOVERY;

{ Valid name, no data record of requested type }

  {$EXTERNALSYM WSANO_DATA}
  WSANO_DATA              = (WSABASEERR+1004);
  {$EXTERNALSYM NO_DATA}
  NO_DATA                 = WSANO_DATA;

{ no address, look for MX record }

  {$EXTERNALSYM WSANO_ADDRESS}
  WSANO_ADDRESS           = WSANO_DATA;
  {$EXTERNALSYM NO_ADDRESS}
  NO_ADDRESS              = WSANO_ADDRESS;
それと、基準にしているというWSABASEERRについては、これ。
{ すべてのWindowsのソケットエラーコード定数は、"normal"から、WSABASEERR によって
  バイアスされている。}
  {$EXTERNALSYM WSABASEERR}
  WSABASEERR              = 10000;

EOFTOP