2013-01-01 25 views
5

Tôi đang viết một ứng dụng điều khiển cho Logitech Media Server (trước đây gọi là Squeezebox Server).Xóa mã bắt buộc từ F # async dòng làm việc

Một phần nhỏ trong số đó là khám phá mà máy chủ đang chạy trên mạng nội bộ. Điều này được thực hiện bằng cách phát sóng một gói UDP đặc biệt đến cổng 3483 và chờ trả lời. Nếu không có máy chủ trả lời sau một thời gian nhất định, (hoặc một máy chủ ưa thích trả lời) ứng dụng sẽ ngừng nghe.

Tôi có nó làm việc trong C#, sử dụng async/chờ đợi tính năng của C# 5, nhưng tôi đã tò mò muốn xem làm thế nào nó sẽ tìm trong F #. Tôi có chức năng sau (ít nhiều được dịch trực tiếp từ 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 là mảng byte chứa dữ liệu phát sóng. getServerName là một hàm được định nghĩa ở nơi khác, trích xuất tên máy chủ có thể đọc được từ dữ liệu trả lời của máy chủ.

Vì vậy, ứng dụng gọi broadCast với hai đối số, thời gian chờ và chức năng gọi lại sẽ được gọi khi máy chủ trả lời. Hàm gọi lại này sau đó có thể quyết định kết thúc nghe hay không, bằng cách trả về true hoặc false. Nếu không có máy chủ trả lời, hoặc không gọi lại trả về true, hàm trả về sau khi hết thời gian chờ.

Mã này hoạt động tốt, nhưng tôi mơ hồ bị làm phiền bởi việc sử dụng ô ref imperative finished.

Vì vậy, đây là câu hỏi: liệu có một F # -y cách thành ngữ để làm điều này loại điều mà không chuyển sang mặt tối bắt buộc?

Cập nhật

Dựa trên câu trả lời chấp nhận bên dưới (mà đã rất gần bên phải), đây là chương trình thử nghiệm đầy đủ tôi đã kết thúc với:

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

Tại sao bạn không viết một vòng lặp vô hạn nhưng chạy nó sử dụng 'Async.RunSynchronously' quy định cụ thể một thời gian chờ 5 giây? –

+0

@ Jon Harrop - bởi vì trong chương trình thực tế (đây chỉ là một ví dụ bare-bones) Tôi không muốn chạy việc khám phá cách đồng bộ. – corvuscorax

Trả lời

4

Bạn có thể thực hiện điều này "chức năng" với chức năng đệ quy cũng tạo ra giá trị Async < 'T> (trong trường hợp này là Async). Mã này sẽ hoạt động - dựa trên mã bạn đã cung cấp - mặc dù tôi không thể kiểm tra mã vì nó phụ thuộc vào các phần khác của mã của bạn.

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

Vì Tác vụ được bắt đầu ngay lập tức khi được tạo, không giống như luồng công việc không đồng bộ, tôi nghi ngờ bạn nên di chuyển khai báo 'timeoutTask' bên trong khối không đồng bộ' loop'. Bằng cách đó bạn nhận được một 'timeoutTask' mới để đi với mỗi' recvTask' mới. –

+0

Cảm ơn Joel, nó đã được sửa. –

+0

Trên thực tế, có 'timeoutTask' bên ngoài vòng lặp là những gì tôi đang theo sau - theo cách đó chỉ có một khoảng thời gian 5 giây tổng thể (nói), nơi tôi đang nghe máy chủ, độc lập khi các câu trả lời đến. Nhưng Mã Jacks gần như đúng, tôi sẽ đăng giải pháp đầy đủ trong một phút, cảm ơn. – corvuscorax

2

tôi sẽ khử trùng các cuộc gọi async và sử dụng một thời gian chờ async, nhiều như thế này:

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" 

Tôi cũng muốn thay thế tác dụng phụ dựa trên serverDiscovered chức năng của bạn với một kiến ​​trúc khác nhau, giống như một đại lý không đồng bộ thu thập câu trả lời cho 5s.

Các vấn đề liên quan