方法論としては、たとえばDBサーバに対してログを記録しつつ、それを参照したりといったスター型の構築も出来た(そして今考えれば、そっちが多分簡単だった)けど、ここはあえて、プログラム間で相互に通信を行うことで、他のプログラムの稼動状況を監視してみよう、と思い立った。苦難の道の始まりだったのだけど。
プログラム間で相互に小さなメッセージをやりとりすることで監視する、という方法論に適しているのは、やはりオーバヘッドが少なく、かつ接続を占有しないUDPだろうと思う。最大で10台くらいが相互に監視しあう状況になるので、わざわざTCPでコネクション貼ることもないだろう。
開発言語はVB.NET。すなわち.NETフレームワークの世界でそれを記述することになる。
.NETフレームワーク2.0では、きっちりUDPの非同期通信がサポートされている。
System.Net.Sockets.UdpClientがそれだ。
まず不特定との相手先と通信するためのソケットをインスタンスとして生成する。
というわけで、まずは不特定の相手と通信するためのソケットを生成。ポート番号は2000としてみた。
UDPソケットを保持するUDPState構造体を用意しておく。
Public Class UDPStateソケットを生成する。
Public e As Net.IPEndPoint
Public u As Net.Sockets.UdpClient
End Class
Public S as UDPState
S = New UDPState()
S.e = New Net.IpEndPoint(Net.IpAddress.Any, 2000)
S.u = New Net.Sockets.UdpClient(S.e)
これだけ。
ただしこれではソケットを準備しただけなので、受信は始まらないが。
先に送信を解決する。
送信は送りたいメッセージのバイト配列を作成して、それをアドレス指定して送信するのみ。
日本語を使うことはとりあえず考えず、Asciiで処理すると、こんな感じ。
Public Sub Send(ByVal Addr As String, ByVal Msg as String)なんとこれだけ。
Dim MsgBt As Byte() = System.Text.Encoding.ASCII.GetBytes(Msg)
S.u.Send(MsgBt, MsgBt.Length, Addr, 2000)
End Sub
ただし、相手先アドレスの名前解決ができなかったり、相手とコネクションできなかったりの場合には例外がライズするので、適宜キャッチする必要がある。
問題は受信。同期受信をするなら簡単な話で、Receiveメソッドを使えばメッセージが来るまで待って、メッセージが着いたらその内容を返してくれる。用途によってはこれで十分だが、通信において何か受信するまでじーっと待っているというのはあまり実用的ではない。
なので今回はコールバックによる非同期通信を実装してみる。これならば、受信時に割り込みで別スレッドでの処理が行われるため、メインの処理が止まることはない。
非同期受信の開始は、.NETフレームワーク2.0で実装された、BeginReceiveメソッド。
こいつにコールバックされる関数のアドレスと、UDPState構造体を渡してコールする。
S.u.BeginReceive(AddressOf receivecallback, S)
こうしておくことで、UDPソケットは受信状態で開かれ、何か受信したら、receivecallbackルーチンが、Sで示される構造体を引数としてシステムから…この場合はフレームワークから、かな?…呼び出される。受信というイベントを定義して、そのイベントプロシジャを呼び出すような感じ。
コールバックで呼ばれる関数、receivecallbackを定義する。
Public Sub receivecallback(ByVal AR As IAsyncResult)
dim LU As Net.Sockets.UdpClient = CType(AR.AsyncState, UDPState).u
dim LE As Net.IPEndPoint = CType(AR.AsyncState, UDPState).e
dim receiveBytes As Byte() = LU.EndReceive(AR, LE)
dim receiveString As String = System.Text.Encoding.ASCII.GetString(receiveBytes)
dim receiveAddress As System.Net.IPAddress = LE.Address
'ここに受信後の処理を記述
End Sub
と、こういうふうに記述する。これでUDP送信元のアドレスがreceiveAddressに、受け取ったメッセージがreceiveStringにセットされるので、それを利用して処理ができる。
コールバック関数が呼び出される際に、BeginReceiveで指定したオブジェクト型の引数が、IAsyncResult型として渡される。これはUDPClient.BeginReceiveに限らず、非同期のBegin何とかメソッドで共通した機構らしい。
なので後は渡された引数を、与えた型、つまりこの場合はUDPState型にキャストして、その要素を取得すればよいわけだ。
注意すべき点として、コールバックで呼ばれる関数内で、そのクラス内のインスタンス変数とかグローバル変数とかにアクセスしようとしないこと。コールバック関数はメインと別のスレッドで動作するので、たとえば受信した内容をフォームのTextBoxに書き出そうとか思って、そのままTextBox.Text=receiveTextとかすると、スレッドをまたいでの操作となるので、値の設定ができず、例外が発生する。
これをスレッドセーフの処理にするためにはデリゲートを利用する。
TextBox.Textに値を代入するSUBを定義し、そのデリゲートを定義。そしてreceivecallback内で、デリゲートをInvokeする。
UDP通信関連を別クラスで実装した場合はなかなかややこしい。
メインクラス・通信クラス・ReceiveCallbackという登場人物の間で、
メインクラスで処理を行うロジック、
上のロジックを、通信クラスから呼び出すためのデリゲート
通信クラスで、メインのデリゲートを呼び出すためのロジック
ReceiveCallback内で、上記のロジックを呼び出すためのデリゲート
ReceiveCallback内のInvoke記述
といったように、2段階のデリゲートを介してメインクラスの関数を呼び出すことになる。とはいえ、最近一年ほどVB.NETと離れているせいか、うまく説明できない。ぜひ、このあたり(codezine)を参考にしてほしい。
なお、BeginReceiveを行ってから、Sendを実行すると、そこでソケットが送信用に使われてしまい、待ちうけ状態が解除されるので、Sendの後に再度BeginReceiveを行わないと、それ以降何かを受信しても反応がなくなる。Sendを行うSUBの最後あたりに、BeginReceiveを記述しておくとよいだろう。