ソケット通信


ソケット通信プログラムの勉強を兼ねて, 指定したホストの情報を得るプログラムをDelphiで作ってみた. IPアドレスまたはホスト名を入力し,[look up]ボタンを押すと そのホストのIPアドレス,正式ホスト名およびエイリアス(別名,ニックネーム)を表示する. 例えば,以下のように "www.yahoo.com" と入力すれば, IPアドレスと正式ホスト名www.yahoo.akadns.netが返ってくる. www.yahoo.comというのはニックネームであることがわかる.

WindowsでのソケットAPIに ついてはhttp://www.sockets.com/に詳しい解説がある. Delphiでは,各種のソケットAPIがwinsock.pasで宣言されているので,このユニットをuses節に追加する.

winsockが提供するソケットAPIには同期型非同期型がある. 同期型APIは,Berkeley Software Distribution (BSD)が策定したソケット通信のパラダイム(paradigm,文法)を そのままWindowsに移植したものであり,通常の関数と同じように使える反面, 処理が終わるまで制御を戻さない. したがって,他のホストからの応答が得られるまでにある程度の時間を要する場合, 実行の流れが止まってしまい,その間ウィンドウの再描画処理などが行えない. これを回避するにはスレッドを使うしかない. また,情報を取得するようなAPIの場合,各種情報を格納した構造体が関数側が確保したメモリ領域に存在する. このようなメモリ領域の寿命は全く不明であるから,必要な情報はすぐに呼び出し側のメモリ領域にコピー しておかなければならない. このように,同期型APIにはWindowsの流儀と異なる部分がある.

非同期型APIは,このような同期型APIの使いづらさを克服するためにMicrosoftが 独自に作成したものである. 非同期型APIは呼び出されるとすぐに制御を戻す. そして,処理が終了すると,呼び出す際に指定したウィンドウにメッセージを送ってくる. したがって,呼び出し側はこのメッセージが来るまで他の処理を行える. Windowsのメッセージ処理プログラミングに慣れていると,このようなスタイルの方が使いやすい. また,情報取得APIも,呼び出し側でメモリ領域を確保し,そのアドレスを渡して そこに情報を書き込んでもらうというWindowsではごく普通のスタイルになっている. ここでのプログラムも非同期型APIを用いる.

ソケットの初期化
IPアドレスからホスト名を得る
ホスト名からIPアドレスを得る
メッセージへの応答
プログラムのソースコード


ソケットの初期化

まず最初に,通信ができるようにソケットをオープンする必要がある. ソケットのオープンにはWSAStartupを用いる. この関数のプロトタイプは以下の通り.

function WSAStartup( wVersionRequired: word; var WSData: TWSAData): Integer;

wVersionRequiredはプログラムが想定するwinsockのバージョンである. Word型の上位バイトにマイナーバージョンを,下位バイトにメジャーバージョンをそれぞれ指定する. 通常はver.1.1で問題ないと思うので,MAKEWORD(1,1)のように指定すればよいだろう. WSDataはWSAStartupが設定する各種の情報が格納されている. wVersionメンバには対応できるwinsockのバージョンが格納されているので, wVersionRequiredで指定した値と異なっていた場合はエラーメッセージを表示する.

使い終わったソケットはWSACleanupを呼び出して閉じなければならない. WSAStartupとWSACleanupはそれぞれフォームのOnCreateおよびOnCloseに書けばよいだろう.

procedure TMainForm.FormCreate(Sender: TObject);
var
  WSAData: TWSaData;
begin
   WSAStartup( MakeWord( 1, 1 ), WSAData );
   VersionLabel.Caption
      := Format( 'Winsock version: %d.%d',
         [ LOBYTE( WSAData.wVersion ), HIBYTE( WSAData.wVersion ) ] );
end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   WSACleanup;
end;


IPアドレスからホスト名を得る

IPアドレスからホスト名を得るにはWSAAsyncGetHostByAddrを用いる. 非同期APIはすべて関数名の最初にWSAAsyncが付いている. このAPIのプロトタイプは,

function WSAAsyncGetHostByAddr( HWindow: HWND; wMsg: u_int; addr: PChar;
  len, Struct: Integer; buf: PChar; buflen: Integer ): THandle;

