2008年04月14日

VB.NETでUDP非同期通信

(少々内容の修正をしました)

複数のマシン上で起動するあるプログラムにおいて、相互に他のマシン上で稼動している同プログラムが、正常に稼動しているかどうかを監視する必要が出た。
方法論としては、たとえば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を記述しておくとよいだろう。
 

【VB.NETの最新記事】
posted by Tig3r at 15:31| Comment(1) | TrackBack(0) | VB.NET
この記事へのコメント
大変役立ちました
Posted by 高宮憲隆 at 2019年02月03日 14:59
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/14012101

この記事へのトラックバック