Reputation: 2920
Sorry for the long post. I would like to use TcpListener
to listen on a port, handle the heavy lifting requested by an incoming connection in a different (background) thread, and then send the response back to the client when it is ready. I have read a lot of the code and samples on MSDN, and come up with the following implementations for the server.
For all of the implementations below, please assume the following variables:
let sva = "127.0.0.1"
let dspt = 32000
let respondToQuery (ns_ : NetworkStream) (bta_ : byte array) : unit =
// DO HEAVY LIFTING
()
IMPLEMENTATION 1 (plain, synchronous server; my translation of the code from this MSDN page)
let runSync () : unit =
printfn "Entering runSync ()"
let (laddr : IPAddress) = IPAddress.Parse sva
let (svr : TcpListener) = new TcpListener (laddr, dspt)
try
svr.Start ()
let (bta : byte array) = Array.zeroCreate<byte> imbs
while true do
printfn "Listening on port %d at %s" dspt sva
let (cl : TcpClient) = svr.AcceptTcpClient ()
let (ns : NetworkStream) = cl.GetStream ()
respondToQuery ns bta
cl.Close ()
svr.Stop ()
printfn "Exiting runSync () normally"
with
| excp ->
printfn "Error: %s" excp.Message
printfn "Exiting runSync () with error"
IMPLEMENTATION 2 (my translation of the code on this MSDN page)
let runAsyncBE () : unit =
printfn "Entering runAsyncBE ()"
let (tcc : ManualResetEvent) = new ManualResetEvent (false)
let (bta : byte array) = Array.zeroCreate<byte> imbs
let datcc (ar2_ : IAsyncResult) : unit =
let tcpl2 = ar2_.AsyncState :?> TcpListener
let tcpc2 = tcpl2.EndAcceptTcpClient ar2_
let (ns2 : NetworkStream) = tcpc2.GetStream ()
respondToQuery ns2 bta
tcpc2.Close ()
tcc.Set () |> ignore
let rec dbatc (tcpl2_ : TcpListener) : unit =
tcc.Reset () |> ignore
printfn "Listening on port %d at %s" dspt sva
tcpl2_.BeginAcceptTcpClient (new AsyncCallback (datcc), tcpl2_) |> ignore
tcc.WaitOne () |> ignore
dbatc tcpl2_
let (laddr : IPAddress) = IPAddress.Parse sva
let (tcpl : TcpListener) = new TcpListener (laddr, dspt)
try
tcpl.Start ()
dbatc tcpl
printfn "Exiting try block"
printfn "Exiting runAsyncBE () normally"
with
| excp ->
printfn "Error: %s" excp.Message
printfn "Exiting runAsyncBE () with error"
IMPLEMENTATION 3 (my implementation based on the MSDN page for asynchronous workflows)
let runAsyncA () : unit =
printfn "Entering runAsyncA ()"
let (laddr : IPAddress) = IPAddress.Parse sva
let (svr : TcpListener) = new TcpListener (laddr, dspt)
try
svr.Start ()
let (bta : byte array) = Array.zeroCreate<byte> imbs
while true do
printfn "Listening on port %d at %s" dspt sva
let (cl : TcpClient) = svr.AcceptTcpClient ()
let (ns : NetworkStream) = cl.GetStream ()
async {respondToQuery ns bta} |> Async.RunSynchronously
cl.Close ()
svr.Stop ()
printfn "Exiting runAsyncA () normally"
with
| excp ->
printfn "Error: %s" excp.Message
printfn "Exiting runAsyncA () with error"
Now, from my reading of the MSDN documentation, I would have thought that Implementation 3
would have been the fastest. But when I hit the server with multiple queries from multiple machines, they all operate at about the same speed. This leads me to believe that I must be doing something wrong.
Is either Implementation 2
or Implementation 3
the "correct" way to implement a TcpListener
that does its heavy lifting in the background, and returns the response to the client when it is finished, while allowing another client to perhaps also connect and start another task in another background thread? If not, could you please tell me which classes (or tutorials) I should read up?
Upvotes: 3
Views: 473
Reputation: 243126
The right structure for the main loop should look like this:
let respondToQuery (client:TcpClient) = async {
try
let stream = client.GetStream()
() // TODO: The actual processing goes here!
finally
client.Close() }
async {
while true do
let! client = t.AcceptTcpClientAsync() |> Async.AwaitTask
respondToQuery client |> Async.Start }
The key things to note are:
I wrapped the main loop inside async
so that you can wait for clients asynchronously using AcceptTcpClientAsync
(without blocking there)
The respondToQuery
function returns an asynchronous computation that is started in background using Async.Start
, so that the processing can continue in parallel with waiting for the next client (when using Async.RunSynchronously
you would block and wait until respondToQuery
completes)
To make this fully asynchronous, the code inside respondToQuery
needs to use asynchronous operations of the stream too - look for AsyncRead
and AsyncWrite
.
You could also use Async.StartChild
, in which case the child computation (body of respondToQuery
) gets the same cancellation token as the parent and so when you cancel the main asynchronous workflow, it will cancel all children too:
while true do
let! client = t.AcceptTcpClientAsync() |> Async.AwaitTask
do! respondToQuery client |> Async.StartChild |> Async.Ignore }
The Async.StartChild
method returns an asynchronous computation (to be started using let!
or do!
) and we need to ignore the token that it returns (can be used to wait until the child completes).
Upvotes: 6