こける Wired-Winsockを使ってみようぜ-4.お話ししましょ'97/12/01

4.お話ししましょ


今回でやっと、通信を行う部分に入ります。
前回まででWinsockが使える環境で動作しているかも判断つくようになりましたし、声をかけるべきサーバーのIPアドレスも分かるようになりました。
やっと通信を行う準備ができたわけです。

ここで作成するのは「チャットクライアント」です。サーバーがまだですが、これだけでも結構役に立ちます。
なにせ、知ってさえいればこれでホームページのソースも取れれば、メールが来ているかぐらいは確認できるのですから。
また、これは独自のプロトコルを作る場合でも、サーバー用の最初のテストプログラムとしても使えます。

まず、通信する為の電話、「ソケット」を作りましょう。
function socket(af, struct, protocol: Integer): TSocket;
afはアドレスファミリ、TCPではAF_INETを指定します。WinsockではTCP/IPをさしてINET、つまりインターネットと呼称することが多いですね。
structはストリームタイプなのかユーザダイアグラムなのかを指定します。TCPはストリームタイプなのでSOCK_STREAMです。
protocolには本来TCPを指定すべきなのですが、INETアドレスファミリにストリームタイプといえばTCPしかありません。「自動で割り振れ」という意味である0を入れとけばTCPになります。

返ってくるのは「ソケット」です。これから通信するのは、このソケットを使いますので保存しておきましょう。
もしソケットの作成に失敗したらINVALID_SOCKETです。WSAGetLastErrorで原因を確かめましょう。

できてしまったソケットを消去する手段も紹介しておきます。
作ったソケットはちゃんと消去しなければいけません。
function closesocket(s: TSocket): Integer;
sは消去すべきソケットです。以後使えなくなります。
また、winsockのデフォルトでは、ソケットを消去すると未送信のパケットがあっても強引に閉じます。
ということは、データの送信を行ってすぐにソケットを消去すると、最後のパケットを送信することができません。
で、このデフォルトを変更することもできるのですが、独自プロトコルを使っているのならそれで逃げられます。
最後に受信する方からソケットを閉じて、最後に送信した方は相手が接続を断ったのを確認してからソケットを閉じたら良いですね。

