SignalR でグループを使用する
作成者: Patrick Fletcher、Tom FitzMacken
警告
このドキュメントは、SignalR の最新バージョン用ではありません。 SignalR の ASP.NET Coreを見てみましょう。
このトピックでは、グループにユーザーを追加し、グループ メンバーシップ情報を保持する方法について説明します。
このトピックで使用するソフトウェアのバージョン
- Visual Studio 2013
- .NET 4.5
- SignalR バージョン 2
このトピックの以前のバージョン
SignalR の以前のバージョンの詳細については、「 SignalR の古いバージョン」を参照してください。
質問とコメント
このチュートリアルが気に入った方法と、ページの下部にあるコメントで改善できる内容に関するフィードバックをお寄せください。 チュートリアルに直接関連しない質問がある場合は、 ASP.NET SignalR フォーラム または StackOverflow.com に投稿できます。
概要
SignalR のグループは、接続されているクライアントの指定されたサブセットにメッセージをブロードキャストするためのメソッドを提供します。 グループには任意の数のクライアントを含めることができます。また、クライアントは任意の数のグループのメンバーにすることができます。 グループを明示的に作成する必要はありません。 実際には、Groups.Add の呼び出しで初めて名前を指定すると、グループが自動的に作成され、最後の接続をメンバーシップから削除すると削除されます。 グループの使用の概要については、「Hubs API - サーバー ガイド」の 「Hub クラスからグループ メンバーシップを管理する方法 」を参照してください。
グループ メンバーシップ リストまたはグループの一覧を取得するための API はありません。 SignalR は、pub/sub モデルに基づいてクライアントとグループにメッセージを送信し、サーバーはグループまたはグループ メンバーシップのリストを保持しません。 これは、Web ファームにノードを追加するたびに SignalR が維持する状態を新しいノードに伝達する必要があるため、スケーラビリティを最大化するのに役立ちます。
メソッドを使用して Groups.Add
グループにユーザーを追加すると、ユーザーは現在の接続の間、そのグループに送信されたメッセージを受信しますが、そのグループのユーザーのメンバーシップは現在の接続を超えて保持されません。 グループとグループ メンバーシップに関する情報を永続的に保持する場合は、そのデータをデータベースや Azure テーブル ストレージなどのリポジトリに格納する必要があります。 次に、ユーザーがアプリケーションに接続するたびに、ユーザーが属しているグループのリポジトリから取得し、そのユーザーを手動でそれらのグループに追加します。
一時的な中断後に再接続すると、ユーザーは以前に割り当てられたグループに自動的に再参加します。 グループへの自動的な再参加は、再接続時にのみ適用され、新しい接続を確立する場合には適用されません。 デジタル署名されたトークンは、以前に割り当てられたグループの一覧を含むクライアントから渡されます。 ユーザーが要求されたグループに属しているかどうかを確認する場合は、既定の動作をオーバーライドできます。
このトピックのセクションは次のとおりです。
- ユーザーの追加と削除
- グループのメンバーを呼び出す
- データベースへのグループ メンバーシップの格納
- 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());
}
}
フィードバック
https://aka.ms/ContentUserFeedback」を参照してください。
以下は間もなく提供いたします。2024 年を通じて、コンテンツのフィードバック メカニズムとして GitHub の issue を段階的に廃止し、新しいフィードバック システムに置き換えます。 詳細については、「フィードバックの送信と表示