SignalR でグループを使用する

作成者: Patrick FletcherTom FitzMacken

警告

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

このトピックでは、グループにユーザーを追加し、グループ メンバーシップ情報を保持する方法について説明します。

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

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

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

質問とコメント

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

概要

SignalR のグループは、接続されているクライアントの指定されたサブセットにメッセージをブロードキャストするためのメソッドを提供します。 グループには任意の数のクライアントを含めることができます。また、クライアントは任意の数のグループのメンバーにすることができます。 グループを明示的に作成する必要はありません。 実際には、Groups.Add の呼び出しで初めて名前を指定すると、グループが自動的に作成され、最後の接続をメンバーシップから削除すると削除されます。 グループの使用の概要については、「Hubs API - サーバー ガイド」の 「Hub クラスからグループ メンバーシップを管理する方法 」を参照してください。

グループ メンバーシップ リストまたはグループの一覧を取得するための API はありません。 SignalR は、pub/sub モデルに基づいてクライアントとグループにメッセージを送信し、サーバーはグループまたはグループ メンバーシップのリストを保持しません。 これは、Web ファームにノードを追加するたびに SignalR が維持する状態を新しいノードに伝達する必要があるため、スケーラビリティを最大化するのに役立ちます。

メソッドを使用して Groups.Add グループにユーザーを追加すると、ユーザーは現在の接続の間、そのグループに送信されたメッセージを受信しますが、そのグループのユーザーのメンバーシップは現在の接続を超えて保持されません。 グループとグループ メンバーシップに関する情報を永続的に保持する場合は、そのデータをデータベースや Azure テーブル ストレージなどのリポジトリに格納する必要があります。 次に、ユーザーがアプリケーションに接続するたびに、ユーザーが属しているグループのリポジトリから取得し、そのユーザーを手動でそれらのグループに追加します。

一時的な中断後に再接続すると、ユーザーは以前に割り当てられたグループに自動的に再参加します。 グループへの自動的な再参加は、再接続時にのみ適用され、新しい接続を確立する場合には適用されません。 デジタル署名されたトークンは、以前に割り当てられたグループの一覧を含むクライアントから渡されます。 ユーザーが要求されたグループに属しているかどうかを確認する場合は、既定の動作をオーバーライドできます。

このトピックのセクションは次のとおりです。

ユーザーの追加と削除

グループに対してユーザーを追加または削除するには、 Add メソッドまたは Remove メソッドを呼び出し、ユーザーの接続 ID とグループの名前をパラメーターとして渡します。 接続が終了したときに、グループからユーザーを手動で削除する必要はありません。

次の例は、 Groups.Add Hub メソッドで使用される メソッドと Groups.Remove メソッドを示しています。

public class ContosoChatHub : Hub
{
    public Task JoinRoom(string roomName)
    {
        return Groups.Add(Context.ConnectionId, roomName);
    }

    public Task LeaveRoom(string roomName)
    {
        return Groups.Remove(Context.ConnectionId, roomName);
    }
}

メソッドと Groups.Remove メソッドはGroups.Add非同期的に実行されます。

グループにクライアントを追加し、そのグループを使用してすぐにメッセージをクライアントに送信する場合は、Groups.Add メソッドが最初に完了していることを確認する必要があります。 次のコード例は、その方法を示しています。

public async Task JoinRoom(string roomName)
{
    await Groups.Add(Context.ConnectionId, roomName);
    Clients.Group(roomName).addChatMessage(Context.User.Identity.Name + " joined.");
}

一般に、 メソッドを呼び出Groups.Removeすときは、削除しようとしている接続 ID が使用できなくなる可能性があるため、 を含awaitめないようにしてください。 その場合、 TaskCanceledException は要求がタイムアウトした後にスローされます。アプリケーションで、グループにメッセージを送信する前にユーザーがグループから削除されていることを確認する必要がある場合は、 の前に Groups.Removeを追加awaitし、スローされる可能性のある例外をTaskCanceledExceptionキャッチできます。

