You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于ASP.NET Core的gRPC服务器如何跟踪客户端并发送定向数据

解决方案:ASP.NET Core gRPC服务器跟踪客户端并实现单发数据

一、获取客户端唯一标识并维护连接列表

要跟踪客户端,首先得确定可靠的唯一标识:优先用客户端主动上报的设备ID(比如Android设备的UUID或自定义唯一标识),如果没有则用客户端IP地址。然后用线程安全的集合维护在线客户端的连接信息。

1. 定义连接管理器

创建静态类管理在线客户端,存储客户端标识与对应的gRPC响应流(用于后续推送):

public class ClientInfo
{
    public IServerStreamWriter<YourCustomResponse> ResponseStream { get; set; }
    public DateTime LastHeartbeatTime { get; set; }
}

public static class ClientConnectionManager
{
    // 键:客户端唯一标识,值:连接信息
    public static ConcurrentDictionary<string, ClientInfo> ConnectedClients { get; } 
        = new ConcurrentDictionary<string, ClientInfo>();
}

2. 在gRPC方法中注册客户端

客户端的保活ping需要基于服务器流式RPC或双向流式RPC(短连接无法主动推送数据),在ping方法里完成客户端注册:

public override async Task KeepAlive(PingRequest request, IServerStreamWriter<PingResponse> responseStream, ServerCallContext context)
{
    // 从请求元数据获取客户端设备ID(客户端需提前在请求头中携带)
    var deviceIdHeader = context.RequestHeaders.FirstOrDefault(h => h.Key.Equals("Device-Id", StringComparison.OrdinalIgnoreCase));
    string clientId = deviceIdHeader?.Value ?? context.GetHttpContext().Connection.RemoteIpAddress?.ToString();

    if (string.IsNullOrEmpty(clientId))
    {
        await responseStream.WriteAsync(new PingResponse { Status = "Invalid client ID" });
        return;
    }

    // 更新或添加客户端连接信息
    if (ClientConnectionManager.ConnectedClients.TryGetValue(clientId, out var clientInfo))
    {
        clientInfo.LastHeartbeatTime = DateTime.UtcNow;
    }
    else
    {
        clientInfo = new ClientInfo
        {
            ResponseStream = responseStream,
            LastHeartbeatTime = DateTime.UtcNow
        };
        ClientConnectionManager.ConnectedClients.TryAdd(clientId, clientInfo);
        
        // 注册连接断开回调,自动移除离线客户端
        context.CancellationToken.Register(() => 
        {
            ClientConnectionManager.ConnectedClients.TryRemove(clientId, out _);
        });
    }

    // 返回保活响应
    await responseStream.WriteAsync(new PingResponse { Status = "Alive" });
}

二、自动清理离线客户端

结合客户端30秒的保活间隔,定期清理超过超时阈值(比如60秒)的无心跳客户端:
在ClientConnectionManager的静态构造函数中添加定时清理逻辑:

static ClientConnectionManager()
{
    var cleanupTimer = new Timer(_ => 
    {
        var now = DateTime.UtcNow;
        var offlineClients = ConnectedClients
            .Where(kv => now - kv.Value.LastHeartbeatTime > TimeSpan.FromSeconds(60))
            .Select(kv => kv.Key)
            .ToList();

        foreach (var clientId in offlineClients)
        {
            ConnectedClients.TryRemove(clientId, out _);
        }
    }, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
}

三、向单个客户端发送特定数据

通过客户端标识从连接管理器中找到对应响应流,直接写入数据即可:

public static async Task SendDataToClient(string clientId, YourCustomResponse message)
{
    if (ClientConnectionManager.ConnectedClients.TryGetValue(clientId, out var clientInfo))
    {
        try
        {
            await clientInfo.ResponseStream.WriteAsync(message);
        }
        catch (Exception)
        {
            // 写入失败说明连接已断开,移除客户端
            ClientConnectionManager.ConnectedClients.TryRemove(clientId, out _);
            // 可添加日志记录逻辑
        }
    }
}

在服务器业务逻辑中调用该方法即可实现单发:

// 示例:向指定设备发送通知
await SendDataToClient("android-device-uuid-123", new YourCustomResponse { Content = "订单已发货" });

关键注意事项

  • 标识可靠性:IP地址可能因客户端切换网络变化,建议强制客户端上报设备级唯一标识(比如Android的Settings.Secure.ANDROID_ID或自定义UUID)。
  • 流式RPC要求:只有长连接的流式RPC才能支持服务器主动推送,客户端的保活请求必须基于流式方法实现。
  • 线程安全:必须使用ConcurrentDictionary等线程安全集合,避免多线程操作连接列表时出现异常。

内容的提问来源于stack exchange,提问作者Nib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 12:17:19