SignalR ユーザーを接続にマッピングする

Tom FitzMacken

警告

このドキュメントは、SignalR の最新バージョン用ではありません。 SignalR の ASP.NET Coreを見てみましょう。

このトピックでは、ユーザーとその接続に関する情報を保持する方法について説明します。

パトリック・フレッチャーはこのトピックを書くのを手伝いました。

このトピックで使用するソフトウェアのバージョン

このトピックの以前のバージョン

SignalR の以前のバージョンの詳細については、「 SignalR の古いバージョン」を参照してください。

質問とコメント

このチュートリアルが気に入った方法と、ページの下部にあるコメントで改善できる内容に関するフィードバックをお寄せください。 チュートリアルに直接関連しない質問がある場合は、 ASP.NET SignalR フォーラム または StackOverflow.com に投稿できます。

はじめに

ハブに接続している各クライアントは、一意の接続 ID を渡します。この値は、ハブ コンテキストの Context.ConnectionId プロパティで取得できます。 アプリケーションでユーザーを接続 ID にマップし、そのマッピングを保持する必要がある場合は、次のいずれかを使用できます。

これらの各実装については、このトピックで説明します。 クラスの OnConnected、、および OnReconnected メソッドを使用して、Hubユーザー接続の状態OnDisconnectedを追跡します。

アプリケーションに最適な方法は、次に依存します。

  • アプリケーションをホストしている Web サーバーの数。
  • 現在接続されているユーザーの一覧を取得する必要があるかどうか。
  • アプリケーションまたはサーバーの再起動時にグループとユーザーの情報を保持する必要があるかどうか。
  • 外部サーバーを呼び出す待機時間が問題であるかどうか。

次の表に、これらの考慮事項に適した方法を示します。

考慮事項 複数のサーバー 現在接続されているユーザーの一覧を取得する 再起動後に情報を保持する 最適なパフォーマンス
UserID プロバイダー
メモリ内
単一ユーザー グループ
永続的、外部

IUserID プロバイダー

この機能を使用すると、ユーザーは新しいインターフェイス IUserIdProvider を使用して、IRequest に基づいて userId を指定できます。

The IUserIdProvider

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

既定では、ユーザーの をユーザー IPrincipal.Identity.Name 名として使用する実装が存在します。 これを変更するには、アプリケーションの起動時に の IUserIdProvider 実装をグローバル ホストに登録します。

GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());

ハブ内から、次の API を使用してこれらのユーザーにメッセージを送信できます。

特定のユーザーにメッセージを送信する

public class MyHub : Hub
{
    public void Send(string userId, string message)
    {
        Clients.User(userId).send(message);
    }
}

メモリ内ストレージ

次の例では、メモリに格納されているディクショナリに接続情報とユーザー情報を保持する方法を示します。 ディクショナリでは、 を HashSet 使用して接続 ID を格納します。ユーザーは、いつでも SignalR アプリケーションに複数の接続を持つことができます。 たとえば、複数のデバイスまたは複数のブラウザー タブを介して接続されているユーザーは、複数の接続 ID を持つことになります。

アプリケーションがシャットダウンすると、すべての情報は失われますが、ユーザーが接続を再確立すると、再入力されます。 環境に複数の Web サーバーが含まれている場合、各サーバーに個別の接続のコレクションがあるため、メモリ内ストレージは機能しません。

最初の例は、接続へのユーザーのマッピングを管理するクラスを示しています。 HashSet のキーは、ユーザーの名前になります。

using System.Collections.Generic;
using System.Linq;

namespace BasicChat
{
    public class ConnectionMapping<T>
    {
        private readonly Dictionary<T, HashSet<string>> _connections =
            new Dictionary<T, HashSet<string>>();

        public int Count
        {
            get
            {
                return _connections.Count;
            }
        }

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

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

