こける Wired-Winsockを使ってみようぜ-8.固まらない通信'97/12/30

8.固まらない通信


さて、再度チャットクライアントです。
今度は固まらないように非同期モードで作成します。

前に作ったTCPクライアントソフトは「同期モード」ですが、今度は「非同期モード」です。
それでも送信、受信、接続それぞれのAPIはまったく同じです。
ただ「呼び出しても固まらない」時点をwinsockからメッセージで教えてもらうんです。
また、「呼び出したら固まりそう」になると、エラーにしてもらいます。

こんな風にソケットに対する操作のモードを変えるのが、こいつ。
function WSAAsyncSelect(s:TSocket; HWindow:HWnd; wMsg: Integer; lEvent:LongInt):Integer;
HWindowってのが「どこに送ってもらいたいか」です。
wMsgが「それが発生したとき何を送るか」です。
ここにAllocateHWndで確保したHWndを設定します。固まらない住所調べで紹介したWSAAsyncGetHostByNameといっしょですね。
lEventは、「どのAPIを非同期モードにするか、どのイベントの時メッセージを送ってもらうか」です。
FD_READ,FD_WRITE,FD_CLOSE,FD_CONNECTでそれぞれ受信,送信,切断(相手のね),接続を設定します。
これはビットパターンになっているので、or で結んでいきます。
指定しなかった物は逆に同期モードに戻されます。追加設定ではないので注意してください。
また、どのイベントが発生しても飛んでくるメッセージは1種類です。複数指定したときにどのイベントがおきたのかはlParamで分かります。

