WPF + Caliburn Micro:异步等待阻止侦听器

时间:2015-08-13 14:30:49

标签: wpf async-await caliburn.micro tcpclient

使用VS2013,WPF 4.5,Caliburn Micro。我想在我的工作客户端应用程序上用新的" async await"替换用于tcp数据监听器的多线程的旧方法。

由于我不熟悉它们,我买了并阅读了这本书" C#cookbook中的并发" (作者:Stephen Cleary先生)和C#深度第三版(作者:Jon Skeet先生)。然后我首先编写一个示例代码(见下文),因为我不想弄乱在客户机器上运行的客户端应用程序。

问题:点击连接按钮后,GUI被阻止/冻结。点击任何按钮,即使是" x"按钮无法帮助。我需要使用任务管理器终止进程。

到目前为止我一直在尝试,但仍然没有找到答案,也没有找到有用的示例代码:

  • 阅读Cleary先生的书以及他关于TCP Socket FAQ的博客。
  • 阅读Skeet先生第5章第15章的书。
  • 通过电子邮件联系Cleary先生(从他的博客中获取),仍然没有回答;也许他还在忙。

因此我在这里问。请告诉我如何解决问题,并随时修改我的代码。提前谢谢

ShellViewModel

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Caliburn.Micro;

namespace TcpClientAsyncAwait.ViewModels
{
public class ShellViewModel : Conductor<Screen>
{
    private const Int64 MAX_DATA_LENGTH = 1024;
    private const String CRLF = "\r\n";

    private TcpClient _client;
    private List<String> _responseData;
    private Boolean _shouldStop;
    private NetworkStream _stream;
    private String _cmdConnect = "<StartConnect/>";

    public TcpClient Client
    {
        get { return _client; }
        set
        {
            _client = value;
            NotifyOfPropertyChange(() => Client);
        }
    }

    public List<String> ResponseData
    {
        get { return _responseData; }
        set
        {
            _responseData = value;
            NotifyOfPropertyChange(() => ResponseData);
        }
    }


    public ShellViewModel()
    {
    }

    public void Connect()
    {
        var hostIp = "127.0.0.1";
        var port = 16770;

        _client = new TcpClient(hostIp, port);
        var listenTask = StartListenerAsync();
        SendCmd(_cmdConnect);
    }

    private async Task StartListenerAsync()
    {
        _shouldStop = false;
        if (_client != null)
        {
            while (!_shouldStop)
            {
                await GetMessageAsync().ConfigureAwait(false);
            }
        }
    }

    private async Task GetMessageAsync()
    {
        try
        {
            if (_stream == null) _stream = _client.GetStream();
            if (_stream.DataAvailable)
            {
                Byte[] buffer = new Byte[MAX_DATA_LENGTH];
                Int32 readBytes = await _stream.ReadAsync(buffer, 0, buffer.Length);
                var receivedData = UTF8Encoding.UTF8.GetString(buffer, 0, readBytes);
                Trace.WriteLine(receivedData);
            }
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.Message);
        }
    }

    private async Task SendCmd(string cmd)
    {
        if (_client != null)
        {
            // Manipulate command to fulfill format, that can be accepted by server
            Byte[] utf8Source = UTF8Encoding.UTF8.GetBytes(cmd);
            Byte[] suffix = UTF8Encoding.UTF8.GetBytes(CRLF);
            Byte[] utf8Result = new byte[utf8Source.Length + suffix.Length];
            Buffer.BlockCopy(utf8Source, 0, utf8Result, 0, utf8Source.Length);
            Buffer.BlockCopy(suffix, 0, utf8Result, utf8Source.Length, suffix.Length);

            if (_stream == null) _stream = _client.GetStream();
            using (var sw = new StreamWriter(_stream))
            {
                var data = UTF8Encoding.UTF8.GetString(utf8Result).ToCharArray();
                await sw.WriteAsync(data, 0, utf8Result.Length).ConfigureAwait(false);
                await sw.FlushAsync().ConfigureAwait(false);
            }
        }

    }

    public void Disconnect()
    {
        _shouldStop = true;
        if (_client != null) _client.Close();
    }
}
}

ShellView

<Window x:Class="TcpClientAsyncAwait.Views.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        d:DesignHeight="300"
        d:DesignWidth="300"
        mc:Ignorable="d">
