【问题标题】:Right approach for asynchronous TcpListener using async/await使用 async/await 的异步 TcpListener 的正确方法
【发布时间】:2014-03-16 20:55:44
【问题描述】:

我一直在思考什么是使用异步编程设置TCP服务器的正确方法。

通常我会为每个传入请求生成一个线程,但我想充分利用 ThreadPool,所以当连接空闲时,没有阻塞线程。

首先,我将创建侦听器并开始接受客户端,在本例中,是在控制台应用程序中:

static void Main(string[] args)
{
    CancellationTokenSource cancellation = new CancellationTokenSource();
    var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8001);
    TcpListener server = new TcpListener(endpoint); 

    server.Start();
    var task = AcceptTcpClients(server, cancellation.Token);

    Console.ReadKey(true);
    cancellation.Cancel();
    await task;
    Console.ReadKey(true);
}

在该方法中,我会循环接受传入的请求并生成一个新任务来处理连接,因此循环可以返回以接受更多客户端:

static async Task AcceptTcpClients(TcpListener server, CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        var ws = await server.AcceptTcpClientAsync();

        Task.Factory.StartNew(async () =>
        {
            while (ws.IsConnected && !token.IsCancellationRequested)
            {
                String msg = await ws.ReadAsync();
                if (msg != null)
                    await ws.WriteAsync(ProcessResponse(msg));
            }
        }, token);
    }
 }

创建新任务并不一定意味着新线程,但这是正确的方法吗?我是在利用 ThreadPool 还是我能做些什么?

这种方法有什么潜在的缺陷吗?

【问题讨论】:

    标签: c# asynchronous threadpool async-await tcplistener


    【解决方案1】:

    Main 中的 await task; 无法编译;如果你想阻止它,你必须使用task.Wait();

    此外,您应该在异步编程中使用Task.Run 而不是Task.Factory.StartNew

    创建新Task并不一定意味着新线程,但这是否正确?

    您当然可以启动单独的任务(使用Task.Run)。虽然你没有。您可以轻松调用单独的 async 方法来处理各个套接字连接。

    不过,您的实际套接字处理存在一些问题。 Connected 属性实际上是无用的。您应该始终不断地从连接的套接字读取,即使您正在写入它。此外,您应该编写“keepalive”消息或读取超时,以便您可以检测到半开的情况。我维护了一个 TCP/IP .NET FAQ 来解释这些常见问题。

    我真的强烈建议人们不要编写 TCP/IP 服务器或客户端。有的陷阱。如果可能的话,自托管 WebAPI 和/或 SignalR 会好得多。

    【讨论】:

    • 我正要写一个答案,但这涵盖了每一点,甚至更多。有人应该一劳永逸地写出常见的陷阱,因为您经常给出这个答案。您现在可以拼凑文本 sn-ps 来回答所有 TCP 问题。
    • 嗯,这正是学习陷阱的重点:)。我正在编写一个 WebSocketListener 类,现在我正在尝试完善它。我会看看你的指南,谢谢!
    • 我不明白“Task.Run”这个东西。为什么“Task.Factory.StartNew”不好?
    • @vtortola:并不是StartNew 是邪恶的;只是Run 更好。我完全解释了on my blog
    • @StephenCleary 问题 - 我认为 - webAPI 或 SignalR 是它们都是相当“垂直”的解决方案 - 例如SignalR 似乎执行 TCP + 持久连接 + 将事件推送到 javascript 客户端。是否有可以构建在其之上的“TCP Server”层?
    【解决方案2】:

    为了优雅地停止服务器接受循环,我注册了一个回调,该回调在取消取消标记时停止侦听 (cancellationToken.Register(listener.Stop);)。

    这将在await listener.AcceptTcpClientAsync(); 上抛出一个易于捕获的 SocketException。

    不需要Task.Run(HandleClient()),因为调用异步方法会返回一个并行运行的任务。

        public async Task Run(CancellationToken cancellationToken)
        {
            TcpListener listener = new TcpListener(address, port);
            listener.Start();
            cancellationToken.Register(listener.Stop);
            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    TcpClient client = await listener.AcceptTcpClientAsync();
                    var clientTask = protocol.HandleClient(client, cancellationToken)
                        .ContinueWith(antecedent => client.Dispose())
                        .ContinueWith(antecedent => logger.LogInformation("Client disposed."));
                }
                catch (SocketException) when (cancellationToken.IsCancellationRequested)
                {
                    logger.LogInformation("TcpListener stopped listening because cancellation was requested.");
                }
                catch (Exception ex)
                {
                    logger.LogError(new EventId(), ex, $"Error handling client: {ex.Message}");
                }
            }
        }
    

    【讨论】:

    • protocolprotocol.HandleClient(client, cancellationToken)中是什么类型的对象
    • 这是一个在public async Task HandleClient(TcpClient client, CancellationToken cancellationToken)中处理通过TcpClient流的协议特定通信的类。
    • 在 .Net 5.0 中,抛出的异常类型已从 ObjectDisposedException 更改为 SocketException。我已经修改了代码。
    猜你喜欢
    • 2019-01-17
    • 2017-05-13
    • 2013-01-28
    • 2019-04-21
    • 2013-09-19
    • 2020-06-02
    • 1970-01-01
    • 2013-02-22
    • 2018-10-07
    相关资源
    最近更新 更多