sendやrecvはこのイベントが来てからでいいのですが、connectでなにか疑問を感じませんか?
connectでサーバーのパラメータを渡す前に、「接続しても固まらない時点」即ち「接続完了時点」がくるわけがない。(^^;;
で、connectは1度「固まるからエラー」と発生させておきます。イベントが来た時点ではつながっているのでconnectをやり直す必要はありません。
また、closeもちょっと違う。イベントの発生は「相手の切断」です。なので自分からcloseしたときには発生しません。
それにcloseを同期モードにするわけではありません。

返り値は0かSOCKET_ERRORです。バグでも無ければSOCKET_ERRORにはならないでしょう。

これを使って発生したメッセージをこんな感じで処理します。
procedure TXXX.WndProc(var Msg:TMessage);
begin
        if Msg.Msg<>wMsgで指定したメッセージ then begin
                { これは必要ないメッセージなのでデフォルト処理 }
                Msg.Result:=DefWindowProc(「AllocateHWndが返したHWnd」,Msg.Msg,Msg.wParam,Msg.lParam);
                Exit;
        end;
        TSocket(Msg.wParam)がソケットです。使わないような気がしますね。
        if WSAGetSelectError(Msg.lParam)<>0 then begin
                エラーが発生しています。
                WSAGetSelectError(Msg.lParam)がWSAGetLastErrorと同じコード体系です。
        end;
        Case WSAGetSelectEvent(Msg.lParam) of
        FD_CONNECT:
                begin
                        接続時の処理を行ってください
                end;
        FD_READ:
                begin
                        recvを呼び出して受信すべきです。
                end;
        FD_WRITE:
                begin
                        送信できます。
                end;
        FD_CLOSE:
                begin
                        相手から切断してきました。
                        自分もsocketcloseでソケットを解放しましょう。
                end;
        end;
end;
WSAGetSelectEventとWSAGetSelectErrorの説明がまだですね。
WSAAsyncSelectではメッセージを1種類しか登録できません。
どのメッセージが発生したかを知るのがWSAGetSelectEventです。
function WSAGetSelectEvent(Param: LongInt): Word;
メッセージのlParamを引数にして渡すとどのイベントが発生したのかを割り出してくれる関数です。

またエラーの発生はWSAGetSelectErrorで判ります。
function WSAGetSelectEvent(Param: LongInt): Word;
これまたメッセージのlParamを引数にして渡します。
すると返り値でエラーコードを返してくれるわけです。
これでエラーの有無を確かめられます。
エラーが無ければ0です。

受信のメッセージが来たときの注意事項ですが、たとえば10バイトのデータがあったとして、このメッセージでrecvは2Byteしか取らなかった場合、すぐにまた受信メッセージが発生します。
もし、まったく受け取らなかったら、このメッセージだけを発生させ続けることになります。
送信の方は、送信メッセージでなにも送らなかったとしたら、もう来ません。

受信のほうは受信メッセージだけで受信すればいいのですが、送信のほうはそうはいきません。
送信したい時に送信するってのを基本方針においた場合、送信したかったんだけど「固まるからエラー」になっちゃった分を送信メッセージで送り出すことになります。
すると、送信する処理を2ヶ所に書かなければいけないわけですね。
それに、アプリケーション側に「送信できなかった」といってつっかえすのもライブラリとしては心苦しいものがあります。
送信バッファを作りましょう。
アプリケーションが呼び出した「送信メソッド」では、この送信バッファにデータを追加します。
で、送信メッセージが来た時と送信バッファにデータが追加された時この送信バッファからデータを送り出します。

送信バッファの性格としては「可変長」で「追加でき」「(送れた分)前から削除できる」という事が必要です。
C++BuilderならSTLの出番なのですが、Delphiでは使えません。
しかし、可変長のバッファ構造で何か思い付きませんか?
ぱっと思い出すのはTMemoryStreamですが、こいつは「前から削除」ができません。
もっと基本的な奴。そ、文字列です。
これならCopy関数で前から削除ができますし、可変長だし、文字列同士の足し算は追加書き込みですしね。
文字列変数には実はPCharにさえ変換しなければバイナリデータでも覚えられますし。

さて、送信バッファはこれでいいとして、送信メッセージが来た時と送信バッファに追加書き込みした時の両方に送信処理を入れなければなりません。
ま、単に送信処理の手続き作って、両方から呼び出せばいいのですけど。
せっかくだからこの時にちょっと役立つAPIを紹介しておきましょう。
WSAMakeSelectReplyです。
function WSAMakeSelectReply(Event, Error: Word): Longint;
WSAGetSelectErrorとWSAGetSelectEventを使って切り分けられるlParamを作り出します。
EventにはFD_WRITEを指定し、Errorに0を入れてlParamを作り、ソケットからwParamを作って、PostMessageすれば「送信メッセージ」を作り出せるわけです。
送信バッファに追加書き込みした場合はこうしてダミーの送信メッセージを作り出します。
送信メッセージの処理ではWSAEWOULDBLOCK「固まるからエラー」が発生するかも知れない、とだけ注意しとけば大丈夫です。

コンポーネントにしたてあげるには、プロパティに「サーバのIPアドレス」を指定してもらうことになるのですが、前回作ったTHostInfを組み込めばプロパティには「サーバのIPアドレス」でも「サーバ名」でもどちらでも対応できますね。
しかし、IPアドレスだけ知りたいのですから、IPアドレスを入力された時はわざわざDNSに聞きに行かなくても良いでしょう。
特殊処理としてinet_addrを入れておきます。

コンポーネントとして用意しておくイベントは、こんな感じでしょうか。
type TKWSockErrEvent=procedure (Sender: TObject; ErrCod:Integer) of object;

property OnError: TKWSockErrEvent;      { エラー発生を伝える }
property OnConnect: TNotifyEvent;       { 接続ができたことを伝える }
property OnRecv: TNotifyEvent;          { 受信を伝える、新たに受信した時またはRecvしたより多く受信していれば発生する }
property OnSendEmpty: TNotifyEvent;     { 送信バッファが空になった事を伝える }
proprtty OnClose: TNotifyEvent;         { 相手が切断したことを伝える }
メソッドはこんなとこかな。
procedure Send(const data; len: Integer);
function Recv(var data; len: Integer):Integer;
procedure Close;
procedure Connect;
プロパティとしてはこんな所を用意しておきましょうか。
property Server: string;        { サーバアドレス IPかホスト名 }
property Port: Word;            { 接続するポート }
property RecvData: string;      { 実行時読み出しのみ Recvで読み出してない受信データ バイナリかも知れない }
という形で仕上げたのがこれ。
[ソースダウンロード]

-実行例-
見た目は前と変りませんが。
今度はWebページでエラーした場合を表示してみました。改行コードが違うのでちょっと見難いですが、でも判るでしょう。
実行イメージ

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