【问题标题】:What is the correct way to make a TcpListener handle connections asynchronously?使 TcpListener 异步处理连接的正确方法是什么?
【发布时间】:2013-02-22 17:12:43
【问题描述】:

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

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

let sva = "127.0.0.1"
let dspt = 32000

let respondToQuery (ns_ : NetworkStream) (bta_ : byte array) : unit =
    // DO HEAVY LIFTING
    ()

实现 1(普通的同步服务器;我翻译的 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"

实现2(我对代码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"

实现3(我的实现基于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"

现在,根据我对 MSDN 文档的阅读,我认为Implementation 3 会是最快的。但是当我用来自多台机器的多个查询访问服务器时,它们都以大致相同的速度运行。这让我相信我一定做错了什么。

Implementation 2Implementation 3 是实现 TcpListener 的“正确”方式,它在后台执行繁重的工作,并在完成后将响应返回给客户端,同时允许另一个客户端可能还连接并在另一个后台线程中启动另一个任务?如果没有,请告诉我应该阅读哪些课程(或教程)?

【问题讨论】:

    标签: asynchronous f# async-await tcplistener


    【解决方案1】:

    主循环的正确结构应该是这样的:

    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 }
    

    需要注意的关键点是:

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

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

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

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

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

    Async.StartChild 方法返回一个异步计算(使用let!do! 开始),我们需要忽略它返回的令牌(可用于等待子进程完成)。

    【讨论】:

    • 你能用use client = client代替try..finally吗?
    猜你喜欢
    • 1970-01-01
    • 2012-01-18
    • 2014-03-16
    • 2013-08-18
    • 2011-04-01
    • 1970-01-01
    • 2013-08-31
    • 1970-01-01
    • 2019-11-09
    相关资源
    最近更新 更多