グループのメンバーを呼び出す

次の例に示すように、グループのすべてのメンバーまたはグループの指定されたメンバーにのみメッセージを送信できます。

  • 指定したグループ内のすべての接続済みクライアント。

    Clients.Group(groupName).addChatMessage(name, message);
    
  • 接続 ID で識別される、指定した クライアントを除く、指定されたグループ内のすべての接続済みクライアント。

    Clients.Group(groupName, connectionId1, connectionId2).addChatMessage(name, message);
    
  • 呼び出し元のクライアントを除く、指定したグループ内のすべての接続済みクライアント。

    Clients.OthersInGroup(groupName).addChatMessage(name, message);
    

データベースへのグループ メンバーシップの格納

次の例では、グループとユーザーの情報をデータベースに保持する方法を示します。 任意のデータ アクセス テクノロジを使用できます。ただし、次の例は、Entity Framework を使用してモデルを定義する方法を示しています。 これらのエンティティ モデルは、データベース テーブルとフィールドに対応しています。 データ構造は、アプリケーションの要件によって大きく異なる場合があります。 この例には、 という名前 ConversationRoom のクラスが含まれています。これは、ユーザーがスポーツやガーデニングなど、さまざまなテーマに関する会話に参加できるようにするアプリケーションに固有のクラスです。 この例には、接続の クラスも含まれています。 接続クラスは、グループ メンバーシップを追跡するために絶対に必要ではありませんが、多くの場合、ユーザーを追跡するための堅牢なソリューションの一部です。

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

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

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

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

    public class ConversationRoom
    {
        [Key]
        public string RoomName { get; set; }
        public virtual ICollection<User> Users { get; set; }
    }
}

その後、ハブで、データベースからグループとユーザー情報を取得し、ユーザーを適切なグループに手動で追加できます。 この例には、ユーザー接続を追跡するためのコードは含まれていません。 この例では、メッセージがグループのawaitメンバーにすぐに送信されないため、キーワード (keyword)は前Groups.Addに適用されません。 新しいメンバーを追加した直後にグループのすべてのメンバーにメッセージを送信する場合は、キーワード (keyword)をawait適用して非同期操作が完了したことを確認します。

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

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            using (var db = new UserContext())
            {
                // Retrieve user.
                var user = db.Users
                    .Include(u => u.Rooms)
                    .SingleOrDefault(u => u.UserName == Context.User.Identity.Name);

                // If user does not exist in database, must add.
                if (user == null)
                {
                    user = new User()
                    {
                        UserName = Context.User.Identity.Name
                    };
                    db.Users.Add(user);
                    db.SaveChanges();
                }
                else
                {
                    // Add to each assigned group.
                    foreach (var item in user.Rooms)
                    {
                        Groups.Add(Context.ConnectionId, item.RoomName);
                    }
                }
            }
            return base.OnConnected();
        }

        public void AddToRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);

                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name};
                    db.Users.Attach(user);

                    room.Users.Add(user);
                    db.SaveChanges();
                    Groups.Add(Context.ConnectionId, roomName);
                }
            }
        }

        public void RemoveFromRoom(string roomName)
        {
            using (var db = new UserContext())
            {
                // Retrieve room.
                var room = db.Rooms.Find(roomName);
                if (room != null)
                {
                    var user = new User() { UserName = Context.User.Identity.Name };
                    db.Users.Attach(user);

                    room.Users.Remove(user);
                    db.SaveChanges();
                    
                    Groups.Remove(Context.ConnectionId, roomName);
                }
            }
        }
    }
}

Azure テーブル ストレージへのグループ メンバーシップの格納

Azure テーブル ストレージを使用してグループとユーザーの情報を格納することは、データベースの使用と似ています。 次の例は、ユーザー名とグループ名を格納するテーブル エンティティを示しています。

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

