【问题标题】:How to cancel await with NetworkStream.WriteAsync? [duplicate]如何使用 NetworkStream.WriteAsync 取消等待? [复制]
【发布时间】:2023-08-09 04:49:01
【问题描述】:

我在玩TcpClient,当我使用一些异步操作时,它们会忽略 CancellationToken。经过一番阅读,我知道这是故意的,也知道存在一些取消异步操作等待的方法。

我刚刚阅读了下一个 * 问题和文章,阐明了一些要点:

How to cancel a Task in await?

https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations/

根据之前的文章,我可以取消NetworkStream.ReadAsync,但是当我在NetworkStream.WriteAsync 上使用它们时,该机制不起作用。

我有这个代码作为最小的例子:

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        CancellationTokenSource ctSource;
        private async void button1_Click(object sender, EventArgs e)
        {
            button1.Enabled = false;
            string http_resp = "";
            var request_uri = new Uri("http://icanhazip.com");

            //BIG FILE as postdata to test the WriteAsync (use one that you have on your disk)
            string contents = File.ReadAllText(@"C:\Portables\4test.txt");
            string post_data = contents;

            ctSource = new CancellationTokenSource();
            CancellationToken ct = ctSource.Token;

            Task<string> task = HttpRequestAsync(post_data, request_uri, ct);
            try
            {
                http_resp = await task;
            }
            catch
            {
                http_resp = "General error";
            }

            textBox1.Text = http_resp;

            button1.Enabled = true;
        }

        private static async Task<string> HttpRequestAsync(string post_data, Uri request_uri, CancellationToken ct)
        {
            string result = string.Empty;
            string http_method = "POST";
            string post_content_type = "application/x-www-form-urlencoded";

            var hostname = request_uri.Host;
            var port = request_uri.Port;
            var scheme = request_uri.Scheme;

            using (TcpClient tcpClient = new TcpClient())
            {
                tcpClient.SendTimeout = 15;
                tcpClient.ReceiveTimeout = 15;
                try
                {
                    await tcpClient.ConnectAsync(hostname, port);
                }
                catch (Exception d1)
                {
                    if (ct.IsCancellationRequested)
                    {
                        result = "Cancelation requested on ConnectAsync";
                    }
                    else
                    {
                        result = d1.Message + "\r\n" + d1.GetType().FullName + d1.StackTrace; ;
                    }
                    return result;
                }


                //Build HTTP headers
                string reqString = "";
                string header_host = "Host: " + hostname + "\r\n";
                string header_close = "Connection: Close\r\n";
                string basic_headers = "User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0\r\n";
                basic_headers += "Referer: https://www.google.com\r\n";

                string header_post = "";
                if (http_method == "POST")
                {
                    string header_content_type = "";
                    header_content_type = "Content-type: " + post_content_type + "\r\n";
                    int content_length = 0;
                    content_length = post_data.Length;
                    string header_content_length = "Content-length: " + content_length + "\r\n";
                    header_post = header_content_type + header_content_length;
                }

                reqString = http_method + " " + request_uri.PathAndQuery + " " + "HTTP/1.1" + "\r\n" + header_host + basic_headers + header_close + header_post + "\r\n";
                if (http_method == "POST")
                {
                    reqString += post_data;
                }
                var header_bytes = Encoding.ASCII.GetBytes(reqString.ToString());

                //Starting the I/O Network operations
                using (NetworkStream tcp_stream = tcpClient.GetStream())
                {
                    try
                    {
                        //HERE is where i have problems cancelling this await while WriteAsync is working.
                        await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct).WithCancellation(ct);
                        //await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct);
                    }
                    catch (Exception d2)
                    {
                        if (ct.IsCancellationRequested)
                        {
                            result = "Cancelation requested on WriteAsync";
                        }
                        else
                        {
                            result = d2.Message + "\r\n" + d2.GetType().FullName + d2.StackTrace;
                        }
                        return result;
                    }

                    using (var memory = new MemoryStream())
                    {
                        try
                        {
                            await tcp_stream.CopyToAsync(memory, 81920, ct);
                        }
                        catch (Exception d3)
                        {
                            if (ct.IsCancellationRequested)
                            {
                                result = "Request cancelled by user (on read)";
                            }
                            else
                            {
                                result = d3.Message + "\r\n" + d3.GetType().FullName + d3.StackTrace;
                            }
                            return result;
                        }
                        memory.Position = 0;
                        byte[] data = memory.ToArray();
                        result = Encoding.UTF8.GetString(data);
                    }
                }
            }
            return result;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            ctSource.Cancel();
        }
    }

当我在 ReadAsync 上使用它时效果很好:

await tcp_stream.ReadAsync(response, 0, response.Length, ct).WithCancellation(ct);

在 WriteAsync 上使用时它不起作用

await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct).WithCancellation(ct);

没有返回错误,只是没有取消等待。为了更清楚,我添加了一个最小示例作为 Visual Studio 2015 项目,您可以在此处下载:https://github.com/Zeokat/minimal_ex/archive/master.zip

它还包括一个文件4test.rar,您可以将其解压缩成一个39MB的文件4test.txt。我将此文本文件用作post_data 内容进行测试,因为在WriteAsync 运行时调用取消操作已经足够大了。

有人可以帮我解决这个问题吗?我花了几天时间试图解决这个问题,但无法找到合适的解决方案。

提前致谢。

【问题讨论】:

  • 您需要重载WithCancellation 方法,该方法接受并返回简单的Task(非泛型类型)。
  • 我很欣赏您的回答,但我不知道该怎么做。我已经尝试过,但无法实现正确的方法。如果你能用代码示例解释一下就好了。
  • Here 是此方法的一个更简单的实现,适用于 TaskTask&lt;Result&gt; 类型。它被命名为AsCancelable 而不是WithCancellation,但其他地方都是一样的。
  • 顺便说一句,这些方法在每次调用时至少分配两个对象,因此它们不适合在热路径中使用(即不要每秒调用它们 100,000 次)!
  • 感谢您的建议西奥多,我已经将它打包成一个任务,但没有成功。我将尝试构建一个控制台应用程序进行测试。

标签: c# asynchronous async-await cancellation


【解决方案1】:

不要使用 .WithCancellation(ct) 仅使用 await tcp_stream.WriteAsync(header_bytes, 0, header_bytes.Length, ct)。

cts = new CancellationTokenSource();

通过 ct = cts.Token

在 cancel_event() 中: if(cts != null) cts.Cancel();

【讨论】: