Reputation: 43
Using TcpClient to push data across the network. Client only sends data. At some point, the server may close the socket. After the server closes the socket, the first client send completes without an exception. A client second send throws an IOException. I would have expected the first client send to return an IOException, because the socket on the far side is closed.
Is there an explanation for why things behave this way?
I would have expected the first send to the closed socket to throw an exception, because it didn't reach the socket on the far side.
Sample code to reproduce this behavior:
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;
}
I also wireharked the app running.
I would have expected the RST,ACK returned from the second packet to generate an exception in the application, because the socket on the far side is closed, so the packet clearly did not get to its recipient.
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```
Upvotes: 4
Views: 1081
Reputation: 70671
Note in your Wireshark log that the RST does not arrive until two seconds after the FIN. The FIN represents the remote endpoint's closure of the connection. The RST represents the response that's a consequence of the attempt to write to the connection. This response doesn't occur until the first attempt to write to the connection after it's been closed.
See e.g. this answer for one example of discussion of this behavior for TCP.
When you call WriteAsync()
, all that will happen is the bytes are sent out. It is the transmission of the bytes that represents the successful completion of the operation. TCP does not provide confirmation that the bytes were received. Importantly, the successful completion is recognized before the RST response is received (indeed, often even before it's sent, though this is a matter of timing and not relevant to this discussion).
The send operation provides an opportunity for the network layer to receive notification of the broken connection (i.e. the RST), but arriving after the send operation has been completed successfully. Thus on any subsequent send operation, the API now is aware of the broken connection and can complete with an error so that your application code can detect the problem.
In other words, yes…the behavior you're seeing is normal. This is why every application-level protocol must include graceful closure semantics, both endpoints must be prepared for a failure from any socket operation, and one must make no assumptions about whether data sent was actually received. The only way to know for sure what the remote endpoint has received is to ask it, and to have it reply.
Specifically, note that while you may not expect the remote endpoint to send any actual data, you still should read from the socket, so that you can detect graceful closure (i.e. the read operation completion with a 0-byte length).
Had you done so in your code example above, you would have seen that the closure is reported immediately when the remote endpoint has closed the socket. In this way, your client code would have been able to receive more timely notification of the closure. It wouldn't be definitive notice of an error condition, since of course an endpoint can shutdown a socket with the "send" reason, while still receiving data. But in your scenario it would provide important information, given your additional knowledge of the way the application protocol actually is designed.
Upvotes: 1