자습서: SignalR 2를 사용한 서버 브로드캐스트

경고

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

이 자습서에서는 ASP.NET SignalR 2를 사용하여 서버 브로드캐스트 기능을 제공하는 웹 애플리케이션을 만드는 방법을 보여줍니다. 서버 브로드캐스트는 서버가 클라이언트로 전송된 통신을 시작한다는 것을 의미합니다.

이 자습서에서 만들 애플리케이션은 서버 브로드캐스트 기능에 대한 일반적인 시나리오인 주식 시세 시뮬레이트합니다. 주기적으로 서버는 임의로 주가를 업데이트하고 연결된 모든 클라이언트에 업데이트를 브로드캐스트합니다. 브라우저에서 변경 및 열의 숫자와 % 기호는 서버의 알림에 대한 응답으로 동적으로 변경됩니다. 동일한 URL에 대한 추가 브라우저를 열면 모두 동일한 데이터와 동일한 변경 내용이 동시에 표시됩니다.

여러 웹 브라우저가 동일한 업데이트된 데이터를 동시에 표시하는 방법을 보여 주는 스크린샷

이 자습서에서는 다음을 수행합니다.

  • 프로젝트 만들기
  • 서버 코드 설정
  • 서버 코드 검사
  • 클라이언트 코드 설정
  • 클라이언트 코드 검사
  • 애플리케이션 테스트
  • 로깅 사용

중요

애플리케이션을 빌드하는 단계를 수행하지 않으려면 새 빈 ASP.NET 웹 애플리케이션 프로젝트에 SignalR.Sample 패키지를 설치할 수 있습니다. 이 자습서의 단계를 수행하지 않고 NuGet 패키지를 설치하는 경우 readme.txt 파일의 지침을 따라야 합니다. 패키지를 실행하려면 설치된 패키지에서 메서드를 호출 ConfigureSignalR 하는 OWIN 시작 클래스를 추가해야 합니다. OWIN 시작 클래스를 추가하지 않으면 오류가 발생합니다. 이 문서의 StockTicker 설치 샘플 섹션을 참조하세요.

사전 요구 사항

프로젝트 만들기

이 섹션에서는 Visual Studio 2017을 사용하여 빈 ASP.NET 웹 애플리케이션을 만드는 방법을 보여줍니다.

  1. Visual Studio에서 ASP.NET 웹 애플리케이션을 만듭니다.

    ASP.NET 웹 애플리케이션을 만드는 방법을 보여 주는 스크린샷

  2. 새 ASP.NET 웹 애플리케이션 - SignalR.StockTicker 창에서 비어 있는 상태로 두고 확인을 선택합니다.

서버 코드 설정

이 섹션에서는 서버에서 실행되는 코드를 설정합니다.

Stock 클래스 만들기

먼저 주식에 대한 정보를 저장하고 전송하는 데 사용할 스톡 모델 클래스를 만듭니다.

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고 클래스 추가> 선택합니다.

  2. 클래스 이름을 Stock 으로 지정하고 프로젝트에 추가합니다.

  3. Stock.cs 파일의 코드를 다음 코드로 바꿉니다.

    using System;
    
    namespace SignalR.StockTicker
    {
        public class Stock
        {
            private decimal _price;
    
            public string Symbol { get; set; }
    
            public decimal Price
            {
                get
                {
                    return _price;
                }
                set
                {
                    if (_price == value)
                    {
                        return;
                    }
    
                    _price = value;
    
                    if (DayOpen == 0)
                    {
                        DayOpen = _price;
                    }
                }
            }
    
            public decimal DayOpen { get; private set; }
    
            public decimal Change
            {
                get
                {
                    return Price - DayOpen;
                }
            }
    
            public double PercentChange
            {
                get
                {
                    return (double)Math.Round(Change / Price, 4);
                }
            }
        }
    }
    

    주식을 만들 때 설정할 두 속성은 (예: Microsoft용 MSFT) 및 Price입니다 Symbol . 다른 속성은 를 설정하는 Price방법과 시기에 따라 달라집니다. 를 처음 설정할 Price때 값이 로 전파됩니다 DayOpen. 그런 다음, 를 설정Price하면 앱은 및 간의 DayOpenPrice 차이에 따라 및 PercentChange 속성 값을 계산합니다Change.

StockTickerHub 및 StockTicker 클래스 만들기

SignalR Hub API를 사용하여 서버-클라이언트 상호 작용을 처리합니다. StockTickerHub SignalR Hub 클래스에서 파생되는 클래스는 클라이언트에서 연결 및 메서드 호출 수신을 처리합니다. 또한 스톡 데이터를 유지하고 개체를 Timer 실행해야 합니다. 개체는 Timer 클라이언트 연결과 관계없이 주기적으로 가격 업데이트를 트리거합니다. 허브는 일시적이므로 이러한 함수를 Hub 클래스에 배치할 수 없습니다. 앱은 클라이언트에서 서버로의 연결 및 호출과 같이 허브의 각 작업에 대한 클래스 instance 만듭니다Hub. 따라서 재고 데이터를 유지하고, 가격을 업데이트하고, 가격 업데이트를 브로드캐스트하는 메커니즘은 별도의 클래스에서 실행해야 합니다. 클래스 StockTicker의 이름을 로 지정합니다.