である. HWindowはメッセージを受け取るウィンドウのハンドルであり,wMsgは メッセージの種類である. メッセージには独自に定義したものを指定すればよいだろう. addrはIPアドレスである. PCharとなっているが,IPアドレスを格納するためのTInAddr変数へのポインタを渡さなければならない. このようにwinsock.pasには文字配列へのポインタでないものがPCharとして宣言されていることが多い. 元々のC言語用ヘッダがchar*で宣言されているためらしい. TInAddr型は以下のように定義されている.

  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;

u_longはLongintである. TInAddr型へのポインタの代わりに4バイト整数へのポインタを渡してもよい.

lenはIPアドレスのサイズで4を指定する. Structはaddrの種類を指定するものであるが, TCP/IPソケットのアドレスを意味するAF_INET(あるいはPF_INET)でなければならない.

bufは結果を格納する領域へのポインタである(これもPCharで宣言されている). 結果は以下のTHostEnt型として格納される.

  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;

正式なホスト名はh_nameであり, エイリアスはh_aliasesである. ただしエイリアスは複数ある場合があるので,ヌル終端文字列の配列へのポインタとなっている. IPアドレスはh_addr_listであるが,PCharへのポインタとなっているのでややこしい. おそらく他のプロトコルではアドレスが4バイトとは限らないからだろう.

と,これがTHostEnt型なのだが,実はbufでアドレスを指定する領域の大きさは SizeOf(THostEnt)+αでなければならない. winsockが使う作業領域も含んでいなければならないからである. 必要な領域の大きさはMAXGETHOSTSTRUCTであり, THostEnt型の領域はこの一番上+0の位置にあるということになっている. つまり,MAXGETHOSTSTRUCTの大きさの領域を確保してそのアドレスを渡すが, 結果が格納されているのはその一部ということである.

このような場合はCでいうところの共用体を使うのが簡単である. 以下のようなTGetHostStruct型を定義しておき,この型の変数のアドレスをbufに指定すればよい.

  PGetHostStruct = ^TGetHostStruct;
  TGetHostStruct = record
      case Integer of
       0: ( Buf: array[ 0..MAXGETHOSTSTRUCT - 1 ] of Byte );
       1: ( HostEnt: THostEnt );
  end;

buflenはSizeOf(TGetHostStruct)すなわちMAXGETHOSTSTRUCTである.

なお,IPアドレスは "aaa.bbb.ccc.ddd" のように表現するのが普通であるから, このように表現された文字列を4バイト整数に変換するための関数inet_addrが用意されている.

function inet_addr( cp: PChar ): u_long;

cpにPCharを渡せば4バイト整数値が返ってくる. 変換に失敗した場合はINADDR_NONE(=$FFFFFFFF)が返ってくる.


ホスト名からIPアドレスを得る

ホスト名からIPアドレスを得るにはWSAAsyncGetHostByNameを使う. この関数のプロトタイプは

function WSAAsyncGetHostByName( HWindow: HWND; wMsg: u_int;
  name, buf: PChar; buflen: Integer ): THandle;

nameにホスト名を指定する以外はWSAAsyncGetHostByAddrと同じである.

ホスト指定の際にはIPアドレスとホスト名のどちらでも指定できるようにしなければならない. このような場合, ユーザはどちらを入力するかわからないので,入力された文字列をinet_addrでIPアドレスに 変換し,成功すればその戻り値を使ってWSAAsyncGetHostByAddrを呼び出す. 変換に失敗した場合はホスト名が入力されたと判断し,WSAAsyncGetHostByNameを呼び出す.


メッセージへの応答

WSAAsyncGetHostByAddrやWSAAsyncGetHostByNameに渡すために以下のメッセージを定義する.

const
  WSM_HOSTFOUND = WM_USER + 1000;

このメッセージのハンドラは以下のようにする.

procedure TMainForm.WSMHostFound( var Mes: TMessage );
begin
   case WSAGETASYNCERROR( Mes.LParam ) of
    0: 
       // 処理に成功した場合
       
    WSAHOST_NOT_FOUND:
       // 処理に失敗した場合
   end;
end;

メッセージのLParamの上位ワードにエラーコードが格納されている. これを取り出すマクロ(Delphiでは関数)がWSAGETASYNCERRORである. 処理が成功した場合は0が格納されており,指定されたホストが見つからなかった場合はWSAHOST_NOT_FOUNDが 格納されている. エラーコードの詳細はここにまとめられている.


プログラムのソースコード

