다음을 통해 공유


SignalR에서 그룹 사용

작성자: Patrick Fletcher, Tom FitzMacken

경고

이 설명서는 최신 버전의 SignalR용이 아닙니다. ASP.NET Core SignalR을 살펴보세요.

이 항목에서는 그룹에 사용자를 추가하고 그룹 멤버 자격 정보를 유지하는 방법을 설명합니다.

이 항목에서 사용되는 소프트웨어 버전

이 항목의 이전 버전

이전 버전의 SignalR에 대한 자세한 내용은 SignalR 이전 버전을 참조하세요.

질문 및 의견

이 자습서를 어떻게 좋아했는지, 그리고 페이지 하단의 주석에서 개선할 수 있는 사항에 대한 피드백을 남겨 주세요. 자습서와 직접 관련이 없는 질문이 있는 경우 ASP.NET SignalR 포럼 또는 StackOverflow.com 게시할 수 있습니다.

개요

SignalR의 그룹은 연결된 클라이언트의 지정된 하위 집합에 메시지를 브로드캐스트하는 방법을 제공합니다. 그룹에는 여러 클라이언트가 있을 수 있으며 클라이언트는 여러 그룹의 구성원일 수 있습니다. 그룹을 명시적으로 만들 필요가 없습니다. 실제로 Groups.Add 호출에서 이름을 처음 지정할 때 그룹이 자동으로 만들어지고 구성원 자격에서 마지막 연결을 제거하면 그룹이 삭제됩니다. 그룹 사용에 대한 소개는 Hubs API - 서버 가이드 의 허브 클래스에서 그룹 멤버 자격을 관리하는 방법을 참조하세요.

그룹 멤버 자격 목록 또는 그룹 목록을 가져오기 위한 API는 없습니다. SignalR은 pub/sub 모델을 기반으로 클라이언트 및 그룹에 메시지를 보내고 서버는 그룹 또는 그룹 멤버 자격 목록을 유지 관리하지 않습니다. 이는 웹 팜에 노드를 추가할 때마다 SignalR이 유지 관리하는 모든 상태를 새 노드로 전파해야 하므로 확장성을 최대화하는 데 도움이 됩니다.

메서드를 사용하여 Groups.Add 그룹에 사용자를 추가하는 경우 사용자는 현재 연결 기간 동안 해당 그룹으로 전달되는 메시지를 수신하지만 해당 그룹의 사용자 멤버 자격은 현재 연결 이상으로 유지되지 않습니다. 그룹 및 그룹 멤버 자격에 대한 정보를 영구적으로 유지하려면 해당 데이터를 데이터베이스 또는 Azure 테이블 스토리지와 같은 리포지토리에 저장해야 합니다. 그런 다음 사용자가 애플리케이션에 연결할 때마다 사용자가 속한 그룹을 리포지토리에서 검색하고 해당 사용자를 해당 그룹에 수동으로 추가합니다.

일시적인 중단 후 다시 연결할 때 사용자는 이전에 할당된 그룹에 자동으로 다시 조인합니다. 그룹을 자동으로 다시 조인하는 것은 새 연결을 설정할 때가 아니라 다시 연결할 때만 적용됩니다. 이전에 할당된 그룹 목록이 포함된 클라이언트에서 디지털 서명된 토큰이 전달됩니다. 사용자가 요청된 그룹에 속하는지 확인하려면 기본 동작을 재정의할 수 있습니다.

이 항목에는 다음 섹션이 포함되어 있습니다.

사용자 추가 및 제거

그룹에서 사용자를 추가하거나 제거하려면 Add 또는 Remove 메서드를 호출하고 사용자의 연결 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.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.");
}

일반적으로 제거하려는 연결 ID를 Groups.Remove 더 이상 사용할 수 없으므로 메서드를 호출할 때 를 포함 await 해서는 안 됩니다. 이 경우 는 TaskCanceledException 요청 시간이 초과된 후 throw됩니다. 애플리케이션이 그룹에 메시지를 보내기 전에 사용자가 그룹에서 제거되었는지 확인해야 하는 경우 앞에 Groups.Remove를 추가 await 한 다음 throw될 수 있는 예외를 TaskCanceledException catch할 수 있습니다.

그룹의 멤버 호출

다음 예제와 같이 그룹의 모든 멤버 또는 그룹의 지정된 멤버에게만 메시지를 보낼 수 있습니다.

  • 지정된 그룹의 연결된 모든 클라이언트입니다.

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