繁体   English   中英

SignalR 多客户端方法调用异步返回-.Net Core 6

[英]SignalR multi client method call async return - .Net Core 6

我正在尝试使用 signalR 来触发流程并实时从客户那里获得回报。 我的使用场景由 2 种客户端类型(流程管理器和存储)和一种服务器(集线器)组成。

第一个客户端类型是“进程管理器”。 此客户端将用于管理流程执行。 它将调用集线器的(服务器)方法,要求它在某个连接上调用某个方法。 然后集线器将在该连接上调用该方法并“等待”答案。 第二种客户端类型(“Store”)将执行调用的方法并通过调用响应方法“返回”到集线器。 然后,该集线器方法将触发等待返回的任务的继续,然后响应最终将到达流程管理器。

PM -- (invokes)--> HUB -- (invokes)--> STORE -- (invokes)--> HUB -- (return)--> PM

我的问题是我需要异步调用多个方法,此时,客户端方法的调用正在锁定后续调用。

通过一些研究,我设法编写了下面的代码。 我调用了两个方法,GetLocalIpAddress 和 GetMachineName。 GetLocalIpAddress 故意添加了 5 秒的延迟来模拟 GetMachineName 是否会首先返回。

发生的事情是 GetMachineName 在返回之前等待 GetLocalIpAddress 完成。

在此处输入图像描述

是否可以异步进行这些调用? 什么先执行先返回,不管其他方法是什么? 对于不同的方法和多次调用的同一方法,我需要这种行为。 例如,假设一个执行查询的方法。 执行不同的查询可能需要比其他查询更长的时间。 我想在方法执行后立即返回数据,而不管首先发送的同一方法的其他调用仍在运行。

我对异步代码没有太多经验。 所以请不要犹豫,指出或改变任何错误。

PM 客户端 - 控制台应用程序

var uri = "https://localhost:7155/SignalR";

HubConnection connection = new HubConnectionBuilder()
    .WithUrl(uri, (opts) =>
    {
        opts.Headers.Add("ID", "ProcessManager");
    }).Build();

await connection.StartAsync();

HubRequest(0, "Store0", "GetLocalIpAddress");
HubRequest(1, "Store0", "GetMachineName");

Console.ReadLine();

async void HubRequest(int i, string connectionID, string methodName, string? data = null)
{
    Console.WriteLine($"{i} - Request {methodName} from remote client - {connectionID}");
    string response = await connection.InvokeAsync<string>("ExecuteOnClient", connectionID, methodName, data);
    Console.WriteLine($"{i} - Response {methodName} from remote client: {response} - {connectionID}");
}

服务器 - Asp.net 核心 Web 应用程序

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();
app.MapHub<MainHub>("/SignalR");
app.MapGet("/", () => "Hub initialized");
app.Run();

public class MainHub : Hub
{
    static Dictionary<string, string> connectionsList = new Dictionary<string, string>();
    private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> _responses = new ();

    public async override Task OnConnectedAsync()
    {
        HttpRequest request = Context.GetHttpContext().Request;
        string id = request.Headers["ID"].ToString().Trim();
        string connectionId = Context.ConnectionId;

        if (!id.Equals(("ProcessManager")))
        {
            id = $"{id}{connectionsList.Where(x => x.Key.Contains("Store")).Count()}";
            await Clients.Client(connectionId).SendAsync("AfterConnected", id);
        }

        connectionsList.Add(id, connectionId);
    }

    public async override Task OnDisconnectedAsync(Exception exception)
    {
        string connectionId = Context.ConnectionId;

        foreach (var item in connectionsList.Where(kvp => kvp.Value == connectionId).ToList())
            connectionsList.Remove(item.Key);        
    }

    public void ClientResponse(Guid guidRequestID, string returnData)
    {
        TaskCompletionSource<object> tcs;

        if (_responses.TryGetValue(guidRequestID, out tcs))
        {
            // Trigger the task continuation
            tcs.TrySetResult(returnData);
        }
        //else
        //{
            // Client response for something that isn't being tracked, might be an error
            //Test Only
            //throw new Exception("Resposta não Esperada.");
        //}
    }

    public async Task<string> ExecuteOnClient(string connectionID, string methodName, string data)
    {
        // Create an entry in the dictionary that will be used to track the client response
        var tcs = new TaskCompletionSource<object>();
        var guidRequestID = Guid.NewGuid();

        _responses.TryAdd(guidRequestID, tcs);

        // Call method on the client passing the identifier
        if (!string.IsNullOrWhiteSpace(data))
            Clients.Client(connectionsList[connectionID]).SendAsync(methodName, guidRequestID, data);
        else
            Clients.Client(connectionsList[connectionID]).SendAsync(methodName, guidRequestID);

        try
        {
            // Wait for the client to respond
            int timeout = 10000;
            var task = tcs.Task;
            if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
            {
                return (string)await task;
            }
            else
            {
                //Cancel this task if the client disconnects (potentially by just adding a timeout)
                return "TimedOut";//Test
            }
        }
        finally
        {
            // Remove the tcs from the dictionary so that we don't leak memory
            _responses.TryRemove(guidRequestID, out tcs);
        }
    }
}

商店客户端 - 控制台应用程序

string uri = "https://localhost:7155/SignalR";

HubConnection connection = new HubConnectionBuilder()
    .WithUrl(uri, (opts) =>
    {
        opts.Headers.Add("ID", "Store");
    }).Build();

connection.On<string>("AfterConnected", (data) =>
{
    Console.WriteLine($"Client name - {data}");
});

connection.On<string>("GetLocalIpAddress", (guidRequestID) =>
{
    Console.WriteLine($"GetLocalIpAddress - {GetLocalIPAddress()}");
    Thread.Sleep(5000);
    connection.InvokeAsync("ClientResponse", guidRequestID, GetLocalIPAddress());
});

connection.On<string>("GetMachineName", (guidRequestID) =>
{
    Console.WriteLine($"GetMachineName - {System.Environment.MachineName}");
    connection.InvokeAsync("ClientResponse", guidRequestID, System.Environment.MachineName);
});

await connection.StartAsync();
Console.ReadKey();

static string GetLocalIPAddress()
{
    var host = Dns.GetHostEntry(Dns.GetHostName());
    foreach (var ip in host.AddressList)
    {
        if (ip.AddressFamily == AddressFamily.InterNetwork)
        {
            return ip.ToString();
        }
    }
    throw new Exception("No network adapters with an IPv4 address in the system!");
}

有两种选择:

  1. 重构您的集线器方法,因此它不会在其中一个中等待结果,而是会存储一些信息以在结果出现时调用客户端
  2. 使用 MaximumParallelInvocationsPerClient 选项并将其设置为大于 1 的值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM