SignalR 1.x で SignalR ユーザーを接続にマッピングするMapping SignalR Users to Connections in SignalR 1.x

によってPatrick FletcherTom FitzMackenby Patrick Fletcher, Tom FitzMacken

Warning

このドキュメントは SignalR の最新バージョンはありません。This documentation isn't for the latest version of SignalR. 見てASP.NET Core SignalRします。Take a look at ASP.NET Core SignalR.

このトピックでは、ユーザーとの接続に関する情報を保持する方法を示します。This topic shows how to retain information about users and their connections.

はじめにIntroduction

ハブに接続する各クライアントでは、一意の接続 id を渡します。この値を取得することができます、Context.ConnectionIdハブ コンテキストのプロパティ。Each client connecting to a hub passes a unique connection id. You can retrieve this value in the Context.ConnectionId property of the hub context. アプリケーションにユーザーの接続 id を割り当てるし、そのマッピングを保持する場合、次のいずれかを使用できます。If your application needs to map a user to the connection id and persist that mapping, you can use one of the following:

このトピックでこれらの各実装に表示されます。Each of these implementations is shown in this topic. 使用する、 OnConnectedOnDisconnected、およびOnReconnectedのメソッド、Hubユーザー接続の状態を追跡するクラス。You use the OnConnected, OnDisconnected, and OnReconnected methods of the Hub class to track the user connection status.

アプリケーションに最適な方法によって異なります。The best approach for your application depends on:

  • アプリケーションをホストする web サーバーの数。The number of web servers hosting your application.
  • かどうかは、現在接続しているユーザーの一覧を取得する必要があります。Whether you need to get a list of the currently connected users.
  • かどうかは、アプリケーションまたはサーバーを再起動すると、グループとユーザーの情報を保持する必要があります。Whether you need to persist group and user information when the application or server restarts.
  • かどうか、外部のサーバーの呼び出しの待機時間が問題です。Whether the latency of calling an external server is an issue.

どちらのアプローチがこれらの考慮事項の動作を次の表に示します。The following table shows which approach works for these considerations.

複数のサーバーMore than one server 現在接続しているユーザーの一覧を取得します。Get list of currently connected users 再起動後の情報を永続化します。Persist information after restarts 最適なパフォーマンスOptimal performance
メモリ内In-memory
シングル ユーザー グループSingle-user groups
恒久的な外部Permanent, external

メモリ内ストレージIn-memory storage

次の例では、メモリに格納されているディクショナリ内の接続とユーザーの情報を保持する方法を示します。The following examples show how to retain connection and user information in a dictionary that is stored in memory. ディクショナリを使用して、HashSet接続 id を格納します。いつでもユーザーには、SignalR アプリケーションへの接続を 1 つ以上の可能性があります。The dictionary uses a HashSet to store the connection id. At any time a user could have more than one connection to the SignalR application. たとえば、複数のデバイスまたは 1 つ以上のブラウザー タブで接続されているユーザーは、1 つ以上の接続 id があります。For example, a user who is connected through multiple devices or more than one browser tab would have more than one connection id.

場合は、アプリケーションがシャット ダウン、すべての情報が失われたがする再作成ように、ユーザーは、その接続を再確立します。If the application shuts down, all of the information is lost, but it will be re-populated as the users re-establish their connections. メモリ内ストレージには、各サーバーが接続の別のコレクションをことになるため、環境に 1 つ以上の web サーバーが含まれている場合は機能しません。In-memory storage does not work if your environment includes more than one web server because each server would have a separate collection of connections.

最初の例では、接続に対するユーザーのマッピングを管理するクラスを示します。The first example shows a class that manages the mapping of users to connections. HashSet のキーは、ユーザーの名前になります。The key for the HashSet will be the user's name.

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);
                    }
                }
            }
        }
    }
}

次の例では、ハブからの接続マッピング クラスを使用する方法を示します。The next example shows how to use the connection mapping class from a hub. 変数名で、クラスのインスタンスが格納されている_connectionsします。The instance of the class is stored in a variable name _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()
        {
            string name = Context.User.Identity.Name;

            _connections.Remove(name, Context.ConnectionId);

            return base.OnDisconnected();
        }

        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();
        }
    }
}

