SignalR でグループを使用する
作成者: Patrick Fletcher、Tom FitzMacken
警告
このドキュメントは、最新版の SignalR に対するものではありません。 ASP.NET Core SignalR に関する記事を参照してください。
このトピックでは、ユーザーをグループに追加し、グループ メンバーシップ情報を保持する方法について説明します。
このトピックで使用されるソフトウェアのバージョン
- Visual Studio 2013
- .NET 4.5
- SignalR バージョン 2
このトピックの以前のバージョン
SignalR の以前のバージョンの詳細については、「SignalR の以前のバージョン」を参照してください。
質問とコメント
このチュートリアルの感想、改善に関するフィードバックをページの下部にあるコメント欄にお寄せください。 チュートリアルに直接関連しない質問がある場合は、ASP.NET SignalR フォーラムまたは StackOverflow.com に投稿できます。
概要
SignalR のグループは、接続されたクライアントの指定されたサブセットにメッセージをブロードキャストする方法を提供します。 グループには任意の数のクライアントを含めることができ、クライアントは任意の数のグループのメンバーにすることができます。 グループを明示的に作成する必要はありません。 実際には、Groups.Add の呼び出しで初めて名前を指定すると、グループは自動的に作成され、最後の接続をメンバーシップから外すと削除されます。 グループの使用の概要については、「ハブ API - サーバー ガイド」の「ハブ クラスからグループ メンバーシップを管理する方法」を参照してください。
グループ メンバーシップ一覧またはグループの一覧を取得するための API はありません。 SignalR は、pub/sub モデルに基づいてクライアントとグループにメッセージを送信します。サーバーはグループやグループ メンバーシップの一覧を維持しません。 Web ファームにノードを追加するたびに、SignalR が維持する状態を新しいノードに伝達する必要があるため、これは、スケーラビリティを最大化するのに役立ちます。
Groups.Add
メソッドを使用してグループにユーザーを追加すると、そのユーザーは現在の接続の間、そのグループに送信されたメッセージを受信しますが、そのグループにおけるユーザーのメンバーシップが現在の接続を超えて永続化されることはありません。 グループとグループ メンバーシップに関する情報を永続的に保持する場合は、そのデータをデータベースや Azure Table Storage などのリポジトリに格納する必要があります。 次に、ユーザーがアプリケーションに接続するたびに、ユーザーがどのグループに属しているかをリポジトリから取得し、ユーザーを手動でそれらのグループに追加します。
一時的な中断後に再接続すると、ユーザーは以前に割り当てられたグループに自動的に再参加します。 グループへの自動再参加は、再接続時にのみ適用され、新しい接続を確立する場合には適用されません。 デジタル署名されたトークンがクライアントから渡され、そのトークンには以前に割り当てられたグループのリストが含まれています。 ユーザーが要求のあったグループに属しているかどうかを確認したい場合は、既定の動作をオーバーライドします。
このトピックのセクションは次のとおりです。
- ユーザーの追加と削除
- グループ メンバーの呼び出し
- データベースへのグループ メンバーシップの格納
- Azure Table Storage へのグループ メンバーシップの格納
- 再接続時のグループ メンバーシップの確認
ユーザーの追加と削除
グループに対してユーザーを追加または削除するには、追加メソッドまたは削除メソッドを呼び出し、ユーザーの接続 ID とグループの名前をパラメーターとして渡します。 接続が終了したときに、グループからユーザーを手動で削除する必要はありません。
次の例は、ハブ メソッドで使用される Groups.Add
メソッドと 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.Add
メソッドと Groups.Remove
メソッドは非同期で実行されます。
クライアントをグループに追加し、そのグループを使ってクライアントにメッセージをすぐに送信する場合は、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
メソッドを呼び出すときは await
を含めないようにします。削除しようとしている接続 ID が今後使用できなくなる可能性があるためです。 その場合、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);
データベースへのグループ メンバーシップの格納
次の例は、グループとユーザー情報を 1 つのデータベースに保持する方法を示しています。 任意のデータ アクセス テクノロジを使用できます。ただし、次の例は、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
キーワードは Groups.Add
の前に適用されません。 新しいメンバーを追加した直後にグループのすべてのメンバーにメッセージを送信する場合は、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 Table Storage へのグループ メンバーシップの格納
Azure Table Storage を使用してグループとユーザー情報を格納することは、データベースを使用することと似ています。 次の例は、ユーザー名とグループ名を格納するテーブル エンティティを示しています。
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());
}
}