如何在 SignalR 客户端中使用 async/await 和 hub.On

Posted

技术标签:

【中文标题】如何在 SignalR 客户端中使用 async/await 和 hub.On【英文标题】:How to use async/await with hub.On in SignalR client 【发布时间】:2015-02-13 15:56:27 【问题描述】:

我有一个与 SignalR Hub(服务器)通信的 .Net Windows 服务(客户端)。大多数客户端方法都需要时间来完成。当收到来自服务器的调用时,我如何(或者我需要)包装目标方法/hub.On以避免警告:

“由于没有等待此调用,因此在调用完成之前继续执行当前方法。考虑将 await 运算符应用于调用结果”

在客户端,这是一个启动/设置代码示例:

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

同样在客户端,这是一个方法示例:

public static async Task<bool> OnMessageFromServer(string Id, string message)

    try
    
        var result = await processMessage(message);  //long running task
    
    catch (Exception ex)
    
        throw new Exception("There was an error processing the message: ", ex);
    
    return result;


public static async Task<bool> OnCommandFromServer(string Id, string command)

    try
    
        var result = await processCommand(command);  //long running task
    
    catch (Exception ex)
    
        throw new Exception("There was an error processing the message: ", ex);
    
    return result;

最终,我认为 _hub.On 正在注册回调,而不是来自服务器的实际执行(调用)。我想我需要进入实际执行的中间,等待 On[X]FromServer 的结果并返回结果。

******************更新的示例,带有更正的代码*********************

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

//original
//_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
//_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

//new async 
_hub.On<Message>("SendMessageToClient", 
    async (i) => await OnMessageFromServer(i.Id, i.Message));

_hub.On<Message>("SendCommandToClient", 
    async (i) => await OnCommandFromServer(i.Id, i.Message));

//expanding to multiple parameters
_hub.On<Message, List<Message>, bool, int>("SendComplexParamsToClient", 
    async (p1, p2, p3, p4) => 
       await OnComplexParamsFromServer(p1.Id, p1.Message, p2, p3, p4));    

然后目标方法签名将类似于

public static async Task<bool> OnComplexParamsFromServer(string id, string message,
                 List<Message> incommingMessages, bool eatMessages, int repeat)

    try
    
        var result = await processCommand(message);  //long running task
        if (result) 
        
             // eat up your incoming parameters
        
    
    catch (Exception ex)
    
        throw new Exception("There was an error processing the message: ", ex);
    
    return result;

感谢@AgentFire 的快速响应!!!

【问题讨论】:

建议不要使用.Wait();,而是使用await connection.Start() 感谢您的提示...我已更新代码以反映未来的读者。 【参考方案1】:

这是一个 void-awaitable 模式,像这样使用它:

_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))

【讨论】:

感谢您的回答!!!从可读性的角度来看,返回 bool 的“void-awaitable”?从代码来看,它看起来并没有返回任何东西。有没有更好的方法来注册回调,这对于查看我的代码的其他开发人员来说可能更清楚?或者这对真正的开发人员来说非常清楚,而对于像我这样的新手来说却不清楚? :-) 是的,async-void 方法是众所周知的。异步方法分为三种类型,实际上,您应该了解整个事情。不会伤害你的:] 您的答案是互联网上关于void-awaitable一词的唯一资源。除了非常不鼓励 async void 之外,我也不明白 async void 与此有什么关系。 这不是异步无效的。这是异步任务 因为OnMessageFromServer 返回Task&lt;bool&gt; 并且非常好。 @bboyle1234 当您不使用匿名函数时,它会变得更加清晰。例如,如果你被允许从这些方法中返回任何东西,他可以这样做:_hub.On&lt;string, string&gt;("SendMessageToClient", OnMessageFromServer);。但你不能。编译器抱怨方法签名不匹配。如果把OnMessageFromServer的返回值改成void就好了。【参考方案2】:

我知道这是旧的,但接受的答案会创建一个 async void 的 lambda。

但如果有未处理的异常,async void 方法可能会使您的应用程序崩溃。阅读here 和here。

那些文章确实说async void 被允许 因为事件,而这些是我们正在谈论的事件。但是,异常可能会使您的整个应用程序崩溃,这仍然是事实。因此,如果您打算这样做,请确保在任何可能引发异常的地方都有 try/catch 块。

但是async void 方法也可能导致意外行为,因为调用它的代码在开始执行其他操作之前没有等待它完成。

请记住,await 的好处是 ASP.NET 可以继续执行其他操作,稍后再返回到其余代码。通常这很好。但是在这种特定情况下,这可能意味着可以同时处理两个(或更多)传入消息,并且对于哪些消息首先完成是一个折腾(第一个完成处理的消息可能不是第一个进来的消息)。尽管这对您的情况可能很重要,也可能无关紧要。

