C# TcpClient WriteAsyn c в закрытом сокете не генерирует исключение - PullRequest
3 голосов
/ 08 июля 2020

Использование TcpClient для передачи sh данных по сети. Клиент только отправляет данные. В какой-то момент сервер может закрыть сокет. После того, как сервер закрывает сокет, первая отправка клиента завершается без исключения. Вторая отправка клиента вызывает исключение IOException. Я ожидал, что первый отправленный клиент вернет исключение IOException, потому что сокет на дальней стороне закрыт.

Есть ли объяснение того, почему все так себя ведет?

первая отправка в закрытый сокет, чтобы вызвать исключение, потому что оно не достигло сокета на дальней стороне.

Пример кода для воспроизведения этого поведения:

        const string _address = "127.0.0.1";
        const int _port = 5500;
        const int _payloadSize = 100;
        static async Task RunTestAsync()
        {
            IPAddress.TryParse(_address, out IPAddress ip);
            Task serverTask = Task.Run(async () =>
            {
                Console.WriteLine("Server listening");
                IPEndPoint ipLocal = new IPEndPoint(ip, _port);
                TcpListener listener = new TcpListener(ipLocal);
                listener.Start();
                using (TcpClient serverSocket = await listener.AcceptTcpClientAsync())
                {
                    Console.WriteLine("Server accepted connection");
                    byte[] serverbytes = new byte[_payloadSize];
                    using (NetworkStream serverStream = serverSocket.GetStream())
                    {
                        int bytesRead = await serverStream.ReadAsync(serverbytes, 0, serverbytes.Length);
                        Console.WriteLine("Server received {0} bytes", bytesRead);
                    }
                }
                Console.WriteLine("Socket closed from server (CLOSE_WAIT until client closes)");
                listener.Stop();
                Console.WriteLine("Listener stopped");

            });

            using (TcpClient clientSocket = new TcpClient())
            {
                await clientSocket.ConnectAsync(ip, _port);
                Console.WriteLine("Client connected");
                byte[] clientbytes = new byte[_payloadSize];
                using (NetworkStream clientStream = clientSocket.GetStream())
                {
                    await clientStream.WriteAsync(clientbytes, 0, clientbytes.Length);
                    Console.WriteLine("Client transmitted bytes");
                    await Task.Delay(2000);
                    Console.WriteLine("Client delay for server to close socket");
                    await clientStream.WriteAsync(clientbytes, 0, clientbytes.Length);
                    Console.WriteLine("Client transmitted bytes on closed socket (FIN_WAIT_2) with no error");
                    await clientStream.WriteAsync(clientbytes, 0, clientbytes.Length);
                    Console.WriteLine("Client never transmitted these bytes.  Got exception instead");
                }
            }
            await serverTask;
        }

Я также проверил приложение работает. Я ожидал, что RST, ACK, возвращенный из второго пакета, сгенерирует исключение в приложении, потому что сокет на дальней стороне закрыт, поэтому пакет явно не дошел до своего получателя.

1   10:14:25.980424 127.0.0.1   127.0.0.1   TCP 66  50131 → 5500 [SYN] Seq=0 Win=65535 Len=0 MSS=65495 WS=256 SACK_PERM=1
2   10:14:25.980464 127.0.0.1   127.0.0.1   TCP 66  5500 → 50131 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=65495 WS=256 SACK_PERM=1
3   10:14:25.980537 127.0.0.1   127.0.0.1   TCP 54  50131 → 5500 [ACK] Seq=1 Ack=1 Win=2619648 Len=0
4   10:14:25.982960 127.0.0.1   127.0.0.1   VNC 154 
5   10:14:25.982976 127.0.0.1   127.0.0.1   TCP 54  5500 → 50131 [ACK] Seq=1 Ack=101 Win=2619648 Len=0
6   10:14:25.984495 127.0.0.1   127.0.0.1   TCP 54  5500 → 50131 [FIN, ACK] Seq=1 Ack=101 Win=2619648 Len=0
7   10:14:25.984511 127.0.0.1   127.0.0.1   TCP 54  50131 → 5500 [ACK] Seq=101 Ack=2 Win=2619648 Len=0
8   10:14:27.986372 127.0.0.1   127.0.0.1   VNC 154 
9   10:14:27.986583 127.0.0.1   127.0.0.1   TCP 54  5500 → 50131 [RST, ACK] Seq=2 Ack=201 Win=0 Len=0```

1 Ответ

1 голос
/ 08 июля 2020

Обратите внимание в журнале Wireshark, что RST не приходит раньше, чем через две секунды после FIN. FIN представляет собой закрытие соединения удаленной конечной точкой. RST представляет собой ответ, который является следствием попытки записи в соединение. Этот ответ не возникает до первой попытки записи в соединение после его закрытия.

См., Например, этот ответ для одного примера обсуждения этого поведения для TCP.

Когда вы вызываете WriteAsync(), все, что происходит, - это отправка байтов. Это передача байтов, которая представляет успешное завершение операции. TCP не предоставляет подтверждения, что байты были получены . Важно отметить, что успешное завершение распознается до ответа RST (действительно, часто даже до его отправки, хотя это вопрос времени и не имеет отношения к этому обсуждению).

Операция отправки предоставляет возможность сетевому уровню получать уведомление о разорванном соединении (то есть RST), но прибывает после того, как операция отправки была успешно завершена. Таким образом, при любой последующей операции отправки API теперь знает о разорванном соединении и может завершиться с ошибкой, чтобы код вашего приложения мог обнаружить проблему.

Другими словами, да ... поведение, которое вы видите это нормально. Вот почему каждый протокол уровня приложения должен включать семантику постепенного закрытия, обе конечные точки должны быть готовы к отказу от любой операции с сокетом, и нельзя делать никаких предположений о том, были ли отправленные данные действительно получены. Единственный способ узнать наверняка, что удаленная конечная точка получила, - это спросить ее и получить ответ.

В частности, обратите внимание, что хотя вы можете не ожидать, что удаленная конечная точка отправит какие-либо фактические данные, вы все равно должен читать из сокета, чтобы вы могли обнаружить изящное закрытие (т.е. завершение операции чтения с длиной 0 байт).

Если бы вы сделали это в приведенном выше примере кода, вы бы увидели, что закрытие сообщается немедленно, когда удаленная конечная точка закрыла сокет. Таким образом, ваш клиентский код мог бы получать более своевременное уведомление о закрытии. Это не будет окончательным уведомлением о состоянии error , поскольку, конечно, конечная точка может выключить сокет с причиной «отправки», продолжая получать данные. Но в вашем сценарии он предоставит важную информацию, учитывая ваши дополнительные знания о том, как на самом деле разработан протокол приложения.

...