使用TcpListener取消NetworkStream.ReadAsync

请考虑以下简化示例(准备在LinqPad中滚动,需要提升帐户):

void Main() { Go(); Thread.Sleep(100000); } async void Go() { TcpListener listener = new TcpListener(IPAddress.Any, 6666); try { cts.Token.Register(() => Console.WriteLine("Token was canceled")); listener.Start(); using(TcpClient client = await listener.AcceptTcpClientAsync() .ConfigureAwait(false)) using(var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { var stream=client.GetStream(); var buffer=new byte[64]; try { var amtRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); Console.WriteLine("finished"); } catch(TaskCanceledException) { Console.WriteLine("boom"); } } } finally { listener.Stop(); } } 

如果我将一个telnet客户端连接到localhost:6666并且在5秒钟内无所事事,为什么我会看到“令牌被取消”但从未看到“繁荣”(或“完成”)?

这个NetworkStream会不会取消取消吗?

我可以通过Task.Delay()Task.WhenAny的组合解决这个Task.WhenAny ,但我更愿意让它按预期工作。

相反,以下取消示例:

 async void Go(CancellationToken ct) { using(var cts=new CancellationTokenSource(TimeSpan.FromSeconds(5))) { try { await Task.Delay(TimeSpan.FromSeconds(10),cts.Token) .ConfigureAwait(false); } catch(TaskCanceledException) { Console.WriteLine("boom"); } } } 

正如预期的那样打印“热潮”。 这是怎么回事?

不, NetworkStream不支持取消。

不幸的是,底层的Win32 API并不总是支持按操作取消。 传统上,您可以取消特定句柄的所有 I / O,但取消单个 I / O操作的方法是最近的。 大多数.NET BCL是针对XP API(或更旧版本)编写的,它不包含CancelIoEx

Stream通过“伪造”支持取消(以及异步I / O)来解决此问题,即使实现不支持它也是如此。 取消的“假”支持只是立即检查令牌,然后启动无法取消的常规异步读取。 这就是您在NetworkStream看到的情况。

对于套接字(以及大多数Win32类型),传统方法是在要中止通信时关闭句柄。 这会导致所有当前操作(读取和写入)失败。 从技术上讲,这是违反了BCL线程安全的记录,但确实有效。

 cts.Token.Register(() => client.Close()); ... catch (ObjectDisposedException) 

另一方面,如果您想要检测半开场景(您的一方正在阅读但另一方已失去其连接),那么最佳解决方案是定期发送数据。 我在博客上更多地描述了这一点。