StockTicker에서 브로드캐스팅

서버에서 클래스의 StockTicker instance 하나만 실행하려면 각 StockTickerHub instance 단일 StockTicker instance 대한 참조를 설정해야 합니다. 클래스는 StockTicker 스톡 데이터가 있고 업데이트를 트리거하지만 StockTicker 클래스가 아니기 때문에 클라이언트에 Hub 브로드캐스트해야 합니다. 클래스는 StockTicker SignalR Hub 연결 컨텍스트 개체에 대한 참조를 가져와야 합니다. 그런 다음 SignalR 연결 컨텍스트 개체를 사용하여 클라이언트에 브로드캐스트할 수 있습니다.

StockTickerHub.cs 만들기

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고새 항목추가>를 선택합니다.

  2. 새 항목 추가 - SignalR.StockTicker에서 설치된>Visual C#>Web>SignalR을 선택한 다음 SignalR Hub 클래스(v2)를 선택합니다.

  3. 클래스 이름을 StockTickerHub로 지정하고 프로젝트에 추가합니다.

    이 단계에서는 StockTickerHub.cs 클래스 파일을 만듭니다. 동시에 SignalR을 지원하는 스크립트 파일 및 어셈블리 참조 집합을 프로젝트에 추가합니다.

  4. StockTickerHub.cs 파일의 코드를 다음 코드로 바꿉니다.

    using System.Collections.Generic;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        [HubName("stockTickerMini")]
        public class StockTickerHub : Hub
        {
            private readonly StockTicker _stockTicker;
    
            public StockTickerHub() : this(StockTicker.Instance) { }
    
            public StockTickerHub(StockTicker stockTicker)
            {
                _stockTicker = stockTicker;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stockTicker.GetAllStocks();
            }
        }
    }
    
  5. 파일을 저장합니다.

앱은 허브 클래스를 사용하여 클라이언트가 서버에서 호출할 수 있는 메서드를 정의합니다. 하나의 메서드 GetAllStocks()를 정의하고 있습니다. 클라이언트가 처음에 서버에 연결하면 이 메서드를 호출하여 현재 가격으로 모든 주식 목록을 가져옵니다. 메서드는 동기적으로 실행되고 메모리에서 데이터를 반환하므로 를 반환 IEnumerable<Stock> 할 수 있습니다.

메서드가 데이터베이스 조회 또는 웹 서비스 호출과 같이 대기와 관련된 작업을 수행하여 데이터를 가져와야 하는 경우 비동기 처리를 사용하도록 설정하는 반환 값으로 를 지정 Task<IEnumerable<Stock>> 합니다. 자세한 내용은 ASP.NET SignalR Hubs API 가이드 - 서버 - 비동기적으로 실행할 시기를 참조하세요.

특성은 HubName 앱이 클라이언트의 JavaScript 코드에서 허브를 참조하는 방법을 지정합니다. 이 특성을 사용하지 않는 경우 클라이언트의 기본 이름은 클래스 이름의 camelCase 버전이며, 이 경우 입니다 stockTickerHub.

나중에 볼 수 있듯이 클래스를 StockTicker 만들 때 앱은 해당 정적 Instance 속성에 해당 클래스의 싱글톤 instance 만듭니다. 의 싱글톤 instance StockTicker 연결 또는 연결 해제 수에 관계없이 메모리에 있습니다. 이 instance 메서드가 GetAllStocks() 현재 주식 정보를 반환하는 데 사용하는 것입니다.