<Grid Width="300" Height="300">
<StackPanel Width="130" Height="120"
            HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button x:Name="Connect"
            Width="120" Height="50" Margin="5"
            HorizontalAlignment="Center" VerticalAlignment="Center"
            Content="Connect" />
    <Button x:Name="DisConnect"
            Width="120" Height="50" Margin="5"
            HorizontalAlignment="Center" VerticalAlignment="Center"
            Content="Disconnect" />
</StackPanel>
</Grid>
</Window>

编辑:

由于完整性,可以通过向ViewModel添加名称空间和向View添加XAML标题来编辑示例代码。

EDIT1: 好的,我得到一个tcp server sample code from MSDN,它作为我的客户端示例代码的服务器。 这里是编译服务器示例代码的步骤。

  1. 从链接下载该项目的C#版本。
  2. 使用VS2013:打开ClientApp.sln。
  3. 在Solution Explorer中:打开项目ServerApp并打开Form1.cs进行编辑。
  4. 转到第30行并将端口3000编辑为16770.保存更改。
  5. 将项目ServerApp作为启动项目。
  6. 构建并运行它。
  7. 现在步骤如何创建和编译我的客户端示例代码,以便您可以重现我遇到的问题。请完全按照步骤进行操作。

    1. 打开VS2013
    2. 获取&#34; SideWaffle模板包&#34;从Visual Studio库扩展,安装并重新启动VS2013。
    3. 创建&#34;新项目&#34; - 选择&#34; Caliburn.Micro WPF应用程序&#34; SideWafle的项目(参见此处)screenshot将解决方案名称设置为TcpClientAsyncAwait
    4. 点击&#34;工具 - NuGet包管理器 - 包管理器控制台&#34;。在Package Manage Console窗口中,您将看到警告&#34;此解决方案中缺少某些NuGet包。单击以从在线包源中恢复。&#34;。只需点击还原按钮
    5. 按照说明操作即可
    6. 下载完成后,构建解决方案(通常我按&#34; F6&#34;)。
    7. 在解决方案资源管理器中:打开&#34;查看&#34;文件夹并打开ShellView.xaml。您将看到ShellView.xaml.cs(后面的代码)。只需删除该文件即可。
    8. 打开ShellView.xaml并删除其中的所有代码。现在你可以复制&amp;完全粘贴我的示例代码ShellView.xaml。
    9. 打开&#34; ViewModels&#34;文件夹并打开ShellViewModel.cs进行编辑。
    10. 删除其中的所有代码并复制&amp;将我的示例代码ShellViewModel.cs粘贴到那里。
    11. 构建它。
    12. 注意:请在运行客户端之前运行服务器,因为我的示例代码非常简单,没有重新连接,也没有&#34; no-server&#34;检测机制。

      EDIT2:

      根据TedyPranolo的建议,似乎循环可能是问题所在。 如果我删除循环,await GetMessageAsync().ConfigureAwait(false);只执行一次然后结束。当服务器稍后发送另一条消息时,客户端不再收到该消息。

      Q1:现在我想知道如何使GetMessageAsync保持活动状态,并始终准备好从服务器一直监听传入消息,直到应用程序关闭。当然仍然在上下文中#async等待&#34;

      Q2:或者&#34; async await&#34;根本不适合这样的目的?

      我很高兴收到大家的一些解决方案建议。先感谢您。

2 个答案:

答案 0 :(得分:1)

您的UI线程可能会卡在此循环中。由于循环和GetMessageAsync之间没有延迟,因此线程可能太忙而无法处理任何其他内容。

while (!_shouldStop)
{
      await GetMessageAsync().ConfigureAwait(false);
}

您可能想尝试使用此SO thread中有关如何执行异步套接字的方法。 这是@spender在答案中的代码,以防该线程出现问题

void Main()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    TcpListener listener = new TcpListener(IPAddress.Any, 6666);
    try
    {
        listener.Start();
        //just fire and forget. We break from the "forgotten" async loops
        //in AcceptClientsAsync using a CancellationToken from `cts`
        AcceptClientsAsync(listener, cts.Token);
        Thread.Sleep(60000); //block here to hold open the server
    }
    finally
    {
        cts.Cancel();
        listener.Stop();
    }
}

