如何使用Task.WhenAny与ReadLineAsync从任何TcpClient获取数据

通过所有async / await(来自线程池)的工作方式,我遇到了一个有趣的挑战。

我有一个在WPF应用程序中运行的TCP服务器,它接受客户端并将它们存储在List ,如:

 private List clients = new List(); while (running && clientCount <= maxClients) { Client client = new Client(await server.AcceptTcpClientAsync()); await client.WriteLineAsync("Connected to the Server!"); clients.Add(client); clientCount++; } 

所以我想要做的是遍历我的客户端列表,如果收到任何数据,我想将它附加到文本框。 我意识到这可能不是实现这一目标的最佳方式 ,我愿意接受建议,但这就是我目前的结构。

一个按钮启动循环并不断调用并等待AllReadLineAsync()

 private async void btnStartReadLoopClick(object sender, RoutedEventArgs e) { btnStartReadLoop.IsEnabled = false; while(server.clientCount > 0) { string text = await server.AllReadLineAsync(); txtOutputLog.AppendText("[client] " + text + "\n"); } } 

这个function是什么:

 public async Task AllReadLineAsync() { var tasklist = new List<Task>(); foreach (var client in clients) tasklist.Add(client.ReadLineAsync()); while (tasklist.Count > 0) { Task finishedTask = await Task.WhenAny(tasklist); if (finishedTask.Status == TaskStatus.RanToCompletion) return await finishedTask; tasklist.Remove(finishedTask); } return "Error: No task finished"; } 

此函数遍历客户端列表并创建所有ReadLineAsync()任务的List<Tast>

在任何给定的时间,我可能只有1或2个客户端实际发送数据,所以我不能WhenAll()和我尝试WhenAny()WaitAny()没有成功。

注意未来的googlers: WaitAny()就像Wait()并且正在阻止。 不要在UI线程上执行此操作。 而是使用WhenAny()并等待它。

所以我当前的实现有点工作,但我无法弄清楚如果其他客户端不发送数据,消息将从1个客户端构建的错误。

TL; DR:我正确地使用了WhenAny()还是有更好的方法让我等待ReadLineAsync并将结果传递给文本框?

编辑:这是我看到我按此顺序键入的行为:左,右,左2,右2,左3,右3,看起来好像有些消息被删除了?

在此处输入图像描述

编辑2:我找到了我在MSDN博客上复制的代码片段的来源: https : //blogs.msdn.microsoft.com/pfxteam/2012/08/02/processing-tasks-as-they-complete/

此代码段专门用于遍历任务列表,确保完成所有任务。 我不关心任务是否重复,所以我需要修改代码以始终检查整个任务列表而不是删除任何任务。

好像有些消息被删除了?

是。 因为在调用方法时会启动异步工作(例如, ReadLineAsync )。 当其中一个完成( Task.WhenAny )时,您的代码将放弃其他任务。 但他们继续跑步 – 他们仍在阅读他们的sockets,无论他们阅读什么,都会被扔掉。

AFAIK,当你再次从同一个套接字读取时,行为是未定义的 – 它可能会读取接下来的内容,或者它可能是排队的。 我知道你不应该同时从套接字(或任何流)发出多次读取。

套接字与async不完全匹配,因为它们可以随时发送数据。 您应该使用Rx或事件。 可以使async工作,但它非常复杂。

好吧,所以我弄清楚我哪里出错了,为什么我以前的代码不起作用。

首先,我们来谈谈这段代码的作用,以及它为什么不起作用:

 public async Task AllReadLineAsync() { var tasklist = new List>(); foreach (var client in clients) tasklist.Add(client.ReadLineAsync()); while (tasklist.Count > 0) { Task finishedTask = await Task.WhenAny(tasklist); if (finishedTask.Status == TaskStatus.RanToCompletion) return await finishedTask; tasklist.Remove(finishedTask); } return "Error: No task finished"; } 

1)创建一个await ReadLineAsync()的列表

2)当该列表的大小大于0时,我们等待等待任何ReadLineAsync函数。

3)当我们点击已完成的任务时,我们返回它的字符串,然后退出该函数

4)任何未完成的剩余ReadLineAsync函数仍在运行,但我们丢失了对其实例的引用。

5)此函数循环,在完成后立即调用AllReadAsync()

6)这使得我们尝试访问StreamReady,同时仍在等待第4步 – 因此抛出一个激活。


使用异步TCP服务器处理多个TcpClient

由于这种结构,我无法想出在我的应用程序中使用WhenAny()的方法。 相反,我将此function添加到我的客户端类:

 public async Task CheckForDataAsync() { if (!stream.DataAvailable) { await Task.Delay(5); return ""; } return await reader.ReadLineAsync(); } 

而不是等待ReadLineAsync() ,我们改为从TcpClient访问NetworkStream,我们检查是否有可用的数据, if(!stream.DataAvailable) ,如果没有,我们提前返回一个空字符串,否则我们await ReadLineAsync()因为我们知道我们有传入的数据,我们希望收到整行。

然后我们替换我所谈到的第一个函数, AllReadLineAsync()具有以下内容:

 public async Task AllReadLineAsync() { string data = "", packet = ""; foreach (var client in clients) { data = await client.CheckForDataAsync(); if (data != string.Empty) packet += string.Format($"[client] {data}\n"); } return packet; } 

这比我之前尝试的方式更简单。 现在,它在for循环中遍历所有客户端,并在每个客户端上调用CheckForDataAsync()函数。 由于此函数提前返回而不是无限期等待完整的ReadLineAsync()因此在AllReadLineAysnc()函数结束后,它不会继续在后台运行。

在我们完成所有客户端的循环之后,我们获取字符串数据包并将其返回到UI上下文,然后我们可以将数据添加到文本框中:

 private async void RecvData(object sender, RoutedEventArgs e) { while(server.hasClient) { string text = await server.AllReadLineAsync(); txtOutputLog.AppendText(text); } } 

就是这样。 这就是我在WPF应用程序中处理多个TcpClient的方式。