자습서: ASP.NET SignalR 1.x를 사용하여 서버 브로드캐스트

작성자: Patrick Fletcher, Tom Dykstra

경고

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

이 자습서에서는 ASP.NET SignalR을 사용하여 서버 브로드캐스트 기능을 제공하는 웹 애플리케이션을 만드는 방법을 보여줍니다. 서버 브로드캐스트는 클라이언트로 전송된 통신이 서버에서 시작됨을 의미합니다. 이 시나리오에는 하나 이상의 클라이언트에서 클라이언트로 전송되는 통신이 시작되는 채팅 애플리케이션과 같은 피어 투 피어 시나리오와는 다른 프로그래밍 접근 방식이 필요합니다.

이 자습서에서 만들 애플리케이션은 서버 브로드캐스트 기능에 대한 일반적인 시나리오인 주식 시세 시뮬레이트합니다.

자습서에 대한 주석은 환영합니다. 자습서와 직접 관련이 없는 질문이 있는 경우 ASP.NET SignalR 포럼 또는 StackOverflow.com 게시할 수 있습니다.

개요

Microsoft.AspNet.SignalR.Sample NuGet 패키지는 Visual Studio 프로젝트에 시뮬레이트된 샘플 주식 시세 애플리케이션을 설치합니다. 이 자습서의 첫 번째 부분에서는 처음부터 해당 애플리케이션의 간소화된 버전을 만듭니다. 자습서의 나머지 부분에서는 NuGet 패키지를 설치하고 만든 추가 기능 및 코드를 검토합니다.

주식 시세 애플리케이션은 서버에서 연결된 모든 클라이언트에 대한 알림을 주기적으로 "푸시" 또는 브로드캐스트하려는 일종의 실시간 애플리케이션을 대표합니다.

이 자습서의 첫 번째 부분에서 빌드할 애플리케이션은 스톡 데이터가 있는 그리드를 표시합니다.

StockTicker 초기 버전

서버는 주기적으로 주가를 임의로 업데이트하고 연결된 모든 클라이언트에 업데이트를 푸시합니다. 브라우저에서 변경 및 열의 숫자와 % 기호는 서버의 알림에 대한 응답으로 동적으로 변경됩니다. 동일한 URL에 대한 추가 브라우저를 열면 모두 동일한 데이터와 동일한 변경 내용이 동시에 표시됩니다.

이 자습서에는 다음 섹션이 포함되어 있습니다.

참고

애플리케이션을 빌드하는 단계를 수행하지 않으려면 새 빈 ASP.NET 웹 애플리케이션 프로젝트에 SignalR.Sample 패키지를 설치하고 다음 단계를 읽어 코드에 대한 설명을 확인할 수 있습니다. 자습서의 첫 번째 부분에서는 SignalR.Sample 코드의 하위 집합에 대해 설명하고, 두 번째 부분에서는 SignalR.Sample 패키지의 추가 기능의 주요 기능을 설명합니다.

사전 요구 사항

시작하기 전에 컴퓨터에 Visual Studio 2012 또는 2010 SP1이 설치되어 있는지 확인합니다. Visual Studio가 없는 경우 ASP.NET 다운로드 를 참조하여 무료 Visual Studio 2012 Express for Web을 다운로드하세요.

Visual Studio 2010이 있는 경우 NuGet 이 설치되어 있는지 확인합니다.

프로젝트 만들기

  1. 파일 메뉴에서 새 프로젝트를 클릭합니다.

  2. 새 프로젝트 대화 상자의 템플릿 아래에서 C#을 확장하고 선택합니다.

  3. ASP.NET 빈 웹 애플리케이션 템플릿을 선택하고 프로젝트 이름을 SignalR.StockTicker로 지정하고 확인을 클릭합니다.

    새 프로젝트 대화 상자

SignalR NuGet 패키지 추가

SignalR 및 JQuery NuGet 패키지 추가

NuGet 패키지를 설치하여 프로젝트에 SignalR 기능을 추가할 수 있습니다.

  1. 도구 | 클릭 NuGet 패키지 관리자 | 패키지 관리자 콘솔.

  2. 패키지 관리자에서 다음 명령을 입력합니다.

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    SignalR 패키지는 여러 다른 NuGet 패키지를 종속성으로 설치합니다. 설치가 완료되면 ASP.NET 애플리케이션에서 SignalR을 사용하는 데 필요한 모든 서버 및 클라이언트 구성 요소가 있습니다.

서버 코드 설정

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

Stock 클래스 만들기

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

  1. 프로젝트 폴더에 새 클래스 파일을 만들고 이름을 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)와 가격입니다. 다른 속성은 가격을 설정하는 방법과 시기에 따라 달라집니다. 가격을 처음 설정하면 값이 DayOpen으로 전파됩니다. 이후 가격을 설정하면 Price와 DayOpen 간의 차이에 따라 Change 및 PercentChange 속성 값이 계산됩니다.