async Task AcceptClientsAsync(TcpListener listener, CancellationToken ct)
{
    var clientCounter = 0;
    while (!ct.IsCancellationRequested)
    {
        TcpClient client = await listener.AcceptTcpClientAsync()
                                            .ConfigureAwait(false);
        clientCounter++;
        //once again, just fire and forget, and use the CancellationToken
        //to signal to the "forgotten" async invocation.
        EchoAsync(client, clientCounter, ct);
    }

}
async Task EchoAsync(TcpClient client,
                     int clientIndex,
                     CancellationToken ct)
{
    Console.WriteLine("New client ({0}) connected", clientIndex);
    using (client)
    {
        var buf = new byte[4096];
        var stream = client.GetStream();
        while (!ct.IsCancellationRequested)
        {
            //under some circumstances, it's not possible to detect
            //a client disconnecting if there's no data being sent
            //so it's a good idea to give them a timeout to ensure that 
            //we clean them up.
            var timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
            var amountReadTask = stream.ReadAsync(buf, 0, buf.Length, ct);
            var completedTask = await Task.WhenAny(timeoutTask, amountReadTask)
                                          .ConfigureAwait(false);
            if (completedTask == timeoutTask)
            {
                var msg = Encoding.ASCII.GetBytes("Client timed out");
                await stream.WriteAsync(msg, 0, msg.Length);
                break;
            }
            //now we know that the amountTask is complete so
            //we can ask for its Result without blocking
            var amountRead = amountReadTask.Result;
            if (amountRead == 0) break; //end of stream.
            await stream.WriteAsync(buf, 0, amountRead, ct)
                        .ConfigureAwait(false);
        }
    }
    Console.WriteLine("Client ({0}) disconnected", clientIndex);
}

答案 1 :(得分:1)

其他人已经指出代码示例在这里和那里缺少一些asyncawait。在您的问题的所有评论和正文中,我有点迷失,但为了确保我会问这个:

您是否检查过Connect()方法(绑定到按钮操作的方法)是否标记为async void?这应该是允许async void的少数情况之一。

评论后

编辑:确保该方法现在看起来与此类似:

public async void Connect()
{
    var hostIp = "127.0.0.1";
    var port = 16770;

    _client = new TcpClient(hostIp, port);

    await StartListenerAsync();  // no return value needed

    await SendCmdAsync(_cmdConnect);  // renamed for clarity & to adhere to conventions
}

此外,while (!_shouldStop)听起来很可疑。如果您需要中断异步操作,则应创建TaskCancellationSource并将其传递到需要此类中断功能的整个调用链。例如。如果以下代表一个调用链:

await YourMethodAsync(..., cancellationSource);
...
   await YourSubMethodAsync(..., cancellationSource);
   ...
      var result = await SomeDotNetMethodAsync(..., cancellationSource);

      await SomeOtherDotNetMethodAsync(..., cancellationSource);

如果您的自定义方法只调用开箱即用的异步方法,那就足够了。如果您的自定义方法执行lenghty操作,或者它们调用长阻塞方法 - 例如一个巨大的 Buffer.BlockCopy() - 然后由你的方法决定将长阻塞操作分解为某种较短的循环,并在每个循环中检查cancellationSource。

EDIT 在进一步重新阅读您的观点后:似乎所有内容的根源都是您希望客户端知道当服务器向其发送内容时。如果是这种情况,那么您正在寻找两者之间的双向通信。

一种原始的,低级别的方法是长轮询:客户端不时询问服务器是否有任何准备就绪。这与您正在进行的操作类似,但客户端无法继续专门继续轮询服务器。它应该每隔X秒/分钟/ ......并在此期间做其他事情。所以至少你应该选择:

private async Task StartListenerAsync()
{
    _shouldStop = false;
    if (_client != null)
    {
        while (!_shouldStop)
        {
            await GetMessageAsync().ConfigureAwait(false);

            // non-blocking sleep equivalent
            await Task.Delay(pauseIntervalInMillis);
        }
    }
}

但是,除了从头开始处理这种双向通信之外,还有更好的方法。您可以使用现成的,经过测试的库。请查看this SO thread,例如他们提到SignalR。如果您的服务器端在node.js + socket.io上,那么客户端可以使用SocketIoClientDotNet之类的内容。