Logitech Media Server (以前は Squeezebox Server と呼ばれていました) 用の制御アプリケーションを作成しています。
その一部は、ローカル ネットワークで実行されているサーバーを検出することです。これは、特別な UDP パッケージをポート 3483 にブロードキャストし、応答を待つことによって行われます。一定時間経過してもサーバーが応答しない場合 (または優先サーバーが応答する場合)、アプリケーションはリッスンを停止する必要があります。
C# 5 の async/await 機能を使用して C# で動作させましたが、F# でどのように見えるか興味がありました。私は次の関数を持っています(多かれ少なかれC#から直接翻訳されています):
let broadCast (timeout:TimeSpan) onServerDiscovered = async {
use udp = new UdpClient ( EnableBroadcast = true )
let endPoint = new IPEndPoint(IPAddress.Broadcast, 3483)
let! _ = udp.SendAsync(discoveryPacket, discoveryPacket.Length, endPoint)
|> Async.AwaitTask
let timeoutTask = Task.Delay(timeout)
let finished = ref false
while not !finished do
let recvTask = udp.ReceiveAsync()
let! _ = Task.WhenAny(timeoutTask, recvTask) |> Async.AwaitTask
finished := if not recvTask.IsCompleted then true
else let udpResult = recvTask.Result
let hostName = udpResult.RemoteEndPoint.Address.ToString()
let serverName = udpResult.Buffer |> getServerName
onServerDiscovered serverName hostName 9090
}
discoveryPacket
ブロードキャストするデータを含むバイト配列です。getServerName
他の場所で定義された関数で、サーバーの応答データから人間が読めるサーバー名を抽出します。
そのため、アプリケーションはbroadCast
タイムアウトと、サーバーが応答したときに呼び出されるコールバック関数の 2 つの引数を使用して呼び出します。このコールバック関数は、true または false を返すことで、リッスンを終了するかどうかを決定できます。サーバーが応答しない場合、またはコールバックが true を返さない場合、関数はタイムアウトの期限が切れた後に戻ります。
このコードは問題なく動作しますが、命令的な ref cell の使用に漠然と悩まされていますfinished
。
ここで質問です: 命令的なダークサイドに目を向けることなく、この種のことを行う慣用的な F#-y の方法はありますか?
アップデート
以下の受け入れられた回答に基づいて(これはほぼ正しい)、これは私が最終的に得た完全なテストプログラムです:
open System
open System.Linq
open System.Text
open System.Net
open System.Net.Sockets
open System.Threading.Tasks
let discoveryPacket =
[| byte 'd'; 0uy; 2uy; 23uy; 0uy; 0uy; 0uy; 0uy;
0uy; 0uy; 0uy; 0uy; 0uy; 1uy; 2uy; 3uy; 4uy; 5uy |]
let getUTF8String data start length =
Encoding.UTF8.GetString(data, start, length)
let getServerName data =
data |> Seq.skip 1
|> Seq.takeWhile ((<) 0uy)
|> Seq.length
|> getUTF8String data 1
let broadCast (timeout : TimeSpan) onServerDiscovered = async {
use udp = new UdpClient (EnableBroadcast = true)
let endPoint = IPEndPoint (IPAddress.Broadcast, 3483)
do! udp.SendAsync (discoveryPacket, Array.length discoveryPacket, endPoint)
|> Async.AwaitTask
|> Async.Ignore
let timeoutTask = Task.Delay timeout
let rec loop () = async {
let recvTask = udp.ReceiveAsync()
do! Task.WhenAny(timeoutTask, recvTask)
|> Async.AwaitTask
|> Async.Ignore
if recvTask.IsCompleted then
let udpResult = recvTask.Result
let hostName = udpResult.RemoteEndPoint.Address.ToString()
let serverName = getServerName udpResult.Buffer
if onServerDiscovered serverName hostName 9090 then
return () // bailout signalled from callback
else
return! loop() // we should keep listening
}
return! loop()
}
[<EntryPoint>]
let main argv =
let serverDiscovered serverName hostName hostPort =
printfn "%s @ %s : %d" serverName hostName hostPort
false
let timeout = TimeSpan.FromSeconds(5.0)
broadCast timeout serverDiscovered |> Async.RunSynchronously
printfn "Done listening"
0 // return an integer exit code