StockTicker.cs 만들기

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고 클래스 추가> 선택합니다.

  2. 클래스 이름을 StockTicker로 지정하고 프로젝트에 추가합니다.

  3. StockTicker.cs 파일의 코드를 다음 코드로 바꿉니다.

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        public class StockTicker
        {
            // Singleton instance
            private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
            private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
            private readonly object _updateStockPricesLock = new object();
    
            //stock can go up or down by a percentage of this factor on each change
            private readonly double _rangePercent = .002;
    
            private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
            private readonly Random _updateOrNotRandom = new Random();
    
            private readonly Timer _timer;
            private volatile bool _updatingStockPrices = false;
    
            private StockTicker(IHubConnectionContext<dynamic> clients)
            {
                Clients = clients;
    
                _stocks.Clear();
                var stocks = new List<Stock>
                {
                    new Stock { Symbol = "MSFT", Price = 30.31m },
                    new Stock { Symbol = "APPL", Price = 578.18m },
                    new Stock { Symbol = "GOOG", Price = 570.30m }
                };
                stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
                _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
            }
    
            public static StockTicker Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
    
            private IHubConnectionContext<dynamic> Clients
            {
                get;
                set;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stocks.Values;
            }
    
            private void UpdateStockPrices(object state)
            {
                lock (_updateStockPricesLock)
                {
                    if (!_updatingStockPrices)
                    {
                        _updatingStockPrices = true;
    
                        foreach (var stock in _stocks.Values)
                        {
                            if (TryUpdateStockPrice(stock))
                            {
                                BroadcastStockPrice(stock);
                            }
                        }
    
                        _updatingStockPrices = false;
                    }
                }
            }
    
            private bool TryUpdateStockPrice(Stock stock)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    return false;
                }
    
                // Update the stock price by a random factor of the range percent
                var random = new Random((int)Math.Floor(stock.Price));
                var percentChange = random.NextDouble() * _rangePercent;
                var pos = random.NextDouble() > .51;
                var change = Math.Round(stock.Price * (decimal)percentChange, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

모든 스레드가 동일한 instance StockTicker 코드를 실행하므로 StockTicker 클래스는 스레드로부터 안전해야 합니다.

서버 코드 검사

서버 코드를 검사하면 앱의 작동 방식을 이해하는 데 도움이 됩니다.

싱글톤 instance 정적 필드에 저장

코드는 클래스의 instance 속성을 백업하는 정적 _instance 필드를 초기화합니다Instance. 생성자는 프라이빗이므로 앱에서 만들 수 있는 클래스의 유일한 instance. 앱은 필드에 지연 초기화를_instance 사용합니다. 성능상의 이유가 아닙니다. instance 만들기가 스레드로부터 안전한지 확인하는 것입니다.

private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

public static StockTicker Instance
{
    get
    {
        return _instance.Value;
    }
}

클라이언트가 서버에 연결할 때마다 별도의 스레드에서 실행되는 StockTickerHub 클래스의 새 instance 클래스의 앞 StockTickerHub 부분에서 보았듯이 정적 속성에서 StockTicker.Instance StockTicker 싱글톤 instance 가져옵니다.

ConcurrentDictionary에 스톡 데이터 저장

생성자는 일부 샘플 주식 데이터를 사용하여 _stocks 컬렉션을 초기화하고 GetAllStocks 주식을 반환합니다. 앞에서 보았듯이 이 주식 컬렉션은 클라이언트가 호출할 수 있는 클래스의 Hub 서버 메서드인 에 의해 StockTickerHub.GetAllStocks반환됩니다.

private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    _stocks.Clear();
    var stocks = new List<Stock>
    {
        new Stock { Symbol = "MSFT", Price = 30.31m },
        new Stock { Symbol = "APPL", Price = 578.18m },
        new Stock { Symbol = "GOOG", Price = 570.30m }
    };
    stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));

    _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
}

public IEnumerable<Stock> GetAllStocks()
{
    return _stocks.Values;
}

주식 컬렉션은 스레드 안전을 위한 ConcurrentDictionary 형식으로 정의됩니다. 또는 Dictionary 개체를 사용하고 변경할 때 사전을 명시적으로 잠글 수 있습니다.

이 샘플 애플리케이션의 경우 애플리케이션 데이터를 메모리에 저장하고 앱이 instance 삭제할 때 데이터를 손실해 StockTicker 도 됩니다. 실제 애플리케이션에서는 데이터베이스와 같은 백 엔드 데이터 저장소로 작업합니다.

주기적으로 주가 업데이트

생성자는 주기적으로 주가를 임의로 Timer 업데이트하는 메서드를 호출하는 개체를 시작합니다.

_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);

private void UpdateStockPrices(object state)
{
    lock (_updateStockPricesLock)
    {
        if (!_updatingStockPrices)
        {
            _updatingStockPrices = true;

            foreach (var stock in _stocks.Values)
            {
                if (TryUpdateStockPrice(stock))
                {
                    BroadcastStockPrice(stock);
                }
            }

            _updatingStockPrices = false;
        }
    }
}

private bool TryUpdateStockPrice(Stock stock)
{
    // Randomly choose whether to update this stock or not
    var r = _updateOrNotRandom.NextDouble();
    if (r > .1)
    {
        return false;
    }

    // Update the stock price by a random factor of the range percent
    var random = new Random((int)Math.Floor(stock.Price));
    var percentChange = random.NextDouble() * _rangePercent;
    var pos = random.NextDouble() > .51;
    var change = Math.Round(stock.Price * (decimal)percentChange, 2);
    change = pos ? change : -change;

    stock.Price += change;
    return true;
}

Timer 은 상태 매개 변수에서 null로 전달되는 를 호출 UpdateStockPrices합니다. 가격을 업데이트하기 전에 앱은 개체에 대한 잠금을 _updateStockPricesLock 사용합니다. 코드는 다른 스레드가 이미 가격을 업데이트하고 있는지 확인한 다음 목록의 각 주식에 대해 를 호출 TryUpdateStockPrice 합니다. 이 TryUpdateStockPrice 방법은 주가를 변경할지 여부와 변경 정도를 결정합니다. 주가가 변경되면 앱은 를 호출 BroadcastStockPrice 하여 연결된 모든 클라이언트에 주가 변경을 브로드캐스트합니다.