StockTicker 및 StockTickerHub 클래스 만들기

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

StockTicker에서 브로드캐스팅

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

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

  2. ASP.NET 및 Web Tools 2012.2 업데이트가 포함된 Visual Studio 2012가 있는 경우 Visual C# 아래의 을 클릭하고 SignalR Hub 클래스 항목 템플릿을 선택합니다. 그렇지 않으면 클래스 템플릿을 선택합니다.

  3. 새 클래스의 이름을 StockTickerHub.cs로 지정하고 추가를 클릭합니다.

    StockTickerHub.cs 추가

  4. 템플릿 코드를 다음 코드로 바꿉니다.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    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();
            }
        }
    }
    

    허브 클래스는 클라이언트가 서버에서 호출할 수 있는 메서드를 정의하는 데 사용됩니다. 하나의 메서드 GetAllStocks()를 정의하고 있습니다. 클라이언트가 처음에 서버에 연결하면 이 메서드를 호출하여 현재 가격으로 모든 주식 목록을 가져옵니다. 메서드는 동기적으로 실행하고 메모리에서 데이터를 반환하기 때문에 를 반환 IEnumerable<Stock> 할 수 있습니다. 메서드가 데이터베이스 조회 또는 웹 서비스 호출과 같이 대기와 관련된 작업을 수행하여 데이터를 가져와야 하는 경우 비동기 처리를 사용하도록 설정하는 반환 값으로 를 지정 Task<IEnumerable<Stock>> 합니다. 자세한 내용은 ASP.NET SignalR Hubs API 가이드 - 서버 - 비동기적으로 실행할 시기를 참조하세요.

    HubName 특성은 클라이언트의 JavaScript 코드에서 허브를 참조하는 방법을 지정합니다. 이 특성을 사용하지 않는 경우 클라이언트의 기본 이름은 클래스 이름의 카멜 대/소문자 버전입니다. 이 경우 stockTickerHub가 됩니다.

    나중에 StockTicker 클래스를 만들 때 볼 수 있듯이 해당 클래스의 싱글톤 instance 정적 Instance 속성에 만들어집니다. StockTicker의 싱글톤 instance 연결 또는 연결 끊김의 수에 관계없이 메모리에 남아 있으며 GetAllStocks 메서드가 현재 주식 정보를 반환하는 데 사용하는 instance.

  5. 프로젝트 폴더에 새 클래스 파일을 만들고 이름을 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 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 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 클래스는 threadsafe여야 합니다.

    싱글톤 instance 정적 필드에 저장

    이 코드는 클래스의 instance 사용하여 Instance 속성을 백업하는 정적 _instance 필드를 초기화하며 생성자가 private으로 표시되어 있으므로 만들 수 있는 클래스의 유일한 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 StockTicker.Instance 정적 속성에서 StockTicker 싱글톤 instance 가져옵니다.

    ConcurrentDictionary에 스톡 데이터 저장

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

    private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
    private StockTicker(IHubConnectionContext 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 개체를 사용하고 변경할 때 사전을 명시적으로 잠글 수 있습니다.

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

    주기적으로 주가 업데이트

    생성자는 주기적으로 주가를 임의로 업데이트하는 메서드를 호출하는 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;
    }
    

    UpdateStockPrices는 상태 매개 변수에서 null로 전달되는 타이머에 의해 호출됩니다. 가격을 업데이트하기 전에 _updateStockPricesLock 개체에 잠금이 설정됩니다. 코드는 다른 스레드가 이미 가격을 업데이트하고 있는지 확인한 다음 목록의 각 주식에서 TryUpdateStockPrice를 호출합니다. TryUpdateStockPrice 메서드는 주가 변경 여부와 변경 방법을 결정합니다. 주가가 변경되면 BroadcastStockPrice가 호출되어 연결된 모든 고객에게 주가 변경을 브로드캐스트합니다.

    _updatingStockPrices 플래그는 스레드로부터의 액세스가 안전하도록 휘발성 으로 표시됩니다.

    private volatile bool _updatingStockPrices = false;
    

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

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

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

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

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

    private readonly static Lazy<StockTicker> _instance =
        new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        // Remainder of constructor ...
    }
    
    private IHubConnectionContext Clients
    {
        get;
        set;
    }
    
    private void BroadcastStockPrice(Stock stock)
    {
        Clients.All.updateStockPrice(stock);
    }
    

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

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

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

SignalR 경로 등록

