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

时间:2017-02-08 14:49:21

标签: c# wpf asynchronous task-parallel-library

通过async / await(来自线程池)完成所有工作并且我遇到了一个有趣的挑战。

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

private List<Client> clients = new List<Client>();
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");
    }
}

这是这个功能:

public async Task<string> AllReadLineAsync()
{
    var tasklist = new List<Task<string>>();

    foreach (var client in clients)
        tasklist.Add(client.ReadLineAsync());

    while (tasklist.Count > 0)
    {
        Task<string> finishedTask = await Task.WhenAny(tasklist);
        if (finishedTask.Status == TaskStatus.RanToCompletion)
            return await finishedTask;

        tasklist.Remove(finishedTask);
    }

    return "Error: No task finished";
}

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

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

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

所以我目前的实现方式很有效,但我无法弄清楚如果其他客户端不发送数据,那么从1个客户端建立消息的错误。

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

编辑:以下是我所看到的行为 我输入的顺序是:左,右,左2,右2,左3,右3 好像有些消息被删除了?

enter image description here

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

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

2 个答案:

答案 0 :(得分:2)

  

看起来好像有些邮件被删除了?

是。因为当您调用其方法时,异步工作已启动(例如,ReadLineAsync)。当其中一个完成(Task.WhenAny)时,您的代码将放弃其他任务。但是他们继续跑步 - 他们仍在从他们的插座中读书,而他们所阅读的一切都将被扔掉。

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

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

答案 1 :(得分:1)

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

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

public async Task<string> AllReadLineAsync()
{
    var tasklist = new List<Task<string>>();

    foreach (var client in clients)
        tasklist.Add(client.ReadLineAsync());

    while (tasklist.Count > 0)
    {
        Task<string> 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()的方法。相反,我将此功能添加到我的客户端类:

public async Task<string> 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<string> 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的方式。