        public IEnumerable<string> GetConnections(T key)
        {
            HashSet<string> connections;
            if (_connections.TryGetValue(key, out connections))
            {
                return connections;
            }

            return Enumerable.Empty<string>();
        }

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

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

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

次の例では、ハブから接続マッピング クラスを使用する方法を示します。 クラスのインスタンスは、変数名 _connectionsに格納されます。

using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace BasicChat
{
    [Authorize]
    public class ChatHub : Hub
    {
        private readonly static ConnectionMapping<string> _connections = 
            new ConnectionMapping<string>();

        public void SendChatMessage(string who, string message)
        {
            string name = Context.User.Identity.Name;

            foreach (var connectionId in _connections.GetConnections(who))
            {
                Clients.Client(connectionId).addChatMessage(name + ": " + message);
            }
        }

        public override Task OnConnected()
        {
            string name = Context.User.Identity.Name;

            _connections.Add(name, Context.ConnectionId);

            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            string name = Context.User.Identity.Name;

            _connections.Remove(name, Context.ConnectionId);

            return base.OnDisconnected(stopCalled);
        }

        public override Task OnReconnected()
        {
            string name = Context.User.Identity.Name;

            if (!_connections.GetConnections(name).Contains(Context.ConnectionId))
            {
                _connections.Add(name, Context.ConnectionId);
            }

            return base.OnReconnected();
        }
    }
}

単一ユーザー グループ

ユーザーごとにグループを作成し、そのユーザーのみに到達する場合は、そのグループにメッセージを送信できます。 各グループの名前は、ユーザーの名前です。 ユーザーが複数の接続を持っている場合、各接続 ID がユーザーのグループに追加されます。

ユーザーが切断されたときに、グループからユーザーを手動で削除しないでください。 このアクションは、SignalR フレームワークによって自動的に実行されます。

次の例は、シングル ユーザー グループを実装する方法を示しています。

using Microsoft.AspNet.SignalR;
using System;
using System.Threading.Tasks;

namespace BasicChat
{
    [Authorize]
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            string name = Context.User.Identity.Name;

            Clients.Group(who).addChatMessage(name + ": " + message);
        }

        public override Task OnConnected()
        {
            string name = Context.User.Identity.Name;

            Groups.Add(Context.ConnectionId, name);

            return base.OnConnected();
        }
    }
}

永続的な外部ストレージ

このトピックでは、データベースまたは Azure テーブル ストレージを使用して接続情報を格納する方法について説明します。 この方法は、各 Web サーバーが同じデータ リポジトリと対話できるため、複数の Web サーバーがある場合に機能します。 Web サーバーの動作が停止した場合、またはアプリケーションが再起動した場合、 OnDisconnected メソッドは呼び出されません。 そのため、データ リポジトリに、有効でなくなった接続 ID のレコードが含まれる可能性があります。 これらの孤立したレコードをクリーンするには、アプリケーションに関連する期間外に作成された接続を無効にできます。 このセクションの例には、接続の作成時に追跡するための値が含まれますが、バックグラウンド プロセスとして行う必要があるため、古いレコードをクリーンする方法は示しません。

データベース

次の例では、接続とユーザー情報をデータベースに保持する方法を示します。 任意のデータ アクセス テクノロジを使用できます。ただし、次の例は、Entity Framework を使用してモデルを定義する方法を示しています。 これらのエンティティ モデルは、データベース テーブルとフィールドに対応しています。 データ構造は、アプリケーションの要件によって大きく異なる場合があります。

最初の例は、多くの接続エンティティに関連付けることができるユーザー エンティティを定義する方法を示しています。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace MapUsersSample
{
    public class UserContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Connection> Connections { get; set; }
    }

    public class User
    {
        [Key]
        public string UserName { get; set; }
        public ICollection<Connection> Connections { get; set; }
    }

    public class Connection
    {
        public string ConnectionID { get; set; }
        public string UserAgent { get; set; }
        public bool Connected { get; set; }
    }
}

次に、ハブから、次に示すコードを使用して、各接続の状態を追跡できます。

using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Microsoft.AspNet.SignalR;

