Asynchronous 从F#异步工作流中删除命令代码

Asynchronous 从F#异步工作流中删除命令代码,asynchronous,f#,Asynchronous,F#,我正在为Logitech媒体服务器(以前称为挤压盒服务器)编写一个控制应用程序 其中一小部分是发现哪些服务器正在本地网络上运行。这是通过向端口3483广播一个特殊的UDP包并等待回复来完成的。如果给定时间后没有服务器应答(或首选服务器应答),则应用程序应停止侦听 我让它在C#中工作,使用C#5的异步/等待特性,但我很好奇它在F#中会是什么样子。我有以下函数(或多或少直接从C#翻译过来): discoveryPacket是一个字节数组,包含要广播的数据getServerName是在别处定义的一个函

我正在为Logitech媒体服务器(以前称为挤压盒服务器)编写一个控制应用程序

其中一小部分是发现哪些服务器正在本地网络上运行。这是通过向端口3483广播一个特殊的UDP包并等待回复来完成的。如果给定时间后没有服务器应答(或首选服务器应答),则应用程序应停止侦听

我让它在C#中工作,使用C#5的异步/等待特性,但我很好奇它在F#中会是什么样子。我有以下函数(或多或少直接从C#翻译过来):

discoveryPacket
是一个字节数组,包含要广播的数据
getServerName
是在别处定义的一个函数,它从服务器应答数据中提取人类可读的服务器名称

因此,应用程序使用两个参数调用
broadCast
,一个超时和一个回调函数,当服务器应答时将调用该函数。然后,这个回调函数可以通过返回true或false来决定是否结束侦听。如果没有服务器应答,或者没有回调返回true,则函数将在超时过期后返回

这段代码运行得很好,但我对命令式ref单元格的使用感到有些不安
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
开放系统
开放系统
开放系统.Text
开放系统.Net
开放式System.Net.Sockets
开放系统.Threading.Tasks
让发现包=
字节'd';0uy;2uy;23uy;0uy;0uy;0uy;0uy;0uy;
0uy;0uy;0uy;0uy;0uy;1uy;2uy;3uy;4uy;5uy |]
让getUTF8String数据开始长度=
Encoding.UTF8.GetString(数据、开始、长度)
让getServerName读取数据=
数据|>序号跳过1
|>序号takeWhile((序号长度
|>getUTF8String数据1
让服务器上的广播(超时:TimeSpan)发现=异步{
使用udp=新的UdpClient(EnableBroadcast=true)
let endPoint=IPEndPoint(IPAddress.Broadcast,3483)
do!udp.SendAsync(discoveryPacket,Array.length discoveryPacket,endPoint)
|>异步任务
|>异步。忽略
let timeoutTask=Task.Delay超时
让rec循环()=异步{
让recvTask=udp.ReceiveAsync()
do!Task.WhenAny(超时任务,recvTask)
|>异步任务
|>异步。忽略
如果recvTask.IsCompleted,则
让udpResult=recvTask.Result
让hostName=udpResult.RemoteEndPoint.Address.ToString()
让serverName=getServerName udpResult.Buffer
如果OnServerName发现了serverName主机名9090,则
return()//从回调发出紧急救援信号
其他的
return!loop()//我们应该继续侦听
}
return!loop()
}
[]
让主argv=
让服务器发现服务器名主机名主机端口=
printfn“%s@%s:%d”服务器名主机名主机端口
假的
让timeout=TimeSpan.FromSeconds(5.0)
广播超时服务器发现|>Async.RunSynchronously
printfn“完成侦听”
0//返回整数退出代码

您可以使用递归函数“从功能上”实现此功能,递归函数也会生成异步调用我将清理异步调用并使用异步超时,更像这样:

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"
开放系统.Net
让发现包=
[d'B;0uy;2uy;23uy;0uy;0uy;0uy;0uy;
0uy;0uy;0uy;0uy;0uy;1uy;2uy;3uy;4uy;5uy |]
让getUTF8String数据开始长度=
System.Text.Encoding.UTF8.GetString(数据、开始、长度)
让getServerName读取数据=
数据
|>序号1
|>序号takeWhile((序号长度
|>getUTF8String数据1
输入Sockets.UdpClient和
成员client.AsyncSend(字节、长度、ep)=
让beginSend(f,o)=client.beginSend(字节,长度,ep,f,o)
Async.FromBeginEnd(beginSend,client.EndSend)
成员client.AsyncReceive()=
异步{let ep=ref null
让我们重新开始=
client.EndReceive(res,ep)
let!bytes=Async.FromBeginEnd(client.BeginReceive,endRecv)
返回字节,!ep}
让服务器上的广播被发现=
async{use udp=new Sockets.UdpClient(EnableBroadcast=true)
let endPoint=IPEndPoint(IPAddress.Broadcast,3483)
let!\uux=udp.AsyncSend(discoveryPacket,discoveryPacket.Length,endPoint)
尽管如此
let!bytes,ep=udp.AsyncReceive()
让hostName=ep.Address.ToString()
让serverName=getServerName字节
OnServerName主机名9090}
做
让服务器发现serverName主机名主机端口=
printfn“%s@%s:%d”服务器名主机名主机端口
让超时=5000
尝试
异步运行(发现广播服务器,超时)
带->()
printfn“完成侦听”

我还将使用不同的体系结构来替换基于副作用的
serverDiscovered
功能,例如收集5天回复的异步代理。

由于任务在创建时立即启动,与异步工作流不同,我怀疑您应该将
timeoutTask
的声明移动到
循环
异步块中k、 这样你就可以得到一个新的
timeoutTask
来处理每一个新的
recvTask
。谢谢Joel,它现在已经修好了。实际上,让
timeoutTask
在循环之外是我想要的-这样,总共只有一个5秒的周期(比如说)我正在监听服务器,与回复的时间无关。但是Jacks代码几乎正确,我会在一分钟内发布完整的解决方案,谢谢。为什么不编写一个无限循环,但使用
Async.RunSynchronously
指定5秒的超时时间?@Jon Harrop-因为
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 ()
    }
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"