【问题标题】:.NET Socket ReadAsync blocked during write loop Async / Await.NET Socket ReadAsync 在写循环 Async / Await 期间被阻塞
【发布时间】:2019-11-26 01:11:04
【问题描述】:

我正在编写一个使用 Async / Await 的 TCP 服务器,它需要根据从每个客户端接收到的信息向连接的客户端发送消息列表。 在发送给客户端的每条消息之间,我需要:

  1. 等待确认/响应,然后发送下一条消息
  2. 如果 5 秒后没有确认,则重新发送命令

为此,我在我的 ConnClient 类上设置一个ResponseReceived 属性,当预期的响应到来时。然后,在ConnClient.SendListAsync 例程中,我检查发送每个命令后该属性是否已更改.但是,在SendListAsync 发送所有消息之前,不会读取传入的响应,如下面的调试语句所示:

Sending Initial Message.
Received response, generate list of 3 initial commands and send them.
SendListAsync 5 second timeout w/o response.
SendListAsync 5 second timeout w/o response.
SendListAsync 5 second timeout w/o response.
Received response.
Received response.
Received response.

问题:如何正确防止ConnClient.SendListAsync 阻止传入的读取?

public class Svr
{
    TcpListener listener;
    public async Task Listen(IPAddress iP, int port)
    {
        listener = new TcpListener(iP, port);
        listener.Start();
        while (true)
        {
            TcpClient client = await listener.AcceptTcpClientAsync();
            ConnClient cc = new ConnClient(client);
            await Receive(ConnClient);
        }
    }

    async Task Receive(ConnClient cc)
    {
        var headerSize = sizeof(short);
        byte[] buffer = new byte[4000];

        //Send initial msg
        await cc.socket.GetStream().WriteAsync(Strings.InitialMsg, 0, Strings.InitialMsg.Length); 

        while (true)
        {
            buffer = new byte[headerSize];
            if (!await ReadToBuffer(cc.socket.GetStream(), buffer, headerSize))
                return;

            var length = BitConverter.ToUInt16(new byte[2] { buffer[1], buffer[0] }, 0 );
            buffer = new byte[length];

            if (!await ReadToBuffer(cc.socket.GetStream(), buffer, length))
                return;

            await DoSomethingBasedOnReceived(messageBuffer, cc);
        }
    }

    async Task<Boolean> ReadToBuffer(NetworkStream stream, byte[] buffer, int bytesToRead)
    {
        int offset = 0;
        while (offset < bytesToRead)
        {
            var length = await stream.ReadAsync(buffer, offset, bytesToRead - offset);
            if (length == 0)
                return false;
            offset += length;
        }
        return true;
    }

    public async Task DoSomethingBasedOnReceived(byte[]  messageBuffer, ConnClient cc)
    {
        await SomeLogicToSetTheRRFlagIfMessageApplicable(messageBuffer, cc);
        List<byte[]> ListOfMessagesToSend = SomeLogicToDetermineListOfMessages(messageBuffer);
        await cc.SendListAsync(ListOfMessagesToSend);
    }
}

ConnClient 类,代表单个连接的客户端。

public class ConnClient
{
    public TcpClient socket { get; set; }
    public Boolean ResponseReceived { get; set; }
    public ConnClient (TcpClient cc)
    {socket = cc}

    public async Task SendListAsync(List<byte[]> messageList)
    {
        foreach (byte[] msg in messageList)
        {
            this.ResponseReceived = false;
            await stream.WriteAsync(msg, 0, msg.Length);

            int waitedSoFar = 0;
            while (waitedSoFar < 5000)
            {
                if (this.ResponseReceived == true)
                {
                    break;
                }
                waitedSoFar += 100;
                await Task.Delay(100);
            }
        }
    }
}

【问题讨论】:

  • “我需要:……如果 5 秒后没有确认,则重新发送命令”——这是 TCP 的可笑要求——基于实施。 TCP 已经在协议中实现了数据确认。您不应该尝试在此之上添加您自己的,并且您当然不应该在无法返回 ack 时重新发送数据。如果远程端点获得了重新发送的数据,它现在将不得不处理相同数据的两个副本
  • 彼得,如果在指定的时间范围内未收到确认,您是否建议最好的方法是简单地关闭与客户端的连接?
  • “你是否建议最好的方法是简单地关闭连接” - 实际上,不。我建议最好的方法是什么都不做。响应延迟的原因有很多,其中一些可以恢复,有些则不能。在可恢复的场景中,只要您什么都不做,TCP 就会为您恢复。在不可恢复的场景中,只要你什么都不做,你的连接最终会自然而然的异常失败。然后,只有到那时,你才会关闭套接字。一旦异常发生,你就知道socket已经没有用了。

标签: c# sockets asynchronous async-await


【解决方案1】:

您的第一个问题是您将无法接受新客户。

while (true)
{
        // accept the next connection
        TcpClient client = await listener.AcceptTcpClientAsync();

        // receive and send list
        ConnClient cc = new ConnClient(client);
        await Receive(ConnClient);

        // the loop cannot continue to receive the next connection 
        // until you have done with your receive
}

您将需要独立执行Receive,以便等待下一个连接,您可以在没有await 的情况下调用它(作为 async void),或将其卸载到新任务。

移除等待

Receive(ConnClient);

卸载

Task.Run(() => Receive(ConnClient));

您的第二个问题是您的客户端在发送时被阻止并且无法接收。您将再次卸载,或者在没有await 的情况下运行。

正如@PeterDuniho 提到的那样

鉴于 OP 已经在使用async/await,并且鉴于 Receive() 已经是异步,没有理由使用Task.Run()。它是 一劳永逸(除非他们更改代码以存储 返回task),所以他们不妨fire-and-forget调用 Receive() 将其包装在对 Task.Run() 的调用中。

注意:创建可扩展的客户端/服务器套接字解决方案并非易事,我并不想展示这一点。但是,它会解决您当前的问题。

无论哪种方式,都要非常注意错误。由于两种提议的解决方案都将在未观察到的情况下运行,因此需要处理异常

【讨论】:

  • 将军,我明白你关于Listen 例程的观点。我确实认为这是我需要纠正的问题。但是,我仍然在单个连接中收到超时通知。您是否建议我在没有等待的情况下另外致电ConnClient.SendListAsync
  • @AliceSmith 是的,您需要独立运行它们。只要确保你捕捉到异常。
猜你喜欢
  • 2019-01-06
  • 2018-07-24
  • 1970-01-01
  • 1970-01-01
  • 2018-05-16
  • 1970-01-01
  • 1970-01-01
  • 2017-11-08
  • 1970-01-01
相关资源
最近更新 更多