6

Logitech Media Server (以前は Squeezebox Server と呼ばれていました) 用の制御アプリケーションを作成しています。

その一部は、ローカル ネットワークで実行されているサーバーを検出することです。これは、特別な UDP パッケージをポート 3483 にブロードキャストし、応答を待つことによって行われます。一定時間経過してもサーバーが応答しない場合 (または優先サーバーが応答する場合)、アプリケーションはリッスンを停止する必要があります。

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タイムアウトと、サーバーが応答したときに呼び出されるコールバック関数の 2 つの引数を使用して呼び出します。このコールバック関数は、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 に答える