_updatingStockPrices 스레드로부터 안전한지 확인하기 위해 휘발성 플래그로 지정되었습니다.

private volatile bool _updatingStockPrices = false;

실제 애플리케이션에서 메서드는 TryUpdateStockPrice 가격을 조회하기 위해 웹 서비스를 호출합니다. 이 코드에서 앱은 난수 생성기를 사용하여 임의로 변경합니다.

StockTicker 클래스가 클라이언트에 브로드캐스트할 수 있도록 SignalR 컨텍스트 가져오기

가격 변경은 여기에서 개체에서 시작되므로 연결된 모든 클라이언트에서 StockTicker 메서드를 updateStockPrice 호출해야 하는 개체입니다. Hub 클래스에는 클라이언트 메서드를 호출하기 위한 API가 있지만 StockTicker 클래스에서 Hub 파생되지 않으며 개체 Hub 에 대한 참조가 없습니다. 연결된 클라이언트에 브로드캐스트하려면 클래스가 StockTicker 클래스에 대한 StockTickerHub SignalR 컨텍스트 instance 가져와서 이를 사용하여 클라이언트에서 메서드를 호출해야 합니다.

코드는 singleton 클래스 instance 만들고, 해당 참조를 생성자에 전달하고, 생성자가 속성에 배치할 때 SignalR 컨텍스트에 대한 참조를 Clients 가져옵니다.

컨텍스트를 한 번만 가져오려는 두 가지 이유가 있습니다. 컨텍스트를 가져오는 것은 비용이 많이 드는 작업이며, 한 번 가져오면 앱이 클라이언트에 전송된 메시지의 의도된 순서를 유지하도록 합니다.

private readonly static Lazy<StockTicker> _instance =
    new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    // Remainder of constructor ...
}

private IHubConnectionContext<dynamic> Clients
{
    get;
    set;
}

private void BroadcastStockPrice(Stock stock)
{
    Clients.All.updateStockPrice(stock);
}

컨텍스트의 Clients 속성을 가져오고 속성에 StockTickerClient 배치하면 클래스와 동일하게 보이는 클라이언트 메서드를 호출하는 코드를 작성할 수 Hub 있습니다. instance 모든 클라이언트에 브로드캐스트하려면 을 작성Clients.All.updateStockPrice(stock)할 수 있습니다.

updateStockPrice 호출 BroadcastStockPrice 하는 메서드가 아직 존재하지 않습니다. 나중에 클라이언트에서 실행되는 코드를 작성할 때 추가합니다. 가 동적이므로 여기서 Clients.All 참조할 updateStockPrice 수 있습니다. 즉, 앱이 런타임에 식을 평가합니다. 이 메서드 호출이 실행되면 SignalR은 메서드 이름과 매개 변수 값을 클라이언트에 보내고 클라이언트에 라는 updateStockPrice메서드가 있는 경우 앱은 해당 메서드를 호출하고 매개 변수 값을 전달합니다.

Clients.All 는 모든 클라이언트에 보내기를 의미합니다. SignalR은 보낼 클라이언트 또는 클라이언트 그룹을 지정하는 다른 옵션을 제공합니다. 자세한 내용은 HubConnectionContext를 참조하세요.

SignalR 경로 등록

서버는 가로채고 SignalR로 직접 보낼 URL을 알고 있어야 합니다. 이렇게 하려면 OWIN 시작 클래스를 추가합니다.

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고새 항목추가>를 선택합니다.

  2. 새 항목 추가 - SignalR.StockTicker에서 설치된>Visual C#>Web을 선택한 다음, OWIN 시작 클래스를 선택합니다.

  3. 클래스 이름을 Startup 으로 지정하고 확인을 선택합니다.

  4. Startup.cs 파일의 기본 코드를 다음 코드로 바꿉니다.

    using System;
    using System.Threading.Tasks;
    using Microsoft.Owin;
    using Owin;
    
    [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
    
    namespace SignalR.StockTicker
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // Any connection or hub wire up and configuration should go here
                app.MapSignalR();
            }
    
        }
    }
    

이제 서버 코드 설정을 완료했습니다. 다음 섹션에서는 클라이언트를 설정합니다.

클라이언트 코드 설정

이 섹션에서는 클라이언트에서 실행되는 코드를 설정합니다.

HTML 페이지 및 JavaScript 파일 만들기

HTML 페이지에 데이터가 표시되고 JavaScript 파일이 데이터를 구성합니다.

StockTicker.html 만들기