シングル ユーザー グループSingle-user groups

ユーザーごとにグループを作成し、そのユーザーだけに接続するときに、そのグループにメッセージを送信できます。You can create a group for each user, and then send a message to that group when you want to reach only that user. 各グループの名前は、ユーザーの名前です。The name of each group is the name of the user. ユーザーが 1 つ以上の接続を持つ場合は、各接続の id がユーザーのグループに追加されます。If a user has more than one connection, each connection id is added to the user's group.

必要がありますいない手動で削除するユーザー、グループからユーザーが切断されたとき。You should not manually remove the user from the group when the user disconnects. この操作は、SignalR フレームワークによって自動的に実行されます。This action is automatically performed by the SignalR framework.

次の例では、シングル ユーザー グループを実装する方法を示します。The following example shows how to implement single-user groups.

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();
        }
    }
}

永続的な外部のストレージPermanent, external storage

このトピックでは、接続情報を格納するため、データベースまたは Azure テーブル ストレージを使用する方法を示します。This topic shows how to use either a database or Azure table storage for storing connection information. このアプローチは、各 web サーバーが、同じデータ リポジトリと対話できるため、複数の web サーバーがある場合は動作します。This approach works when you have multiple web servers because each web server can interact with the same data repository. Web サーバー アプリケーションが再起動されるかの処理を停止する場合、OnDisconnectedメソッドは呼び出されません。If your web servers stop working or the application restarts, the OnDisconnected method is not called. したがって、データ リポジトリが有効になっている接続 id のレコードにあることができます。Therefore, it is possible that your data repository will have records for connection ids that are no longer valid. これらの孤立したレコードをクリーンアップするには、アプリケーションに関連する時間枠の外部で作成されたすべての接続を無効にします。To clean up these orphaned records, you may wish to invalidate any connection that was created outside of a timeframe that is relevant to your application. このセクションの例には、接続が作成された日時を追跡するための値が含まれているバック グラウンド プロセスとして実行するために古いレコードをクリーンアップする方法を表示しません。The examples in this section include a value for tracking when the connection was created, but do not show how to clean up old records because you may want to do that as background process.

データベースDatabase

次の例では、データベースに接続し、ユーザーの情報を保持する方法を示します。The following examples show how to retain connection and user information in a database. 任意のデータ アクセス テクノロジを使用することができます。ただし、次の例では、Entity Framework を使用してモデルを定義する方法を示します。You can use any data access technology; however, the example below shows how to define models using Entity Framework. これらのエンティティ モデルは、データベース テーブルとフィールドに対応します。These entity models correspond to database tables and fields. データ構造は、アプリケーションの要件に応じて大きく異なる可能性があります。Your data structure could vary considerably depending on the requirements of your application.

最初の例では、多くの接続のエンティティに関連付けることができるユーザー エンティティを定義する方法を示します。The first example shows how to define a user entity that can be associated with many connection entities.

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; }
    }
}

次に、ハブから、以下に示すコードで各接続の状態を追跡できます。Then, from the hub, you can track the state of each connection with the code shown below.

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()
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected();
        }
    }
}

Azure テーブル ストレージAzure table storage

Azure テーブル ストレージの次の例では、データベースの例に似ています。The following Azure table storage example is similar to the database example. すべての Azure Table Storage サービスを開始する必要のある情報が含まれません。It does not include all of the information that you would need to get started with Azure Table Storage Service. 詳しくは、次を参照してください。 .NET からテーブル ストレージを使用する方法します。For information, see How to use Table storage from .NET.

次の例では、接続情報を格納するためのテーブル エンティティを示します。The following example shows a table entity for storing connection information. ユーザーの名前で、データをパーティション分割し、ユーザーでは、いつでも複数の接続ができるように、接続 id を使用して各エンティティを識別します。It partitions the data by user name, and identifies each entity by the connection id, so a user can have multiple connections at any time.

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;
        }
    }
}

ハブでは、各ユーザーの接続の状態を追跡します。In the hub, you track the status of each user's connection.

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()
        {
            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();
        }

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