ソケットで通信する

 Tcl/Tkには、インターネットで使われる通信プロトコルTCP/IPで通信する機構の最も標準的な実装であるBSDソケット(socket) を使ってコンピュータ間で通信をするコマンドが提供されています。 C言語の元来のソケットはUNIX、Windows PC、Macintosh それぞれで仕様が微妙に違うので注意が必要なのに対し、 Tcl/Tkではそれらの相違をうまく吸収しているので、 どのプラットフォームでも同じ書き方で同じ結果を得ることができます。 じゃあ、そもそもソケットでどうやって通信がされとるんじゃ? というところからの話になるとこのページだけではとても紹介しきれないので、 ソケットってナニ?というところは専門の書籍をあたってください。 ただ、C言語の場合のように「ねっとわーくばいとおーだー」の話とか、 「bind」とか「listen」など個々の手順の動作を詳しく知る必要はないので、 簡単な予備知識で十分です。

●サーバーソケットを作る
 Tcl/Tkスクリプトは、 他のマシン上のアプリケーションから接続を受けるサーバーソケットを作ることも、 他のマシン上のサーバーアプリケーションに接続してサービスを受けるクライアントソケットを作ることもでき、 ありていに言って、TCPソケットでできることはだいたいできます。 まず、サーバーソケットを作ってみます。 下のサンプルは異常にシンプルですが、 他のマシンからTCPのポート5544で接続を受け、1行のテキストメッセージを受け取り、 その内容をTcl言語で「expr」した結果を返すという、 立派なサービス(?)を行うサーバーアプリケーションです。 (ただし、セキュリティホールにならないよう注意が必要です)

proc accepted {handle cliehost clieport} {
    puts "accepted connection from: <$cliehost:$clieport>"
    set bun [gets $handle]
    flush $handle
    if {[catch {set result [expr $bun]} err]} {
        puts $handle "ERROR $err\n"
    } else {
        puts $handle "OK $result\n"
    }
    close $handle
}
set serversock [socket -server accepted 5544]
vwait zzz
 サーバーソケットを作るには、socket -server コマンドを使います。その次の引数、ここでいうacceptedは、 クライアント機から接続される度に呼ばれるプロシージャの名前 (このページではソケットハンドラと呼ぶことにします)、 最後の引数(5544)は接続を待つ (「listen」する)「ポート番号」です。 サーバーソケットが接続を待つのは、 イベント待ちの状態でなければいけません。 従って、socket -serverでサーバーソケットを作ったら、 wishのイベント待ちループか、 vwait、tkwaitなどのコマンドでむりやりイベント待ちにする必要があります。

 クライアントが接続してきたら、 定義しておいたソケットハンドラが呼び出されます。 ソケットハンドラは上のhandle、cliehost、clieportのような3つの引数をとり、 後ろの2つで世界の中のどのマシンがどのポート番号で接続してきたかがわかります。 一番重要なのは最初の引数handleで、 これをreadやgetsで読めばクライアントが書きこんだ文字列を読むことができ、 逆にこのhandleのputsすればクライアントにメッセージを返すことができます。 このhandleはいわゆる双方向ハンドルなので、 クライアントとサーバーが互いに相手の応答を待ってじっとしてしまう 「デッドロック」にならないよう、どちらかが忘れずにflushする必要があります。

 では実際にクライアント機から接続して試してみます。 クライアント機がUNIXでもWindows PCでも、シェルやDOSプロンプトから 「telnet localhost 5544」 と打ってtelnetの画面を出し、「11*20」などの数式を打って

11*20
OK 220
と応答が返ってくればOKです。

●クライアントソケットを作る
 今度は、このわずか数十行しかないサーバーアプリケーションにクライアントとして接続するTclスクリプトを書いてみます。

set sock [socket localhost 5544]
puts  $sock 3*5
flush $sock
puts  [gets $sock]
close $sock
今度は単純にsocketコマンドを使うだけです。 クライアントとして使うときのsocketの引数は、 接続したいサーバーアプリケーションが動いているマシンのホスト名 (またはIPアドレス)とポート番号です。 上の例のように、クライアントがメッセージを何か書きこみ、 これをサーバーが読んで内容に応じて応答を返す、というのは、 SMTPやHTTPなどのTCP/IPベースの有名なプレゼンテーションプロトコルも基本は全部同じです。 そういえば、HTTPで通信するようなアプリケーションをTcl/Tkで書く場合には、 httpという組み込みのパッケージを使うと、 socketでべたに書くよりは楽にそのようなアプリケーションが作れるみたいです。

●ファイルイベントの話
 上のサーバーソケットのところで少し出てきましたが、

クライアントとサーバーが互いに相手の応答を待ってじっとしてしまう 「デッドロック」にならないよう、どちらかが忘れずにflushする必要があります。
このようなことにならないように、 fileeventというコマンドを使えば、 「ソケットやファイルIDが読み書き可能になったらこの処理を実行してチョ」 というのを登録しておくことができます。 fileeventコマンドは、ソケットからの相手のデータ送信を待ちつつ、 画面表示など他の処理も同時に行う場合に特に威力を発揮します。
proc accepted {handle cliehost clieport} {
    puts "accepted connection from: <$cliehost:$clieport>"
    fileevent $handle readable "react $handle"
    puts "waiting..."
}

proc react handle {
    puts "been reactable!"
    set bun [gets $handle]
    flush $handle
    if {[catch {set result [expr $bun]} err]} {
        puts $handle "ERROR $err\n"
    } else {
        puts $handle "OK $result\n"
    }
    close $handle
}
socket -server accepted 5544
vwait zzz
# end.
「fileevent ソケットID readable 処理」 「fileevent ソケットID writable 処理」 が基本的な使い方で、「ソケットID」が「読み書き可能」 になったら「処理」を実行するように、という、そのままやん。 上の例を実行するとわかると思いますが、 readableになるタイミングは、相手がそのソケットIDにputsで書き込んで、 flushした直後になります。 これでflushするのを忘れてデッドロックになる心配はバイバイブルー (本当かなあ)これで今日から安心して眠れます。

やねうらの top
(first uploaded 1998/05/16 last updated 2005/06/18, MISUMI URANO)