먼저 HTML 클라이언트를 추가합니다.

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고HTML 페이지추가>를 선택합니다.

  2. 파일 이름을 StockTicker로 지정하고 확인을 선택합니다.

  3. StockTicker.html 파일의 기본 코드를 다음 코드로 바꿉다.

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ASP.NET SignalR Stock Ticker</title>
        <style>
            body {
                font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
                font-size: 16px;
            }
            #stockTable table {
                border-collapse: collapse;
            }
                #stockTable table th, #stockTable table td {
                    padding: 2px 6px;
                }
                #stockTable table td {
                    text-align: right;
                }
            #stockTable .loading td {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <h1>ASP.NET SignalR Stock Ticker Sample</h1>
    
        <h2>Live Stock Table</h2>
        <div id="stockTable">
            <table border="1">
                <thead>
                    <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
                </thead>
                <tbody>
                    <tr class="loading"><td colspan="5">loading...</td></tr>
                </tbody>
            </table>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="/Scripts/jquery-1.10.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-2.1.0.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="/signalr/hubs"></script>
        <!--Reference the StockTicker script. -->
        <script src="StockTicker.js"></script>
    </body>
    </html>
    

    HTML은 5개의 열, 머리글 행 및 5개 열 모두에 걸쳐 있는 단일 셀이 있는 데이터 행이 있는 테이블을 만듭니다. 데이터 행에 "로드 중..."이 표시됩니다. 잠시 앱이 시작될 때 JavaScript 코드는 해당 행을 제거하고 서버에서 검색된 스톡 데이터를 사용하여 해당 위치 행에 추가합니다.

    스크립트 태그는 다음을 지정합니다.

    • jQuery 스크립트 파일입니다.

    • SignalR 핵심 스크립트 파일입니다.

    • SignalR 프록시 스크립트 파일입니다.

    • 나중에 만들 StockTicker 스크립트 파일입니다.

    앱은 SignalR 프록시 스크립트 파일을 동적으로 생성합니다. "/signalr/hubs" URL을 지정하고 허브 클래스의 메서드에 대한 프록시 메서드(이 경우)를 StockTickerHub.GetAllStocks정의합니다. 원하는 경우 SignalR 유틸리티를 사용하여 이 JavaScript 파일을 수동으로 생성할 수 있습니다. 메서드 호출에서 동적 파일 생성을 MapHubs 사용하지 않도록 설정하는 것을 잊지 마세요.

  4. 솔루션 탐색기스크립트를 확장합니다.

    jQuery 및 SignalR에 대한 스크립트 라이브러리는 프로젝트에 표시됩니다.

    중요

    패키지 관리자는 이후 버전의 SignalR 스크립트를 설치합니다.

  5. 프로젝트의 스크립트 파일 버전에 맞게 코드 블록의 스크립트 참조를 업데이트합니다.

  6. 솔루션 탐색기StockTicker.html마우스 오른쪽 단추로 클릭한 다음 시작 페이지로 설정을 선택합니다.

StockTicker.js 만들기

이제 JavaScript 파일을 만듭니다.

  1. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고JavaScript 파일추가>를 선택합니다.

  2. 파일 이름을 StockTicker로 지정하고 확인을 선택합니다.

  3. 이 코드를StockTicker.js 파일에 추가합니다.

    // A simple templating method for replacing placeholders enclosed in curly braces.
    if (!String.prototype.supplant) {
        String.prototype.supplant = function (o) {
            return this.replace(/{([^{}]*)}/g,
                function (a, b) {
                    var r = o[b];
                    return typeof r === 'string' || typeof r === 'number' ? r : a;
                }
            );
        };
    }
    
    $(function () {
    
        var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
            up = '▲',
            down = '▼',
            $stockTable = $('#stockTable'),
            $stockTableBody = $stockTable.find('tbody'),
            rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
    
        function formatStock(stock) {
            return $.extend(stock, {
                Price: stock.Price.toFixed(2),
                PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
                Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
            });
        }
    
        function init() {
            ticker.server.getAllStocks().done(function (stocks) {
                $stockTableBody.empty();
                $.each(stocks, function () {
                    var stock = formatStock(this);
                    $stockTableBody.append(rowTemplate.supplant(stock));
                });
            });
        }
    
        // Add a client-side hub method that the server will call
        ticker.client.updateStockPrice = function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock));
    
            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            }
    
        // Start the connection
        $.connection.hub.start().done(init);
    
    });
    

클라이언트 코드 검사

클라이언트 코드를 검사하는 경우 클라이언트 코드가 서버 코드와 상호 작용하여 앱을 작동하는 방법을 알아보는 데 도움이 됩니다.

연결 시작

$.connection 는 SignalR 프록시를 나타냅니다. 코드는 클래스에 대한 프록시에 대한 참조를 StockTickerHub 가져오고 변수에 ticker 넣습니다. 프록시 이름은 특성에 의해 HubName 설정된 이름입니다.

var ticker = $.connection.stockTickerMini
[HubName("stockTickerMini")]
public class StockTickerHub : Hub