서버는 가로채고 SignalR로 직접 보낼 URL을 알고 있어야 합니다. 이렇게 하려면 Global.asax 파일에 일부 코드를 추가합니다.

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

  2. 전역 애플리케이션 클래스 항목 템플릿을 선택한 다음 추가를 클릭합니다.

    global.asax 추가

  3. signalR 경로 등록 코드를 Application_Start 메서드에 추가합니다.

    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHubs();
    }
    

    기본적으로 모든 SignalR 트래픽에 대한 기본 URL은 "/signalr"이고 "/signalr/hubs"는 애플리케이션에 있는 모든 허브에 대한 프록시를 정의하는 동적으로 생성된 JavaScript 파일을 검색하는 데 사용됩니다. MapHubs 메서드에는 HubConfiguration 클래스의 instance 다른 기본 URL 및 특정 SignalR 옵션을 지정할 수 있는 오버로드가 포함되어 있습니다.

  4. 파일 맨 위에 using 문을 추가합니다.

    using System.Web.Routing;
    
  5. Global.asax 파일을 저장하고 닫고 프로젝트를 빌드합니다.

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

클라이언트 코드 설정

  1. 프로젝트 폴더에 새 HTML 파일을 만들고 이름을 로StockTicker.html.

  2. 템플릿 코드를 다음 코드로 바꿉니다.

    <!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.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.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/hubs" URL을 지정하는 SignalR 프록시 스크립트 파일은 동적으로 생성되며, 이 경우 StockTickerHub.GetAllStocks의 경우 허브 클래스의 메서드에 대한 프록시 메서드를 정의합니다. 원하는 경우 SignalR 유틸리티 를 사용하여 이 JavaScript 파일을 수동으로 생성하고 MapHubs 메서드 호출에서 동적 파일 생성을 사용하지 않도록 설정할 수 있습니다.

  3. 중요

    StockTicker.html JavaScript 파일 참조가 올바른지 확인합니다. 즉, 스크립트 태그의 jQuery 버전(예제의 1.8.2)이 프로젝트의 Scripts 폴더에 있는 jQuery 버전과 동일한지 확인하고 스크립트 태그의 SignalR 버전이 프로젝트의 Scripts 폴더에 있는 SignalR 버전과 동일한지 확인합니다. 필요한 경우 스크립트 태그의 파일 이름을 변경합니다.

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

  5. 프로젝트 폴더에 새 JavaScript 파일을 만들고 이름을 StockTicker.js.

  6. 템플릿 코드를 다음 코드로 바꿉니다.

    // 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 start 함수를 호출하여 SignalR 연결을 초기화합니다. start 함수는 비동기적으로 실행되고 jQuery Deferred 개체를 반환합니다. 즉, done 함수를 호출하여 비동기 작업이 완료될 때 호출할 함수를 지정할 수 있습니다.

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

    init 함수는 서버에서 getAllStocks 함수를 호출하고 서버가 반환하는 정보를 사용하여 스톡 테이블을 업데이트합니다. 기본적으로 클라이언트에서 카멜 대/소문자를 사용해야 하지만 메서드 이름은 서버에서 파스칼 대/소문자입니다. 카멜 대/소문자 규칙은 개체가 아닌 메서드에만 적용됩니다. 예를 들어 재고를 참조합니다. 기호 및 스톡. stock.symbol 또는 stock.price가 아닌 가격입니다.

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

    클라이언트에서 파스칼 대/소문자를 사용하려는 경우 또는 완전히 다른 메서드 이름을 사용하려는 경우 HubName 특성으로 Hub 클래스 자체를 데코레이팅한 것과 동일한 방식으로 Hub 메서드를 HubMethodName 특성으로 데코레이트할 수 있습니다.

    init 메서드에서 table 행에 대한 HTML은 formatStock을 호출하여 stock 개체의 속성을 포맷한 다음, supplant( StockTicker.js맨 위에 정의됨)를 호출하여 서버에서 받은 각 스톡 개체에 대해 만들어집니다. 그런 다음 결과 HTML이 스톡 테이블에 추가됩니다.

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

    서버가 주식 가격을 변경하면 연결된 클라이언트에서 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. F5 키를 눌러 디버그 모드에서 애플리케이션을 실행합니다.

    스톡 테이블에는 처음에 "로드 중..."이 표시됩니다. 잠시 후 초기 주식 데이터가 표시되고 주가가 변경되기 시작합니다.

    로드

    초기 주식 테이블

    서버에서 변경 내용을 수신하는 스톡 테이블

  2. 브라우저 주소 표시줄에서 URL을 복사하여 하나 이상의 새 브라우저 창에 붙여넣습니다.

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

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

    StockTicker 싱글톤 개체는 서버에서 계속 실행되었으므로 주식 테이블 디스플레이는 주식이 계속 변경되었음을 보여줍니다. (변경 수치가 0인 초기 테이블은 표시되지 않습니다.)

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

