我正在为Logitech Media Server(以前称为Squeezebox Server)编写控件应用程序。
其中一小部分是发现哪些服务器在本地网络上运行。这是通过向端口3483广播特殊的UDP包并等待回复来完成的。如果服务器在给定时间后没有回复(或首选服务器回复),则应用程序应该停止监听。
我使用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
,即服务器回复时将调用的超时和回调函数。然后,通过返回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
答案 0 :(得分:4)
您可以使用递归函数“功能性地”实现此功能,该递归函数也会产生Async&lt; T&gt;。值(在本例中为Async)。这段代码应该可行 - 它基于你提供的代码 - 虽然我无法测试它,因为它依赖于代码的其他部分。
open System
open System.Net
open System.Net.Sockets
open System.Threading.Tasks
open Microsoft.FSharp.Control
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 rec loop () =
async {
let timeoutTask = Task.Delay timeout
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
onServerDiscovered serverName hostName 9090
return! loop ()
}
return! loop ()
}
答案 1 :(得分:2)
我会清理异步调用并使用异步超时,更像是这样:
open System.Net
let discoveryPacket =
[|'d'B; 0uy; 2uy; 23uy; 0uy; 0uy; 0uy; 0uy;
0uy; 0uy; 0uy; 0uy; 0uy; 1uy; 2uy; 3uy; 4uy; 5uy|]
let getUTF8String data start length =
System.Text.Encoding.UTF8.GetString(data, start, length)
let getServerName data =
data
|> Seq.skip 1
|> Seq.takeWhile ((<) 0uy)
|> Seq.length
|> getUTF8String data 1
type Sockets.UdpClient with
member client.AsyncSend(bytes, length, ep) =
let beginSend(f, o) = client.BeginSend(bytes, length, ep, f, o)
Async.FromBeginEnd(beginSend, client.EndSend)
member client.AsyncReceive() =
async { let ep = ref null
let endRecv res =
client.EndReceive(res, ep)
let! bytes = Async.FromBeginEnd(client.BeginReceive, endRecv)
return bytes, !ep }
let broadCast onServerDiscovered =
async { use udp = new Sockets.UdpClient (EnableBroadcast = true)
let endPoint = IPEndPoint (IPAddress.Broadcast, 3483)
let! _ = udp.AsyncSend(discoveryPacket, discoveryPacket.Length, endPoint)
while true do
let! bytes, ep = udp.AsyncReceive()
let hostName = ep.Address.ToString()
let serverName = getServerName bytes
onServerDiscovered serverName hostName 9090 }
do
let serverDiscovered serverName hostName hostPort =
printfn "%s @ %s : %d" serverName hostName hostPort
let timeout = 5000
try
Async.RunSynchronously(broadCast serverDiscovered, timeout)
with _ -> ()
printfn "Done listening"
我还会将基于副作用的serverDiscovered
函数替换为不同的体系结构,例如收集5s回复的异步代理。