모든 변수와 함수를 정의한 후 파일의 마지막 코드 줄은 SignalR 함수를 호출하여 SignalR start 연결을 초기화합니다. 함수는 start 비동기적으로 실행되고 jQuery Deferred 개체를 반환합니다. 완료 함수를 호출하여 앱이 비동기 작업을 완료할 때 호출할 함수를 지정할 수 있습니다.

$.connection.hub.start().done(init);

모든 주식 가져오기

함수는 init 서버에서 getAllStocks 함수를 호출하고 서버가 반환하는 정보를 사용하여 스톡 테이블을 업데이트합니다. 기본적으로 서버에서 메서드 이름이 pascal-cased인 경우에도 클라이언트에서 camelCasing을 사용해야 합니다. camelCasing 규칙은 개체가 아닌 메서드에만 적용됩니다. 예를 들어 또는 가 아닌 stock.symbolstock.pricestock.Pricestock.Symbol 참조합니다.

function init() {
    ticker.server.getAllStocks().done(function (stocks) {
        $stockTableBody.empty();
        $.each(stocks, function () {
            var stock = formatStock(this);
            $stockTableBody.append(rowTemplate.supplant(stock));
        });
    });
}
public IEnumerable<Stock> GetAllStocks()
{
    return _stockTicker.GetAllStocks();
}

init 메서드에서 앱은 를 호출하여 개체의 속성 서식을 지정한 다음, 를 호출 formatStocksupplant 하여 변수의 자리 표시자를 개체 속성 값으로 stock 바꿔 서버에서 받은 각 스톡 개체의 stock 테이블 행에 rowTemplate 대한 HTML을 만듭니다. 그런 다음 결과 HTML이 스톡 테이블에 추가됩니다.

참고

비동 start 기 함수가 완료된 후 실행되는 함수로 callback 전달하여 를 호출 init 합니다. 를 호출start한 후 별도의 JavaScript 문으로 호출 init 하면 시작 함수가 연결 설정을 완료할 때까지 기다리지 않고 즉시 실행되므로 함수가 실패합니다. 이 경우 함수는 앱이 initgetAllStocks 서버 연결을 설정하기 전에 함수를 호출하려고 합니다.

업데이트된 주가 가져오기

서버가 주식의 가격을 변경하면 연결된 클라이언트에서 를 updateStockPrice 호출합니다. 앱은 프록시의 클라이언트 속성에 함수를 stockTicker 추가하여 서버의 호출에 사용할 수 있도록 합니다.

ticker.client.updateStockPrice = function (stock) {
    var displayStock = formatStock(stock),
        $row = $(rowTemplate.supplant(displayStock));

    $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
        .replaceWith($row);
    }

함수는 updateStockPrice 서버에서 받은 스톡 개체의 형식을 함수와 동일한 방식으로 init 테이블 행으로 지정합니다. 테이블에 행을 추가하는 대신 테이블에서 주식의 현재 행을 찾아 해당 행을 새 행으로 바꿉니다.

애플리케이션 테스트

앱을 테스트하여 작동하는지 확인할 수 있습니다. 모든 브라우저 창에 주가 변동이 있는 라이브 주식 테이블이 표시됩니다.

  1. 도구 모음에서 스크립트 디버깅을 켜고 재생 단추를 선택하여 디버그 모드에서 앱을 실행합니다.

    사용자가 디버깅 모드를 켜고 재생을 선택하는 스크린샷

    라이브 스톡 테이블이 표시된 브라우저 창이 열립니다. 주식 테이블은 처음에 "로드 중..."을 표시합니다. 그런 다음 짧은 시간 후에 앱이 초기 주식 데이터를 표시한 다음 주가가 변경하기 시작합니다.

  2. 브라우저에서 URL을 복사하고 다른 두 브라우저를 열고 URL을 주소 표시줄에 붙여넣습니다.

    초기 스톡 디스플레이는 첫 번째 브라우저와 동일하며 변경 내용이 동시에 발생합니다.

  3. 모든 브라우저를 닫고, 새 브라우저를 열고, 동일한 URL로 이동합니다.

    StockTicker 싱글톤 개체는 서버에서 계속 실행되었습니다. 라이브 스톡 테이블은 주식이 계속 변화하고 있음을 보여줍니다. 변경 그림이 없는 초기 테이블은 표시되지 않습니다.

  4. 브라우저를 닫습니다.

로깅 사용

SignalR에는 클라이언트에서 문제 해결에 도움이 되도록 설정할 수 있는 기본 제공 로깅 함수가 있습니다. 이 섹션에서는 로깅을 사용하도록 설정하고 로그가 다음 중 SignalR에서 사용하는 전송 방법을 알려주는 방법을 보여 주는 예제를 확인합니다.

