PowershellでAsyncCallbackを使うにはどうしたらよいか
PowershellでUdp双方向通信を行うスクリプトを書こうとしてハマった
前提
メインスレッド Udp送信担当
サブスレッド Udp受信担当
受信はメインスレッド内でUdpClient.BeginReceiveを呼び出して非同期処理にする。
コールバック関数を定義し、 Receiveが値を受け取った時に関数呼び出し(受信データを基にメインスレッドのフォーム更新や結果のUdp送信など)を実行して、再びUdpClient.BeginReceiveを呼ぶ
いわゆるAPM(Asynchronous Programming Model)ってやつです
スクリプト
受信開始とコールバックのところのみ
(略) # UDP受信待ち受け function udpreceiver_start { param( $ipaddress = "127.0.0.1", #バインドするアドレス $port = 4444 #バインドするポート ) # ローカルエンドポイント定義 $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port) # リモートエンドポイント定義 #$remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0); # UDPサーバーを生成 $udprecv = new-Object System.Net.Sockets.UdpClient($localEP) # 非同期的なデータ受信を開始する Write-Host "BeginReceive!" $handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv); } # UDP受信callback function udpreceiver_callback { param( $ar ) Write-host "Callback!!" $udprecv = [System.Net.Sockets.UdpClient]{$ar.AsyncState}; # 非同期受信を終了する [System.Net.IPEndPoint]$remoteEP = $null; try{ $buf = $udprecv.EndReceive($ar,[ref]$remoteEP); } catch [System.Net.Sockets.SocketException] { Write-Host ("受信エラー({0}/{1})" -f $_.Message, $_.ErrorCode) return; } catch [ObjectDisposedException] { //すでに閉じている時は終了 Write-Host "Socketは閉じられています。" return; } $str = [System.TexT.Encoding]::UTF8.GetString($buf) Write-host "データを受信しました" Write-Host ("受信したデータ:{0}" -f $str) Write-Host ("送信元アドレス:{0}/ポート番号:{1}" -f $remoteEP.Address, $remoteEP.Port) //再びデータ受信を開始する $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv); } (略) udpreceiver_start (略)
問題点
UDPでデータを受信すると・・・
_人人 人人_
> 突然の死 <
 ̄YYYY ̄
原因分析1
コールバックとして普通の関数を[System.AsyncCallback]
でキャストしたものを渡したのが原因だろうかと思って、そこをC#で書いて渡してみる。
(略) # UDP受信待ち受け function udpreceiver_start { param( $ipaddress = "127.0.0.1", #バインドするアドレス $port = 4444 #バインドするポート ) # ローカルエンドポイント定義 $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port) # リモートエンドポイント定義(空:後で使う) $remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0); # UDPサーバーを生成 $udprecv = new-Object System.Net.Sockets.UdpClient($localEP) # 非同期的なデータ受信を開始する #無理だった Write-Host "BeginReceive!" #$handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv); $handle = $udprecv.BeginReceive([Udp]::udpreceiver_callback,$udprecv); } Add-type @" using System; using System.Net; using System.Net.Sockets; using System.Text; public static class Udp { public static void udpreceiver_callback(IAsyncResult ar=null) { Console.WriteLine("Callback!"); var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState; byte[] buf; // 非同期受信を終了する var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0); try { buf = udprecv.EndReceive(ar, ref remoteEP); } catch(System.Net.Sockets.SocketException ex) { Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode); return; } catch(ObjectDisposedException) { //すでに閉じている時は終了 Console.WriteLine("Socketは閉じられています。"); return; } var str = System.Text.Encoding.UTF8.GetString(buf); Console.WriteLine("データを受信しました"); Console.WriteLine("受信したデータ:{0}", str); Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port); //再びデータ受信を開始する Console.WriteLine("BeginReceive!"); udprecv.BeginReceive(udpreceiver_callback, udprecv); } } "@ (略) udpreceiver_start (略)
・
・
・
・
_人人 人人_
> 突然の死 <
 ̄YYYY ̄
ダメでした
対応策
全部C#で書く(爆)
(略) Add-type @" using System; using System.Net; using System.Net.Sockets; using System.Text; public static class Udp { public static void udpreceiver_start( //AsyncCallback callback, string ipaddress = "127.0.0.1", //バインドするアドレス int port = 4445 //バインドするポート ) { //ローカルエンドポイント定義 var localEP = new System.Net.IPEndPoint(System.Net.IPAddress.Parse(ipaddress), port); //リモートエンドポイント定義(空:後で使う) var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0); //UDPサーバーを生成 var udpClient = new System.Net.Sockets.UdpClient(localEP); //非同期的なデータ受信を開始する Console.WriteLine("BeginReceive!"); //BeginReceive2(callback, udpClient); udpClient.BeginReceive(udpreceiver_callback, udpClient); } public static void udpreceiver_callback(IAsyncResult ar=null) { Console.WriteLine("Callback!"); var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState; byte[] buf; // 非同期受信を終了する var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0); try { buf = udprecv.EndReceive(ar, ref remoteEP); } catch(System.Net.Sockets.SocketException ex) { Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode); return; } catch(ObjectDisposedException) { //すでに閉じている時は終了 Console.WriteLine("Socketは閉じられています。"); return; } var str = System.Text.Encoding.UTF8.GetString(buf); Console.WriteLine("データを受信しました"); Console.WriteLine("受信したデータ:{0}", str); Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port); //再びデータ受信を開始する udprecv.BeginReceive(udpreceiver_callback, udprecv); } } "@ (略) [Udp]::udpreceiver_start() (略)
これだとうまくいきました。
改良版
PowershellのUdpClientではBeginReceive()
メソッドを呼び出さず、C#で記述したstaticメソッド[Udp]::BeginReceive
の中で呼び出すようにすると、
問題点1を解決しつつ、startのメソッドをpowershellにできます。
(略) # UDP受信待ち受け function udpreceiver_start { param( $ipaddress = "127.0.0.1", #バインドするアドレス $port = 4445 #バインドするポート ) # ローカルエンドポイント定義 $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port) # リモートエンドポイント定義(空:後で使う) $remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0); # UDPサーバーを生成 $udprecv = new-Object System.Net.Sockets.UdpClient($localEP) # 非同期的なデータ受信を開始する Write-Host "BeginReceive!" #$handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv); $handle = [UDP]::BeginReceive($null,$udprecv) } Add-type @" using System; using System.Net; using System.Net.Sockets; using System.Text; public static class Udp { public static void udpreceiver_callback(IAsyncResult ar) { Console.WriteLine("Callback!"); var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState; byte[] buf; // 非同期受信を終了する var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0); try { buf = udprecv.EndReceive(ar, ref remoteEP); } catch(System.Net.Sockets.SocketException ex) { Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode); return; } catch(ObjectDisposedException) { //すでに閉じている時は終了 Console.WriteLine("Socketは閉じられています。"); return; } var str = System.Text.Encoding.UTF8.GetString(buf); Console.WriteLine("データを受信しました"); Console.WriteLine("受信したデータ:{0}", str); Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port); //再びデータ受信を開始する udprecv.BeginReceive(udpreceiver_callback, udprecv); } // UdpClient.BeginReceive(callback)を呼び出す関数 public static System.IAsyncResult BeginReceive( Action<IAsyncResult> callback, System.Net.Sockets.UdpClient udpClient ) { return udpClient.BeginReceive( _ => {udpreceiver_callback(_);}, udpClient); } } "@ udpreceiver_start
ちなみにpowershellで書いた関数をコールバック内で呼ぶだけで死ぬので、コールバックはC#で書くしかなかった。どうしようもなかった。