使TcpListener异步处理连接的正确方法是什么?

Shr*_*roy 3 f# asynchronous tcplistener async-await

对不起,很长的帖子.我想TcpListener用来监听端口,处理不同(后台)线程中传入连接所请求的繁重工作,然后在准备就绪时将响应发送回客户端.我在MSDN上阅读了很多代码和示例,并为服务器提出了以下实现.

对于以下所有实现,请假设以下变量:

let sva = "127.0.0.1"
let dspt = 32000

let respondToQuery (ns_ : NetworkStream) (bta_ : byte array) : unit =
    // DO HEAVY LIFTING
    ()
Run Code Online (Sandbox Code Playgroud)

IMPLEMENTATION 1(普通的同步服务器;我从这个MSDN页面翻译代码)

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"
Run Code Online (Sandbox Code Playgroud)

实施2(我在MSDN页面上的代码翻译)

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"
Run Code Online (Sandbox Code Playgroud)

实现3(我的实现基于异步工作流的MSDN页面)

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"
Run Code Online (Sandbox Code Playgroud)

现在,从我阅读的MSDN文档中,我原本以为这Implementation 3本来是最快的.但是当我从多台机器上查询多个查询时,它们都以大致相同的速度运行.这让我相信我一定做错了.

要么Implementation 2Implementation 3"正确"的方式来实现TcpListener,做了繁重的背景下,并返回它完成时给客户端的响应,同时允许其他客户或许还连接并开始在另一个后台线程另一项任务?如果没有,你能告诉我我应该阅读哪些课程(或教程)吗?

Tom*_*cek 6

主循环的正确结构应如下所示:

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 }
Run Code Online (Sandbox Code Playgroud)

需要注意的关键事项是:

  • 我将主循环包装在内部,async以便您可以异步使用等待客户端AcceptTcpClientAsync(不会阻塞)

  • respondToQuery函数返回一个在后台使用启动的异步计算Async.Start,这样处理可以继续并行等待下一个客户端(当Async.RunSynchronously你使用时会阻塞并等待直到respondToQuery完成)

  • 为了使其完全异步,内部代码也respondToQuery需要使用流的异步操作 - 查找AsyncReadAsyncWrite.

您也可以使用Async.StartChild,在这种情况下,子计算(body respondToQuery)获取与父项相同的取消令牌,因此当您取消主异步工作流时,它也将取消所有子项:

  while true do 
    let! client = t.AcceptTcpClientAsync() |> Async.AwaitTask
    do! respondToQuery client |> Async.StartChild |> Async.Ignore }
Run Code Online (Sandbox Code Playgroud)

Async.StartChild方法返回一个异步计算(使用let!或启动do!),我们需要忽略它返回的标记(可以用来等待子进程完成).