로깅 사용

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

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

  1. StockTicker.js 열고 코드 줄을 추가하여 파일 끝에 있는 연결을 초기화하는 코드 바로 앞에 로깅을 사용하도록 설정합니다.

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

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

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

    IE 10 IIS 8 콘솔

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

    IE 10 콘솔, IIS 7.5

    Firefox에서 Firebug 추가 기능을 설치하여 콘솔 창을 가져옵니다. Windows 8(IIS 8)에서 Firefox 19를 실행하는 경우 전송 방법은 WebSockets입니다.

    Firefox 19 IIS 8 Websockets

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

    Firefox 19 IIS 7.5 콘솔

전체 StockTicker 샘플 설치 및 검토

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

SignalR.Sample NuGet 패키지 설치

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

  2. NuGet 패키지 관리 대화 상자에서 온라인을 클릭하고 온라인 검색 상자에 SignalR.Sample을 입력한 다음 SignalR.Sample 패키지에서 설치를 클릭합니다.

    SignalR.Sample 패키지 설치

  3. Global.asax 파일에서 RouteTable.Routes.MapHubs(); Application_Start 메서드의 앞부분에서 추가한 줄입니다.

    SignalR.Sample 패키지가 App_Start/RegisterHubs.cs 파일에 SignalR 경로를 등록하므로 Global.asax의 코드는 더 이상 필요하지 않습니다.

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    어셈블리 특성에서 참조하는 WebActivator 클래스는 SignalR.Sample 패키지의 종속성으로 설치된 WebActivatorEx NuGet 패키지에 포함됩니다.

  4. 솔루션 탐색기SignalR.Sample 패키지를 설치하여 만든 SignalR.Sample 폴더를 확장합니다.

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

    참고

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

애플리케이션 실행

  1. F5 키를 눌러 애플리케이션을 실행합니다.

    앞에서 본 표 외에도 전체 주식 시세 애플리케이션은 동일한 주식 데이터를 표시하는 가로 스크롤 창을 표시합니다. 애플리케이션을 처음으로 실행하면 "market"이 "닫힘"으로 표시되고 스크롤되지 않는 정적 눈금과 시세 창이 표시됩니다.

    StockTicker 화면 시작

    시장 열기를 클릭하면 라이브 주식 시세 상자가 가로로 스크롤하기 시작하고 서버가 주기적으로 주식 가격 변동을 임의로 브로드캐스트하기 시작합니다. 주가가 변경 될 때마다 라이브 스톡 테이블 그리드와 라이브 주식 시세 상자가 모두 업데이트됩니다. 주식의 가격 변동이 양수이면 주식은 녹색 배경으로 표시되고 변경 내용이 음수이면 주식이 빨간색 배경으로 표시됩니다.

    StockTicker 앱, 마켓 오픈

    시장 닫기 단추는 변경 내용을 중지하고 시세 스크롤을 중지하고 다시 설정 단추는 가격 변경이 시작되기 전에 모든 주식 데이터를 초기 상태로 다시 설정합니다. 더 많은 브라우저 창을 열고 동일한 URL로 이동하면 각 브라우저에서 동일한 데이터가 동시에 동적으로 업데이트됩니다. 단추 중 하나를 클릭하면 모든 브라우저가 동시에 동일한 방식으로 응답합니다.

Live Stock Ticker 디스플레이

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

주식 시세 HTML:

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

주식 시세 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;
    }

스크롤하게 만드는 jQuery 코드:

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

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

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

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

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

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

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

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

StockTicker 클래스에서 시장 상태는 MarketState 열거형 값을 반환하는 MarketState 속성에 의해 유지됩니다.

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

public enum MarketState
{
    Closed,
    Open
}

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

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

이 코드가 threadsafe인지 확인하려면 MarketState 속성을 백업하는 _marketState 필드가 volatile로 표시됩니다.

private volatile MarketState _marketState;

BroadcastMarketStateChange 및 BroadcastMarketReset 메서드는 클라이언트에 정의된 다른 메서드를 호출하는 경우를 제외하고 이미 본 BroadcastStockPrice 메서드와 유사합니다.

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 새로운 함수는 시장 상태에 따라 단추를 사용하거나 사용하지 않도록 설정하고 시세 창 가로 스크롤을 중지하거나 시작합니다. 여러 함수가 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();
    }
});

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

클라이언트가 연결을 설정한 후에는 적절한 marketOpened 또는 marketClosed 함수를 호출하기 위해 시장이 열려 있는지 또는 닫혀 있는지 확인하고 서버 메서드 호출을 단추에 연결해야 합니다.

$.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 개발 개념을 알아보려면 SignalR 소스 코드 및 리소스에 대한 다음 사이트를 방문하세요.