Reputation: 63816
I am using NetworkStream
with TcpClient
.
First I setup my tcp client:
tcp = new TcpClient(AddressFamily.InterNetwork)
{ NoDelay = true, ReceiveTimeout = 5000};
My main data-receiving loop:
while (true)
{
//read available data from the device
int numBytesRead = await ReadAsync();
Console.WriteLine($"{numBytesRead} bytes read"); //BP2
}
And the actual TCP data reading:
public Task<int> ReadAsync()
{
var stream = tcp.GetStream();
return stream.ReadAsync(InBuffer, 0, InBuffer.Length); //BP1
}
I have this connected to a testbed which lets me send manual packets. Through setting breakpoints and debugging I have checked that stream.ReadTimeout
takes the value 5000 from tcp
.
If I send data frequently it all works as expected. But if I don't send any data, nothing appears to happen after 5s, no timeout. I see breakpoint BP1
being hit in the debugger but until I send data from my testbed, BP2
is not hit. I can leave it a minute or more and it just seems to sit waiting, but receives data sent after a minute, which appears to be incorrect behavior. After 5 seconds something should happen, surely (an exception as I understand it)?
It's late so I am expecting something really basic but can anyone see what my mistake is and a resolution?
OK so when I RTFM for the actual .Net version I'm using (how may times have I been caught out by MS defaulting to .Net Core 3, I did say it was late) I see in the remarks sectio for ReadTimeout
:
This property affects only synchronous reads performed by calling the Read method. This property does not affect asynchronous reads performed by calling the BeginRead method.
I'm unclear now if I can use modern awaitable calls at all to read socket data safely and with a timeout specifically. It's working except for the timeout but I'm not sure how given ReadAsync
has no override in NetworkStream
. Must I do some ugly hack or is there a simple solution?
In my case 5000 is the longest I can expect not to receive data before concluding there is a problem - the protocol has no ping mechanism so if nothing appears I assume the connection is dead. Hence thinking an Async read with a 5000ms timeout would be nice and neat.
Upvotes: 6
Views: 4452
Reputation: 70701
Timeout values for network objects apply only to synchronous operations. For example, from the documentation:
This option applies to synchronous Receive calls only.
For Socket.ReceiveTimeout
, TcpClient.ReceiveTimeout
, and NetworkStream.ReadTimeout
, the implementations all ultimately result in a call to SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, ...)
which in turn is effectively calling the native setsockopt()
function. From that documentation:
SO_RCVTIMEO
DWORD
Sets the timeout, in milliseconds, for blocking receive calls.
(emphasis mine)
It's this limitation in the underlying native API that is the reason for the same limitation in the managed API. Timeout values will not apply to asynchronous I/O on the network objects.
You will need to implement the timeout yourself, by closing the socket if and when the timeout should occur. For example:
async Task<int> ReadAsync(TcpClient client, byte[] buffer, int index, int length, TimeSpan timeout)
{
Task<int> result = client.GetStream().ReadAsync(buffer, index, length);
await Task.WhenAny(result, Task.Delay(timeout));
if (!result.IsCompleted)
{
client.Close();
}
return await result;
}
Other variations on this theme can be found in other related questions:
NetworkStream.ReadAsync with a cancellation token never cancels
Cancel C# 4.5 TcpClient ReadAsync by timeout
Closing the socket is really all that you can do. Even for synchronous operations, if a timeout occurs the socket would no longer be usable. There is no reliable way to interrupt a read operation and expect the socket to remain consistent.
Of course, you do have the option of prompting the user before closing the socket. However, if you were to do that, you would implement the timeout at a higher level in your application architecture, such that the I/O operations themselves have no awareness of timeouts at all.
Upvotes: 13