I'm writing a control application for the Logitech Media Server (formerly known as Squeezebox Server).
A small part of that is discovering which servers are running on the local network. This is done by broadcasting a special UDP package to port 3483 and waiting for replies. If no server replies after a given time, (or a preferred server replies) the application should stop listening.
I have it working in C#, using the async/await features of C# 5, but I was curious to see how it would look in F#. I have the following function (more or less directly translated from 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
}
The discoveryPacket
is a byte array containing the data to broadcast. getServerName
is a function defined elsewhere, that extracts the human-readable server name from the server reply data.
So the application calls broadCast
with two arguments, a timeout and a callback function that will be called when a server replies. This callback function can then decide to end listening or not, by return true or false. If no server replies, or no callback returns true, the function returns after the timeout expires.
This code works just fine, but I am vaguely bothered by the use of the imperative ref cell finished
.
So here's the question: is there an idiomatic F#-y way to do this kind of thing without turning to the imperative dark side?
Update
Based on the accepted answer below (which was very nearly right), this is the full test program I ended up with:
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
You can implement this "functionally" with a recursive function which also produces an Async<'T> value (in this case, Async). This code should work -- it's based on the code you provided -- though I couldn't test it as it depends on other parts of your code.
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 ()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With