これからが実際の通信らしいところですが、その前に「ビッグエンディアン」「リトルエンディアン」変換を紹介しておきましょう。
function htons(hostshort: u_short): u_short;
function htonl(hostlong: u_long): u_long;
function ntohs(netshort: u_short): u_short;
function ntohl(netlong: u_long): u_long;
AtoBCという形になっているでしょう?
Aが変換前でh:hostか、n:netです。
Bが変換後。
CはWORDかDWORDかを示していてsがshortつまりWORD(SmallInt)、lがlongでDWORD(LongInt,Integer)。
実は、htonsとntohs、htonlとntohlはそれぞれ同じ物だったりしますが、ホストのデータをネットワーク形式に変換するのか、ネットワーク形式をホストの形式に変換するのかで使い分けた方が、後々見やすいですよ。
ついでに、ネットワーク上の形式を「ビッグエンディアン」ともいいますが、これは「上位先送り」といっているだけです。
これとは別に「ネットワーク上の形式」は「ネットワークバイトオーダー」という言い方があります。
使い分けは判りますよね。
モトローラ系のCPUは「ビッグエンディアン」ですが「ネットワークバイトオーダー」とはいいません。(^^;
「ネットワークバイトオーダー」が、お約束として「ビッグエンディアン」なわけです。
ちなみにモトローラ系のCPU用のwinsockがあったとしたら、そのなかのhostnsとかのAPIは何もせず引数をそのまま返す関数のはずです。
winsockのもとになったUNIX socketにもhtonsとかありますが、そうなってます。

さて本題にもどって、クライアント側はサーバーに接続にいかなければなりません。
すでにinet_addrやgethostbynameでIPアドレスは取得しているはずですね。
function connect(s: TSocket; var name: TSockAddr; namelen: Integer): Integer;
これまたパターンですが、接続できたら0、失敗したらSOCKET_ERRORで、WSAGetLastErrorで原因調べです。
このconnectも、「接続するまで(接続に失敗するまで)返ってこない」ので凍結したように見えます。
「固まらない通信」の回までお待ち下さい。

sがソケットってのは判るでしょう。
namelenはnameのサイズです。
でnameですが、サーバーのIPアドレスとポートを指定するためのRecordです。宣言はこんなの。
TSockAddrIn = packed record
  case Integer of
    0: (sin_family: u_short;
        sin_port: u_short;
        sin_addr: TInAddr;
        sin_zero: array[0..7] of Char);
    1: (sa_family: u_short;
        sa_data: array[0..13] of Char)
end;
見ていくべき宣言が別れていますが、0の方を見ればいいです。
sin_familyのアドレスファミリ、やはりAF_INETを入れます。
sin_portはポート。注意するのはネットワークバイトオーダ、つまりビッグエンディアンで設定しなければならないことです。
ここでhtonsを使います。
sin_addrはサーバーのIPアドレスです。inet_addrでもgethostbynameでも取得したIPアドレスはすでにビッグエンディアンになっていますのでこちらはそのまま設定します。
sin_zeroは関係ありませんが、zeroだという名前なので、0で埋めておきましょう。(^^;;

接続が成功すると、次は「送信」か「受信」ですね。
これはどっちが先かはアプリケーションによります。
どうもサーバー側が先に送信する、つまりクライアントは先に受信するものが多いようですが。
#ま、電話でも受けた方が「はい、こちらXXXです。」ってことのが多いからな。
で、受信です。
function recv(s: TSocket; var Buf; len, flags: Integer): Integer;
なにか受信するとか、エラーが発生するまで返ってきません。
で、チャット型のプログラムの場合、相手が何時データを送ってくるか、または送らないかなんて判りませんよね。
機械が相手なら、何時送ってくるかは決めうちできるのですが。
今回はしかたないので、スレッドを使います。受信だけするスレッド。
スレッドに関する説明はしませんが、DelphiにはTThreadという強い味方がいるので、スレッドといってもたいしたことはありません。

返り値は受信バイト数です。エラーの場合はマイナスの値が返ってきます。
相手が接続を断ってしまった場合は、0が返ってきます。

sは説明を飛ばして(ソケットです。もういいでしょ?)、Bufが受信データを入れるバッファです。
recvを呼び出すと、Bufの中にデータが入ってかえってくるわけです。
lenは、Bufの大きさ。Bufより大きなデータを入れようとしてデータがこぼれるのを防ぐ為のものです。
Bufに入れられなかった受信データは次のrecv呼び出しまで取っておかれるので、取りこぼしは心配しなくても大丈夫です。
flagsですが、気にせず0を入れておいてください。少し厄介な使い方をするときしか他の値を入れません。

次は送信。
function send(s: TSocket; var Buf; len, flags: Integer): Integer;
送りおわるまでは返ってきません、といっても割とすぐ返ってきます。
ということは、実はどっかのバッファに溜まっています。
返ってきたからといってさっさとclosesocketを呼び出すと、多分相手に届かないでしょう。

引数は、recvと全く一緒です。
flagsも0にしておきましょう。
恐いのは、返り値まで同じということです。
私はまだ見た事が無いのですが、lenより小さな値が返ってきたら、次のsend呼び出しで続きを送らなければなりません。
ちょっと工夫がいりますね。(「荷造り」の回で説明します)
今回は面倒なのでパスします。

あと注意事項としては、送信する文字列に行の区切りとして改行文字を入れておきましょう。
改行文字として「#13」と「#10」それに「#13#10」と用途によって変ります。
メールなら「#13#10」です。自分でゲームを作るとかなら好きにしてください。

これで、TCPのチャットクライアントソフトを作る準備ができました。
サンプルプログラムがこれ。

[ソースダウンロード]

まずPPP接続を行ってから、このソフトを立ちあげます。
ホスト名に自分のプロバイダのPOPサーバ名を、ポートに110を設定し「接続ボタン」を押すと、POP3サーバーからの最初の挨拶が表示されるはずです。
が、そんな事はしないでくださいね。
これを行っても、プロバイダのPOP3サーバーから見れば単にあなたのメーラーが途中で暴走して落ちたのと同じはずで、それくらいではPOPサーバーがおかしくなるはずはないし、管理者からも「あぁ、こいつのメーラー暴走したんだね」ぐらいにしか見えないはずですが。
これをやってプロバイダやみんなから怒られても私は責任持てません。
#と、逃げを打っておこう。(^^;;
そうですね、これを使うのはサーバー編が始まってから自分のサーバプログラムと接続するときか、自分で何かのサーバーソフトを持っていてそれにつないで表示してみる時ということにしておいてください。

-実行例-
POP3サーバと接続してみました。
え、やるなっていったばかりだって?
ちゃんとPOP3のコマンドを判ってうっているから大丈夫ですってば。(^^;
それと、PASSの後ろはほんとはパスワードです。消してあります。
実行イメージ

次の話
[表紙] [Program Files] [オブジェクト指向異聞] [プログラム未整理知識] [Winsockを使ってみようぜ] [だべり] [What's New] [書いた奴] [リンク]