namespace GroupsExample
{
    public class UserGroupEntity : TableEntity
    {
        public UserGroupEntity() { }

        public UserGroupEntity(string userName, string groupName)
        {
            this.PartitionKey = userName;
            this.RowKey = groupName;
        }
    }
}

ハブでは、ユーザーが接続したときに割り当てられたグループを取得します。

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

namespace GroupsExample
{
    [Authorize]
    public class ChatHub : Hub
    {
        public override Task OnConnected()
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();
            table.CreateIfNotExists();
            var query = new TableQuery<UserGroupEntity>()
                .Where(TableQuery.GenerateFilterCondition(
                "PartitionKey", QueryComparisons.Equal, userName));
            
            foreach (var entity in table.ExecuteQuery(query))
            {
                Groups.Add(Context.ConnectionId, entity.RowKey);
            }

            return base.OnConnected();
        }

        public Task AddToRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var insertOperation = TableOperation.InsertOrReplace(
                new UserGroupEntity(userName, roomName));
            table.Execute(insertOperation);

            return Groups.Add(Context.ConnectionId, roomName);
        }

        public Task RemoveFromRoom(string roomName)
        {
            string userName = Context.User.Identity.Name;

            var table = GetRoomTable();

            var retrieveOperation = TableOperation.Retrieve<UserGroupEntity>(
                userName, roomName);
            var retrievedResult = table.Execute(retrieveOperation);

            var deleteEntity = (UserGroupEntity)retrievedResult.Result;

            if (deleteEntity != null)
            {
                var deleteOperation = TableOperation.Delete(deleteEntity);
                table.Execute(deleteOperation);
            }

            return Groups.Remove(Context.ConnectionId, roomName);
        }

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

再接続時のグループ メンバーシップの確認

既定では、SignalR は、接続が切断され、接続がタイムアウトする前に再確立されたときなど、一時的な中断から再接続するときに、適切なグループにユーザーを自動的に再割り当てします。再接続時にユーザーのグループ情報がトークンに渡され、そのトークンがサーバー上で検証されます。 ユーザーをグループに再参加させる検証プロセスの詳細については、「 再接続時のグループの再参加」を参照してください。

一般に、再接続時にグループに自動的に再参加する既定の動作を使用する必要があります。 SignalR グループは、機密データへのアクセスを制限するためのセキュリティ メカニズムとして意図されていません。 ただし、再接続時にアプリケーションでユーザーのグループ メンバーシップをダブルチェックする必要がある場合は、既定の動作をオーバーライドできます。 既定の動作を変更すると、ユーザーが接続するときだけでなく、再接続ごとにユーザーのグループ メンバーシップを取得する必要があるため、データベースに負担が加わる可能性があります。

再接続時にグループ メンバーシップを確認する必要がある場合は、次に示すように、割り当てられたグループの一覧を返す新しいハブ パイプライン モジュールを作成します。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace GroupsExample
{
    public class RejoingGroupPipelineModule : HubPipelineModule
    {
        public override Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            BuildRejoiningGroups(Func<HubDescriptor, IRequest, IList<string>, IList<string>> 
            rejoiningGroups)
        {
            rejoiningGroups = (hb, r, l) => 
            {
                List<string> assignedRooms = new List<string>();
                using (var db = new UserContext())
                {
                    var user = db.Users.Include(u => u.Rooms)
                        .Single(u => u.UserName == r.User.Identity.Name);
                    foreach (var item in user.Rooms)
                    {
                        assignedRooms.Add(item.RoomName);
                    }
                }
                return assignedRooms;
            };

            return rejoiningGroups;
        }
    }
}

次に、次に強調表示されているように、そのモジュールをハブ パイプラインに追加します。

public partial class Startup {
    public void Configuration(IAppBuilder app) {
        app.MapSignalR();
        GlobalHost.HubPipeline.AddModule(new RejoingGroupPipelineModule());
    }
}