[SignalR] 05 - In-Memory


繼續練習,這次想要取得線上使用者人數

先前的方式並沒有辦法取得線上使用者人數,所以如果有這個需求,勢必只能選擇用記憶體或是外部儲存的方式來記錄使用者的連線資訊了

ref:將 SignalR 使用者對應至連線

記憶體的方式其實也就是透過 Client 端連線、斷線、重新連線至 SignalR 的時候將資訊記錄下來,並做出相應的處理,而發送訊息的部分則依據發送對象的 ID,查詢目前連線,然後逐一發送給該對象底下的各個連線

Hub 連線事件處理

連線的時候,要記錄目前使用者,以及連線 ID

public override Task OnConnected()
{
    var userId = Context.QueryString["id"];
    Connections.Add(userId, Context.ConnectionId);

    return base.OnConnected();
}

斷線的時候,要將該使用者的連線 ID 刪除

public override Task OnDisconnected(bool stopCalled)
{
    var userId = Context.QueryString["id"];
    Connections.Remove(userId, Context.ConnectionId);

    return base.OnDisconnected(stopCalled);
}

重新連線的時候,如果該使用者的連線 ID 不存在,就紀錄連線 ID

public override Task OnReconnected()
{
    var userId = Context.QueryString["id"];
    // 如果該使用者的ClientId不在清單內,就加入
    if (!Connections.GetConnections(userId).Contains(Context.ConnectionId))
    {
        Connections.Add(userId, Context.ConnectionId);
    }

    return base.OnReconnected();
}

發送給單一使用者訊息的時候,透過我們所記錄的連線資訊逐一發送

/// <summary>
/// 傳遞訊息給某人
/// </summary>
/// <param name="userId">要傳遞的對象</param>
/// <param name="msg">訊息內容</param>
public void SendPrivateMsg(string userId, string msg)
{
    foreach (var connectionId in Connections.GetConnections(userId))
    {
        Clients.Client(connectionId).Received($"{msg} at {DateTime.Now:f} By Connections");
    }
}

傳送訊息給所有人就直接調用原有的 Client.All 就好了

管理使用者連線

新建一個類別,並於類別內宣告一個 Dictionary<T,HashSet>來記錄連線資訊,用 HashSet 是避免記錄到重複資料

public class ConnectionMapping<T>
{
    private readonly Dictionary<T, HashSet<string>> _connections =
        new Dictionary<T, HashSet<string>>();
    // ...略
}

記錄使用者連線

public void Add(T key, string connectionId)
{
    lock (_connections)
    {
        if (!_connections.TryGetValue(key, out var connections))
        {
            connections = new HashSet<string>();
            _connections.Add(key, connections);
        }

        lock (connections)
        {
            connections.Add(connectionId);
        }
    }
}

移除使用者連線

public void Remove(T key, string connectionId)
{
    lock (_connections)
    {
        if (!_connections.TryGetValue(key, out var connections))
        {
            return;
        }

        lock (connections)
        {
            connections.Remove(connectionId);

            if (connections.Count == 0)
            {
                _connections.Remove(key);
            }
        }
    }
}

取得某使用者的所有連線 ID

public IEnumerable<string> GetConnections(T key)
{
    return _connections.TryGetValue(key, out var connections)
        ? connections
        : Enumerable.Empty<string>();
}

綜合以上的方式,已經可以在 Hub 的資料傳遞當下,輸出目前的線上人數,但是如果是想要透過 API 取得目前線上使用者人數,還是應該要用外部儲存的方式去做

練習程式碼:Github

前端因為先前忽略的關係,密語對象的 value 應該要在 onclick 的時候才去抓,所以 app.js 應調整如下

這個方案當然還是有問題的,所以接下來的篇章將會再另外討論…