ここで作成したプログラムのソースコードを以下に示す. アーカイブはここに置いておく.

unit main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Winsock, StdCtrls, ComCtrls;

const
  WSM_HOSTFOUND = WM_USER + 1000;  // メッセージを定義

type
  TMainForm = class(TForm)
    VersionLabel: TLabel;
    Label4: TLabel;
    InputEdit: TEdit;
    LookUpBtn: TButton;
    GroupBox1: TGroupBox;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    IPEdit: TEdit;
    NameEdit: TEdit;
    AliaseMemo: TMemo;
    StatusBar: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure LookUpBtnClick(Sender: TObject);
  private
    // メッセージハンドラ
    procedure WSMHostFound( var Mes: TMessage ); message WSM_HOSTFOUND;
  public
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

// ホスト情報を受け取るためのTGetHostStruct型を定義する.
// 大きさはMAXGETHOSTSTRUCTだけ必要である.
type
  PGetHostStruct = ^TGetHostStruct;
  TGetHostStruct = record
      case Integer of
       0: ( Buf: array[ 0..MAXGETHOSTSTRUCT - 1 ] of Byte );
       1: ( HostEnt: THostEnt );
  end;

// TGetHostStruct型のグローバル変数を宣言
var
  HostStruct: TGetHostStruct;

procedure TMainForm.FormCreate(Sender: TObject);
var
  WSAData: TWSaData;
begin
   // winsockを初期化
   WSAStartup( MakeWord( 1, 1 ), WSAData );
   VersionLabel.Caption
      := Format( 'Winsock version: %d.%d',
         [ LOBYTE( WSAData.wVersion ), HIBYTE( WSAData.wVersion ) ] );
end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   // winsockを終了
   WSACleanup;
end;

procedure TMainForm.LookUpBtnClick(Sender: TObject);
var
  HostName: String;
  IPAddress: TInAddr;
begin
   IPEdit.Text := '';
   NameEdit.Text := '';
   AliaseMemo.Lines.Clear;
   HostName := InputEdit.Text;
   // ホスト名をIPアドレスに変換
   IPAddress.S_addr := inet_addr( PChar( HostName ) );
   // 変換に成功したらIPアドレスが入力されている.WSAAsyncGetHostByAddrを呼び出す.
   if IPAddress.S_addr <> u_long( INADDR_NONE ) then
      WSAAsyncGetHostByAddr( Handle, WSM_HOSTFOUND, PChar( @IPAddress ),
         Sizeof( IPAddress ), AF_INET, PChar( @HostStruct ), SizeOf( HostStruct ) )
   // 変換に失敗したらホスト名が入力されている.WSAAsyncGetHostByNameを呼び出す.
   else
      WSAAsyncGetHostByName( Handle, WSM_HOSTFOUND,
         PChar( HostName ), PChar( @HostStruct ), SizeOf( HostStruct ) );
end;

// エイリアスリストを読むためにPCharへのポインタ型を定義
type
  PPChar = ^PChar;

procedure TMainForm.WSMHostFound( var Mes: TMessage );
var
  IPAddr: TInAddr;
  HostName, Aliase: String;
  P: PPChar;
begin
   case WSAGETASYNCERROR( Mes.LParam ) of
   // 処理が成功した場合
    0: begin
         // ホストの正式名を取得
         NameEdit.Text := HostStruct.HostEnt.h_name;
         // IPアドレスを取得し,aaa.bbb.ccc.dddの形式の文字列に変換する.
         IPAddr := PInAddr( HostStruct.HostEnt.h_addr_list^ )^;
         IPEdit.Text := inet_ntoa( IPAddr );
         // h_aliases(エイリアスを表すPCharの配列)をPPCharとして取得
         P := PPChar( HostStruct.HostEnt.h_aliases );
         // P^が有意である,すなわち,Pの参照先がPCharである間繰り返す.配列の最後にはnilが入っている.
         while Assigned( P^ ) do
         begin
            // P^をPCharとして取り出す
            Aliase := PChar( P^ );
            AliaseMemo.Lines.Add( Aliase );
            // Pを1バイト増やす.すなわち次の配列要素に移る.
            Inc( P );
         end;
         StatusBar.SimpleText := 'Winsock is successfully.';
       end;
    WSAHOST_NOT_FOUND:
       StatusBar.SimpleText := 'No such host is known.';
   end;
end;

end.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送