지정된 연결에 대해 SignalR은 서버와 클라이언트가 모두 지원하는 최상의 전송 방법을 선택합니다.

  1. StockTicker.js엽니다.

  2. 이 강조 표시된 코드 줄을 추가하여 파일 끝에 있는 연결을 초기화하는 코드 바로 앞에 로깅을 사용하도록 설정합니다.

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  3. F5 키를 눌러 프로젝트를 실행합니다.

  4. 브라우저의 개발자 도구 창을 열고 콘솔을 선택하여 로그를 확인합니다. 새 연결에 대한 전송 방법을 협상하는 SignalR의 로그를 보려면 페이지를 새로 고쳐야 할 수 있습니다.

    • Windows 8(IIS 8)에서 인터넷 Explorer 10을 실행하는 경우 전송 방법은 WebSockets입니다.

    • Windows 7(IIS 7.5)에서 인터넷 Explorer 10을 실행하는 경우 전송 방법은 iframe입니다.

    • Windows 8(IIS 8)에서 Firefox 19를 실행하는 경우 전송 방법은 WebSockets입니다.

      Firefox에서 Firebug 추가 기능을 설치하여 콘솔 창을 가져옵니다.

    • Windows 7(IIS 7.5)에서 Firefox 19를 실행하는 경우 전송 방법은 서버에서 보낸 이벤트입니다.

StockTicker 샘플 설치

Microsoft.AspNet.SignalR.Sample은 StockTicker 애플리케이션을 설치합니다. NuGet 패키지에는 처음부터 만든 간소화된 버전보다 더 많은 기능이 포함되어 있습니다. 자습서의 이 섹션에서는 NuGet 패키지를 설치하고 새 기능과 이를 구현하는 코드를 검토합니다.

중요

이 자습서의 이전 단계를 수행하지 않고 패키지를 설치하는 경우 프로젝트에 OWIN 시작 클래스를 추가해야 합니다. NuGet 패키지에 대한 이 readme.txt 파일은 이 단계를 설명합니다.

SignalR.Sample NuGet 패키지 설치

  1. 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리를 선택합니다.

  2. NuGet 패키지 관리자: SignalR.StockTicker에서 찾아보기를 선택합니다.

  3. 패키지 원본에서 nuget.org 선택합니다.

  4. 검색 상자에 SignalR.Sample 을 입력하고 Microsoft.AspNet.SignalR.Sample>설치를 선택합니다.

  5. 솔루션 탐색기SignalR.Sample 폴더를 확장합니다.

    SignalR.Sample 패키지를 설치하면 폴더와 해당 콘텐츠가 생성되었습니다.

  6. SignalR.Sample 폴더에서StockTicker.html마우스 오른쪽 단추로 클릭한 다음 시작 페이지로 설정을 선택합니다.

    참고

    SignalR.Sample NuGet 패키지를 설치하면 Scripts 폴더에 있는 jQuery 버전이 변경될 수 있습니다. 패키지가 SignalR.Sample 폴더에 설치하는 새 StockTicker.html 파일은 패키지가 설치하는 jQuery 버전과 동기화되지만 원래 StockTicker.html 파일을 다시 실행하려면 먼저 스크립트 태그에서 jQuery 참조를 업데이트해야 할 수 있습니다.

애플리케이션 실행

첫 번째 앱에서 본 테이블에는 유용한 기능이 있습니다. 전체 주식 시세 애플리케이션은 새로운 기능을 보여줍니다: 주식 데이터와 주식이 상승하고 하락할 때 색을 변경하는 주식을 보여주는 수평 스크롤 창입니다.

  1. F5 키를 눌러 앱을 실행합니다.

    앱을 처음 실행하면 "market"이 "닫힘"으로 표시되고 스크롤되지 않는 정적 테이블과 시세 창이 표시됩니다.

  2. 시장 열기를 선택합니다.

    라이브 시세 표시 스크린샷

    • 라이브 주식 시세 상자가 가로로 스크롤하기 시작하고 서버가 주기적으로 주식 가격 변동을 임의로 브로드캐스트하기 시작합니다.

    • 주가가 변경 될 때마다 앱은 Live Stock TableLive Stock Ticker를 모두 업데이트합니다.

    • 주식의 가격 변동이 긍정적이면 앱은 녹색 배경의 주식을 표시합니다.

    • 변경 내용이 음수이면 앱에 빨간색 배경이 있는 주식이 표시됩니다.

  3. 시장 닫기를 선택합니다.

    • 테이블 업데이트가 중지됩니다.

    • 시세는 스크롤을 중지합니다.

  4. 재설정을 선택합니다.

    • 모든 주식 데이터가 다시 설정됩니다.

    • 앱은 가격 변경이 시작되기 전에 초기 상태를 복원합니다.

  5. 브라우저에서 URL을 복사하고 다른 두 브라우저를 열고 URL을 주소 표시줄에 붙여넣습니다.

  6. 각 브라우저에서 동일한 데이터가 동시에 동적으로 업데이트됩니다.

  7. 컨트롤을 선택하면 모든 브라우저가 동시에 동일한 방식으로 응답합니다.

Live Stock Ticker 디스플레이

