2013-01-01 48 views
5

我正在爲Logitech媒體服務器(以前稱爲Squeezebox Server)編寫控制應用程序。從F#異步工作流程中刪除命令式代碼

其中的一小部分是發現哪些服務器正在本地網絡上運行。這是通過向端口3483廣播一個特殊的UDP包並等待回覆來完成的。如果沒有服務器在給定時間後回覆(或首選服務器回覆),應用程序應該停止監聽。

我在C#中使用C#5的異步/等待功能,但我很好奇,看看它在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,則該函數在超時到期後返回。

此代碼工作得很好,但我對使用命令式參考單元finished隱約感到困擾。

所以,這裏有一個問題:有沒有一種慣用的F#方式來做這種事情,而不轉向必要的黑暗面?

更新

基於下面的接受的答案(這是非常接近右),這是一個完整的測試程序,我結束了:

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

爲什麼不寫一個無限循環,而是使用'Async.RunSynchronously'指定5秒超時運行它? –

+0

@Jon Harrop - 因爲在真正的程序中(這只是一個簡單的例子),我不想同步運行發現。 – corvuscorax

回答

4

您可以實現這個「功能」用遞歸函數也產生一個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() 
    } 
+1

由於任務在創建時立即啓動,與異步工作流不同,我懷疑你應該在'loop'異步塊內移動'timeoutTask'的聲明。這樣你就可以得到一個新的'timeoutTask'來與每個新的'recvTask'一起使用。 –

+0

謝謝喬爾,現在已經修好了。 –

+0

實際上,在循環之外擁有'timeoutTask'就是我所追求的 - 那樣的話,我只需要一個整體5秒的時間(比如說)我在監聽服務器的位置,而不管響應是什麼時候進入。 但是傑克斯的代碼幾乎是正確的,我會在一分鐘後發佈完整的解決方案,謝謝。 – corvuscorax

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功能,像異步代理收集5秒的回覆。