Shredderroy
Shredderroy

Reputation: 2920

What is the correct way to make a TcpListener handle connections asynchronously?

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

Answers (1)

Tomas Petricek
Tomas Petricek

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

Related Questions