你最好还是等着看:

_hub.On<Message>("SendMessageToClient",
                 i => OnMessageFromServer(i.Id, i.Message).GetAwaiter().GetResult());

请参阅here 和here 以了解使用.GetAwaiter().GetResult() 而不是.Wait() 的好处。

【讨论】:

另一方面,对所有集线器方法都有正常和异步调用。那么,你推荐哪一个?我应该对 Hub 类中的连接和其他方法使用异步方法吗?还是有些是异步的,有些是正常的? @hexadecimal 如果您正在调用一个方法,并且有一个异步方法,那么将它与await 一起使用。那是最好的。但在这种情况下,On&lt;T&gt;() 方法并非旨在接受 async 方法,这意味着如果您向其传递 async 方法,它不会等待您的方法,这可能会导致我描述的问题。【参考方案3】:

SignalR 客户端旨在按顺序调用处理程序方法,而无需交错。换句话说,“单线程”。您通常可以根据称为“SingleThreaded”的所有处理程序方法来设计 signalR 客户端代码。 (我在引号中使用“SingleThreaded”,因为......它不是单线程的,但我们似乎没有语言可以表达顺序调用的异步方法,而不以概念上单线程的方式交错)

然而,这里讨论的“async-void”方法打破了这个设计假设,并导致了客户端处理程序方法现在被并发调用的意外副作用。这是导致副作用的代码示例:

/// Yes this looks like a correct async method handler but the compiler is
/// matching the connection.On<int>(string methodName, Action<int> method)
/// overload and we get the "async-void" behaviour discussed above
connection.On<int>(nameof(AsyncHandler), async (i) => await AsyncHandler(i)));

/// This method runs interleaved, "multi-threaded" since the SignalR client is just
/// "fire and forgetting" it.
async Task AsyncHandler(int value) 
    Console.WriteLine($"Async Starting value");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending value");


/* Example output:
Async Starting 0
Async Starting 1
Async Starting 2
Async Starting 3
Async Starting 4
Async Starting 5
Async Starting 6
Async Starting 7
Async Starting 8
Async Ending 2
Async Ending 3
Async Ending 0
Async Ending 1
Async Ending 8
Async Ending 7
*/

如果您使用的是 ASP.NET Core,我们可以附加异步方法处理程序,并让客户端按顺序依次调用它们,无需交错,也不会阻塞任何线程。 我们利用 SignalR for ASP.NET Core 中引入的following override。

IDisposable On(this HubConnection hubConnection, string methodName, Type[] parameterTypes,
                Func<object[], Task> handler)

这是实现它的代码。可悲的是,您编写的附加处理程序的代码有点迟钝,但这里是:

/// Properly attaching an async method handler
connection.On(nameof(AsyncHandler), new[]  typeof(int) , AsyncHandler);

/// Now the client waits for one handler to finish before calling the next.
/// We are back to the expected behaviour of having the client call the handlers
/// one at a time, waiting for each to finish before starting the next.
async Task AsyncHandler(object[] values) 
    var value = values[0];
    Console.WriteLine($"Async Starting value");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending value");


/* Example output
Async Starting 0
Async Ending 0
Async Starting 1
Async Ending 1
Async Starting 2
Async Ending 2
Async Starting 3
Async Ending 3
Async Starting 4
Async Ending 4
Async Starting 5
Async Ending 5
Async Starting 6
Async Ending 6
Async Starting 7
Async Ending 7
*/

当然,现在您知道如何根据您的要求实现任何一种客户端行为。如果您选择使用 async-void 行为,最好将其注释得非常好,这样您就不会困住其他程序员,并确保您不会抛出未处理的任务异常。

【讨论】:

对于未来的读者,您所说的“最新版本的 SignalR 客户端”是 SignalR for ASP.NET Core (Microsoft.AspNetCore.SignalR.Client)。最新版 SignalR for ASP.NET (Microsoft.AspNet.SignalR.Client) 没有这个方法。这很重要,因为客户端版本必须与服务器版本匹配。如果服务器不是 ASP.NET Core,则无法使用 ASP.NET Core 客户端。 @GabrielLuci 很好,如果你喜欢,请随意编辑 非常有趣的信息!你在哪里找到它?我能找到的任何文档都仅限于如何编写一个 hello world 客户端,并且根本不涉及线程等主题......

以上是关于如何在 SignalR 客户端中使用 async/await 和 hub.On的主要内容,如果未能解决你的问题,请参考以下文章

SignalR .Net 客户端

如何使用 SignalR 加入群组

如何确定与 SignalR 客户端的服务器断开连接?

SignalR如何将javascript对象从一个客户端发送到另一个客户端

SignalR系列教程:在MVC在使用SignalR

使用HubConnection在SignalR中重新连接的正确逻辑