我正在为 Logitech Media Server(以前称为 Squeezebox Server)编写一个控制应用程序。
其中一小部分是发现哪些服务器正在本地网络上运行。这是通过向端口 3483 广播一个特殊的 UDP 包并等待回复来完成的。如果在给定时间后没有服务器回复(或首选服务器回复),则应用程序应停止侦听。
我让它在 C# 中工作,使用 C# 5 的 async/await 功能,但我很想知道它在 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
使用两个参数调用,一个超时和一个回调函数,当服务器回复时将调用该函数。然后,此回调函数可以通过返回 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