namespace MapUsersSample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            var name = Context.User.Identity.Name;
            using (var db = new UserContext())
            {
                var user = db.Users.Find(who);
                if (user == null)
                {
                    Clients.Caller.showErrorMessage("Could not find that user.");
                }
                else
                {
                    db.Entry(user)
                        .Collection(u => u.Connections)
                        .Query()
                        .Where(c => c.Connected == true)
                        .Load();

                    if (user.Connections == null)
                    {
                        Clients.Caller.showErrorMessage("The user is no longer connected.");
                    }
                    else
                    {
                        foreach (var connection in user.Connections)
                        {
                            Clients.Client(connection.ConnectionID)
                                .addChatMessage(name + ": " + message);
                        }
                    }
                }
            }
        }

        public override Task OnConnected()
        {
            var name = Context.User.Identity.Name;
            using (var db = new UserContext())
            {
                var user = db.Users
                    .Include(u => u.Connections)
                    .SingleOrDefault(u => u.UserName == name);
                
                if (user == null)
                {
                    user = new User
                    {
                        UserName = name,
                        Connections = new List<Connection>()
                    };
                    db.Users.Add(user);
                }

                user.Connections.Add(new Connection
                {
                    ConnectionID = Context.ConnectionId,
                    UserAgent = Context.Request.Headers["User-Agent"],
                    Connected = true
                });
                db.SaveChanges();
            }
            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected(stopCalled);
        }
    }
}

Azure Table Storage

次の Azure テーブル ストレージの例は、データベースの例に似ています。 Azure Table Storage Service の使用を開始するために必要なすべての情報が含まれているわけではありません。 詳細については、「 .NET から Table Storage を使用する方法」を参照してください。

次の例は、接続情報を格納するためのテーブル エンティティを示しています。 ユーザー名でデータをパーティション分割し、接続 ID で各エンティティを識別するため、ユーザーはいつでも複数の接続を持つことができます。

using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace MapUsersSample
{
    public class ConnectionEntity : TableEntity
    {
        public ConnectionEntity() { }        

        public ConnectionEntity(string userName, string connectionID)
        {
            this.PartitionKey = userName;
            this.RowKey = connectionID;
        }
    }
}

ハブでは、各ユーザーの接続の状態を追跡します。

using Microsoft.AspNet.SignalR;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace MapUsersSample
{
    public class ChatHub : Hub
    {
        public void SendChatMessage(string who, string message)
        {
            var name = Context.User.Identity.Name;
            
            var table = GetConnectionTable();

            var query = new TableQuery<ConnectionEntity>()
                .Where(TableQuery.GenerateFilterCondition(
                "PartitionKey", 
                QueryComparisons.Equal, 
                who));

            var queryResult = table.ExecuteQuery(query).ToList();
            if (queryResult.Count == 0)
            {
                Clients.Caller.showErrorMessage("The user is no longer connected.");
            }
            else
            {
                foreach (var entity in queryResult)
                {
                    Clients.Client(entity.RowKey).addChatMessage(name + ": " + message);
                }
            }
        }

        public override Task OnConnected()
        {
            var name = Context.User.Identity.Name;
            var table = GetConnectionTable();
            table.CreateIfNotExists();

            var entity = new ConnectionEntity(
                name.ToLower(), 
                Context.ConnectionId);
            var insertOperation = TableOperation.InsertOrReplace(entity);
            table.Execute(insertOperation);
            
            return base.OnConnected();
        }

        public override Task OnDisconnected(bool stopCalled)
        {
            var name = Context.User.Identity.Name;
            var table = GetConnectionTable();

            var deleteOperation = TableOperation.Delete(
                new ConnectionEntity(name, Context.ConnectionId) { ETag = "*" });
            table.Execute(deleteOperation);

            return base.OnDisconnected(stopCalled);
        }

        private CloudTable GetConnectionTable()
        {
            var storageAccount =
                CloudStorageAccount.Parse(
                CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            return tableClient.GetTableReference("connection");
        }
    }
}