使用 TcpListener 取消 NetworkStream.ReadAsync

时间:2023-02-04
本文介绍了使用 TcpListener 取消 NetworkStream.ReadAsync的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

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

void Main(){去();线程睡眠(100000);}异步无效 Go(){TcpListener 监听器 = new TcpListener(IPAddress.Any, 6666);尝试{cts.Token.Register(() => Console.WriteLine("令牌被取消"));listener.Start();using(TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false))使用(var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))){var stream=client.GetStream();var 缓冲区=新字节[64];尝试{var amtRead = await stream.ReadAsync(buffer,0,缓冲区.长度,cts.Token);Console.WriteLine("完成");}捕获(任务取消异常){Console.WriteLine("boom");}}}最后{listener.Stop();}}

如果我将 telnet 客户端连接到 localhost:6666 并坐下来 5 秒钟什么都不做,为什么我会看到令牌已取消"但从未看到boom"(或finished")?

这个 NetworkStream 不会尊重取消吗?

我可以结合使用 Task.Delay()Task.WhenAny 来解决这个问题,但我更愿意让它按预期工作.>

相反,以下取消示例:

async void Go(CancellationToken ct){使用(var cts=new CancellationTokenSource(TimeSpan.FromSeconds(5))){尝试{等待 Task.Delay(TimeSpan.FromSeconds(10),cts.Token).ConfigureAwait(false);}捕获(任务取消异常){Console.WriteLine("boom");}}}

按预期打印boom".怎么回事?

解决方案

不,NetworkStream 不支持取消.

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

Stream 通过假装"支持取消(以及异步 I/O)使这个问题复杂化,即使实现不支持它.对取消的假"支持只是立即检查令牌,然后开始无法取消的常规异步读取.这就是您在 NetworkStream 中看到的情况.

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

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

另一方面,如果您想检测半开场景(您的一侧正在读取但另一侧已断开连接),那么最好的解决方案是定期发送数据.我在在我的博客上详细描述了这一点.

Consider the following simplified example (ready to roll in LinqPad, elevated account required):

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();
    }
}

If I connect a telnet client to localhost:6666 and sit around doing nothing for 5 seconds, why do I see "Token was canceled" but never see "boom" (or "finished")?

Will this NetworkStream not respect cancellation?

I can work around this with a combination of Task.Delay() and Task.WhenAny, but I'd prefer to get it working as expected.

Conversely, the following example of cancellation:

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");
        }
    }
}

Prints "boom", as expected. What's going on?

解决方案

No, NetworkStream does not support cancellation.

Unfortunately, the underlying Win32 APIs do not always support per-operation cancellation. Traditionally, you could cancel all I/O for a particular handle, but the method to cancel a single I/O operation is fairly recent. Most of the .NET BCL was written against the XP API (or older), which did not include CancelIoEx.

Stream compounds this issue by "faking" support for cancellation (and asynchronous I/O, too) even if the implementation doesn't support it. The "fake" support for cancellation just checks the token immediately and then starts a regular asynchronous read that cannot be cancelled. That's what you're seeing happen with NetworkStream.

With sockets (and most Win32 types), the traditional approach is to close the handle if you want to abort communications. This causes all current operations (both reads and writes) to fail. Technically this is a violation of BCL thread safety as documented, but it does work.

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

If, on the other hand, you want to detect a half-open scenario (where your side is reading but the other side has lost its connection), then the best solution is to periodically send data. I describe this more on my blog.

这篇关于使用 TcpListener 取消 NetworkStream.ReadAsync的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:处理后调用 MemoryStream.ToArray() 是否危险? 下一篇:一个流中的多个文件,自定义流

相关文章

最新文章