SignalR 1.x에서 SignalR 사용자를 연결에 매핑
작성자: Patrick Fletcher, Tom FitzMacken
경고
이 설명서는 최신 버전의 SignalR용이 아닙니다. ASP.NET Core SignalR을 살펴보세요.
이 항목에서는 사용자 및 해당 연결에 대한 정보를 유지하는 방법을 보여 줍니다.
소개
허브에 연결하는 각 클라이언트는 고유한 연결 ID를 전달합니다. 허브 컨텍스트의 속성에서 Context.ConnectionId
이 값을 검색할 수 있습니다. 애플리케이션이 사용자를 연결 ID에 매핑하고 해당 매핑을 유지해야 하는 경우 다음 중 하나를 사용할 수 있습니다.
- 사전과 같은 메모리 내 스토리지
- 각 사용자에 대한 SignalR 그룹
- 데이터베이스 테이블 또는 Azure 테이블 스토리지와 같은 영구 외부 스토리지
이러한 각 구현은 이 항목에 나와 있습니다. 클래스의 OnConnected
, OnDisconnected
및 OnReconnected
메서드를 Hub
사용하여 사용자 연결 상태 추적합니다.
애플리케이션에 가장 적합한 방법은 다음에 따라 다릅니다.
- 애플리케이션을 호스트하는 웹 서버의 수입니다.
- 현재 연결된 사용자 목록을 가져와야 하는지 여부입니다.
- 애플리케이션 또는 서버를 다시 시작할 때 그룹 및 사용자 정보를 유지해야 하는지 여부입니다.
- 외부 서버를 호출하는 대기 시간이 문제인지 여부입니다.
다음 표에서는 이러한 고려 사항에 적합한 방법을 보여 줍니다.
고려 사항 | 둘 이상의 서버 | 현재 연결된 사용자 목록 가져오기 | 다시 시작한 후 정보 유지 | 최적의 성능 |
---|---|---|---|---|
메모리 내 | ||||
단일 사용자 그룹 | ||||
영구, 외부 |
메모리 스토리지 내
다음 예제에서는 메모리에 저장된 사전에서 연결 및 사용자 정보를 유지하는 방법을 보여 줍니다. 사전은 를 HashSet
사용하여 연결 ID를 저장합니다. 언제든지 사용자가 SignalR 애플리케이션에 둘 이상의 연결을 가질 수 있습니다. 예를 들어 여러 디바이스 또는 둘 이상의 브라우저 탭을 통해 연결된 사용자는 둘 이상의 연결 ID를 갖습니다.
애플리케이션이 종료되면 모든 정보가 손실되지만 사용자가 연결을 다시 설정하면 다시 채워집니다. 각 서버에는 별도의 연결 컬렉션이 있기 때문에 환경에 둘 이상의 웹 서버가 포함된 경우 메모리 내 스토리지가 작동하지 않습니다.
첫 번째 예제에서는 연결에 대한 사용자 매핑을 관리하는 클래스를 보여 줍니다. 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);
}
}
}
}
}
}
다음 예제에서는 허브에서 연결 매핑 클래스를 사용하는 방법을 보여 줍니다. 클래스의 instance 변수 이름 _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();
}
}
}
단일 사용자 그룹
각 사용자에 대한 그룹을 만든 다음 해당 사용자에게만 도달하려는 경우 해당 그룹에 메시지를 보낼 수 있습니다. 각 그룹의 이름은 사용자의 이름입니다. 사용자에게 둘 이상의 연결이 있는 경우 각 연결 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 Table Storage를 사용하여 연결 정보를 저장하는 방법을 보여 줍니다. 이 방법은 각 웹 서버가 동일한 데이터 리포지토리와 상호 작용할 수 있으므로 여러 웹 서버가 있는 경우에 작동합니다. 웹 서버의 작동이 중지되거나 애플리케이션이 다시 시작되면 메서드가 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()
{
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 Table Storage 서비스를 시작하는 데 필요한 모든 정보는 포함되지 않습니다. 자세한 내용은 .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()
{
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");
}
}
}