6

我正在为 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
4

2 回答 2

5

您可以使用递归函数实现此“功能性”,该函数还产生 Async<'T> 值(在本例中为 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 ()
    }
于 2013-01-01T15:20:48.637 回答
3

我会清理异步调用并使用异步超时,更像这样:

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不同的架构替换您基于副作用的功能,例如收集 5 秒回复的异步代理。

于 2013-01-04T01:54:35.863 回答