Live Stock Ticker 디스플레이는 CSS 스타일별로 <div> 한 줄로 서식이 지정된 요소의 순서가 지정되지 않은 목록입니다. 앱은 템플릿 문자열에서 자리 표시자를 <li> 바꾸고 요소를 <ul> 요소에 동적으로 추가하여 <li> 테이블과 동일한 방식으로 시세를 초기화하고 업데이트합니다. 앱에는 jQuery animate 함수를 사용하여 내에서 순서가 지정되지 않은 목록의 왼쪽 여백을 변경하여 스크롤하는 것이 <div>포함됩니다.

SignalR.Sample StockTicker.html

주식 시세 HTML 코드:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

SignalR.Sample StockTicker.css

주식 시세 CSS 코드:

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
    }

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

SignalR.Sample SignalR.StockTicker.js

스크롤을 만드는 jQuery 코드:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

클라이언트가 호출할 수 있는 서버의 추가 메서드

앱에 유연성을 추가하기 위해 앱에서 호출할 수 있는 추가 메서드가 있습니다.

SignalR.Sample StockTickerHub.cs

클래스는 StockTickerHub 클라이언트가 호출할 수 있는 네 가지 추가 메서드를 정의합니다.

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

앱은 페이지 맨 위에 있는 단추에 대한 응답으로 , CloseMarketReset 를 호출OpenMarket합니다. 모든 클라이언트에 즉시 전파되는 상태 변경을 트리거하는 한 클라이언트의 패턴을 보여 줍니다. 이러한 각 메서드는 클래스에서 StockTicker 시장 상태 변경을 유발하는 메서드를 호출한 다음 새 상태를 브로드캐스트합니다.

SignalR.Sample StockTicker.cs

클래스에서 StockTicker 앱은 열거형 값을 반환 MarketState 하는 속성을 사용하여 MarketState 시장의 상태를 유지 관리합니다.

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

시장 상태를 변경하는 각 메서드는 클래스가 스레드로부터 안전해야 하므로 잠금 블록 StockTicker 내에서 이 작업을 수행합니다.

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

이 코드가 스레드로부터 안전한 _marketState 지 확인하려면 지정된 volatile속성을 백업하는 MarketState 필드입니다.

private volatile MarketState _marketState;

및 메서드는 BroadcastMarketStateChange 클라이언트에 정의된 다른 메서드를 호출한다는 점을 제외하고 이미 본 BroadcastStockPrice 메서드와 BroadcastMarketReset 비슷합니다.

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

서버에서 호출할 수 있는 클라이언트의 추가 함수

이제 함수는 updateStockPrice 테이블과 시세 표시를 모두 처리하고 를 사용하여 jQuery.Color 빨간색과 녹색을 깜박입니다.

SignalR.StockTicker.js 새로운 함수는 시장 상태에 따라 단추를 사용하거나 사용하지 않도록 설정합니다. 또한 Live Stock Ticker 가로 스크롤을 중지하거나 시작합니다. 많은 함수가 에 ticker.client추가되므로 앱은 jQuery extend 함수 를 사용하여 함수를 추가합니다.

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
            .replaceWith($li);

        $row.flash(bg, 1000);
        $li.flash(bg, 1000);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

연결을 설정한 후 추가 클라이언트 설정

클라이언트가 연결을 설정한 후 수행할 몇 가지 추가 작업이 있습니다.

  • 적절한 또는 marketClosed 함수를 호출 marketOpened 하기 위해 시장이 열려 있는지 또는 닫혀 있는지 확인합니다.

  • 서버 메서드 호출을 단추에 연결합니다.

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
    });

서버 메서드는 앱이 연결을 설정할 때까지 단추에 연결되지 않습니다. 따라서 코드를 사용할 수 있기 전에 서버 메서드를 호출할 수 없습니다.

추가 리소스

이 자습서에서는 서버에서 연결된 모든 클라이언트로 메시지를 브로드캐스트하는 SignalR 애플리케이션을 프로그래밍하는 방법을 알아보았습니다. 이제 모든 클라이언트의 알림에 대한 응답으로 메시지를 주기적으로 브로드캐스트할 수 있습니다. 다중 스레드 싱글톤 instance 개념을 사용하여 다중 플레이어 온라인 게임 시나리오에서 서버 상태를 유지할 수 있습니다. 예를 들어 SignalR 기반의 ShootR 게임을 참조하세요.

피어 투 피어 통신 시나리오를 보여 주는 자습서는 SignalR을 사용한 시작SignalR을 사용한 실시간 업데이트를 참조하세요.

SignalR에 대한 자세한 내용은 다음 리소스를 참조하세요.

다음 단계

이 자습서에서는 다음을 수행합니다.

  • 프로젝트를 만들었습니다.
  • 서버 코드 설정
  • 서버 코드 검사
  • 클라이언트 코드 설정
  • 클라이언트 코드 검사
  • 애플리케이션 테스트
  • 사용 로깅

다음 문서로 이동하여 ASP.NET SignalR 2를 사용하는 실시간 웹 애플리케이션을 만드는 방법을 알아봅니다.