ASP.NET SignalR 2.0: 자습서: SignalR 2.0을 이용한 서버 브로드캐스트
- 본 번역문서의 원문은 Getting Started with ASP.NET SignalR 2.0: Tutorial: Server Broadcast with SignalR 2.0 www.asp.net 입니다.
- 본 번역문서는 ASP.NET SignalR 2.0 : SignalR 2.0을 이용한 서버 브로드캐스트 www.taeyo.net 에서도 함께 제공됩니다.
본문에서는 ASP.NET SignalR 2.0을 이용해서 서버 브로드캐스트 기능을 제공해주는 웹 응용 프로그램을 구현해봅니다. 여기에서 말하는 서버 브로드캐스트(Server Broadcast)란 클라이언트로 전송되는 통신이 서버에 의해서 초기화되는 것을 의미합니다. 이 시나리오에서는 클라이언트로 전송되는 통신이 하나 이상의 클라이언트에 의해서 초기화되는, 챗 응용 프로그램 같은 피어-투-피어 시나리오와는 다른 프로그래밍 접근 방식이 필요합니다.
본 자습서에서 작성해보게 될 응용 프로그램은 서버 브로드캐스트 기능의 대표적인 시나리오인 주식 시세 표시기(Stock Ticker)를 흉내냅니다.
본 자습서에 관한 의견들은 언제라도 환영합니다. 본문과 직접 관련이 없는 질문들은 ASP.NET SignalR 포럼이나 StackOverflow.com에 남겨주시면 됩니다.
개요
Microsoft.AspNet.SignalR.Sample NuGet 패키지를 설치하면 Visual Studio 프로젝트에 모의 주식 시세 표시기 응용 프로그램이 설치됩니다. 본문의 전반부에서는 이 응용 프로그램의 간략한 버전을 처음부터 하나씩 만들어봅니다. 그리고, 나머지 후반부에서는 이 NuGet 패키지를 실제로 설치해보고 추가적인 기능들과 그 기능들을 구현한 코드들을 살펴보겠습니다.
주식 시세 표시기 응용 프로그램은 서버에서 연결되어 있는 모든 클라이언트들에게 주기적으로 통지를 푸시(Push)하거나 브로드캐스트 하는 실시간 응용 프로그램의 대표격인 프로그램입니다.
본 자습서의 전반부에서 만들어보게 될 응용 프로그램은 다음과 같이 주가 시세를 그리드 형태로 보여줍니다.
또한, 서버가 주기적으로 주가 시세를 무작위로 갱신하고 이 갱신된 정보를 연결되어 있는 모든 클라이언트들에게 푸시합니다. 그러면, 브라우저가 서버의 통지에 대응해서 Change 컬럼의 값과 % 컬럼의 기호들이 동적으로 변경됩니다. 만약, 브라우저들을 추가로 연 다음, 동일한 URL로 이동해보면 모든 브라우저들이 같은 데이터를 출력하고 데이터도 동시에 똑같이 변경될 것입니다.
본 자습서는 다음과 같은 절들로 구성되어 있습니다:
- 전제조건
- 프로젝트 생성하기
- 서버 코드 구성하기
- 클라이언트 코드 구성하기
- 응용 프로그램 테스트하기
- 로깅 활성화하기
- 최종본 StockTicker 샘플을 설치하고 살펴보기
- 이후 과정 안내
노트: 전체 응용 프로그램 구축 과정을 일일이 따라하기 싫다면, 새 빈 ASP.NET 웹 응용 프로그램에 SignalR.Sample을 설치한 다음, 각 과정들에 대한 코드 설명만 읽어봐도 무방합니다. 본 자습서의 전반부에서는 SignalR.Sample 코드의 일부분을 설명하고, 후반부에서는 SignalR.Sample 패키지의 추가 기능들 중 핵심 기능들을 살펴봅니다.
전제조건
본문을 시작하기 전에, 먼저 컴퓨터에 Visual Studio 2013이 설치되어 있는지 확인하시기 바랍니다. 만약, Visual Studio 2013을 보유하고 있지 않다면, ASP.NET Downloads에서 무료 Visual Studio 2013 Express Development Tool을 다운로드 받을 수 있습니다.
프로젝트 생성하기
-
먼저, Visual Studio 2013을 실행하고 파일(File) 메뉴 등을 이용해서 새 프로젝트(New Project) 대화 상자를 엽니다.
-
새 프로젝트(New Project) 대화 상자에서 템플릿(Templates) 하위의 Visual C# 노드를 확장한 다음, 웹(Web) 노드를 선택합니다.
-
ASP.NET 웹 응용 프로그램(ASP.NET Web Application) 템플릿을 선택하고 프로젝트의 이름을 SignalR.StockTicker로 지정한 다음, 확인(OK) 버튼을 클릭합니다.
-
새 ASP.NET 프로젝트(New ASP.NET Project) 대화 상자에서 Empty 템플릿을 선택하고 확인(OK) 버튼을 클릭합니다.
서버 코드 구성하기
이번 절에서는 서버에서 실행되는 코드들을 작성해봅니다.
Stock 클래스 작성하기
먼저, 주식 정보를 저장하거나 전송하기 위한 Stock 모델 클래스부터 작성해보겠습니다.
-
프로젝트 폴더에 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); } } } }
이 Stock 클래스의 인스턴스를 생성하려면 Symbol 속성 값(Microsoft의 경우 MSFT)과 Price 속성 값도 함께 설정해줘야 합니다. 나머지 속성들은 Price 속성을 설정하는 시기 및 방법에 따라 값이 달라집니다. 다시 말해서, Price 속성 값을 처음 설정하면 그 값이 그대로 DayOpen 속성 값으로 전달됩니다. 그 이후부터는 Price 속성 값을 설정할 때마다 Price 속성과 DayOpen 속성의 값 차이를 근거로 Change 속성 값과 PercentChange 속성 값이 다시 계산됩니다.
StockTicker 클래스와 StockTickerHub 클래스 작성하기
서버와 클라이언트 간의 상호작용은 SignalR Hub API를 사용해서 처리합니다. SignalR Hub 클래스를 상속 받는 StockTickerHub 클래스가 그런 클라이언트로부터의 연결 수신과 메서드 호출을 처리하게 됩니다. 반면, 클라이언트 연결과는 무관하게, 주가 데이터도 관리해야 하고, 주기적으로 주가 갱신 작업을 수행하기 위해서 Timer 개체도 실행해야 합니다. 이런 기능들까지 Hub 클래스에 작성할 수는 없는데, 왜냐하면 Hub의 인스턴스는 일시적으로만 존재하기 때문입니다. 즉, 서버에 클라이언트가 연결되거나 서버의 메서드를 호출할 때처럼 허브를 대상으로 한 작업이 발생할 때마다 매번 Hub 클래스의 인스턴스가 생성되는 것입니다. 따라서, 주식 데이터를 유지한다거나, 주가를 갱신한다거나, 갱신된 가격을 브로드캐스트하는 메커니즘은 별도 클래스에서 실행되야 하는데, 그 클래스의 이름이 바로 StockTicker입니다.
단 하나의 StockTicker 클래스 인스턴스만 서버에서 실행되게 하려면, 각 StockTickerHub 인스턴스에서 싱글톤 StockTicker 인스턴스를 참조하도록 구성해야 합니다. 더군다나, 주식 데이터와 갱신 기능이 StockTicker 클래스에 존재하기 때문에, 이 클래스가 클라이언트에게 브로드캐스트를 할 수 있어야 하지만, 정작 이 클래스는 Hub 클래스가 아닙니다. 따라서, StockTicker 클래스는 SignalR Hub 연결 컨텍스트 개체의 참조를 갖고 있어야만 합니다. 그러면, 이 참조를 이용해서 클라이언트들에게 브로드캐스트 할 수 있습니다.
-
마우스 오른쪽 버튼으로 솔루션 탐색기에서 프로젝트를 클릭한 다음, 추가(Add) | 새 항목(New Item) 메뉴를 선택해서 새 항목 추가(Add New Item) 대화 상자를 띄우고 웹(Web) 카테고리에서 SignalR 허브 클래스(v2)(SignalR Hub Class (v2))를 선택합니다.
-
새 허브 클래스의 이름을 StockTickerHub.cs로 지정하고 추가(Add) 버튼을 클릭합니다. 그러면, SignalR NuGet 패키지가 프로젝트에 추가됩니다.
-
방금 생성한 StockTickerHub의 코드를 다음과 같이 변경합니다:
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(); } } }
Hub 클래스는 클라이언트에서 호출할 서버 메서드를 정의하기 위해서 사용됩니다. 이 예제 코드에서는
GetAllStocks()
라는 하나의 메서드만 정의되었습니다. 클라이언트는 서버에 최초로 연결할 때, 이 메서드를 호출해서 현재 주가와 함께 모든 주식들의 목록을 가져오게 됩니다. 이 메서드는 동기적으로 실행할 수 있으며 메모리로부터 데이터를 가져오기 때문에IEnumerable<Stock>
형식을 반환합니다. 만약, 데이터베이스 조회나 웹 서비스 호출처럼 지연이 발생시킬 수 있는 다른 어떤 방법으로 데이터를 가져와야 한다면, 반환 값을Task<IEnumerable<Stock>>
형식으로 지정해서 비동기적으로 처리를 수행하는 편이 더 좋을 것입니다. 보다 자세한 정보는 ASP.NET SignalR Hubs API Guide - Server - When to execute asynchronously를 참고하시기 바랍니다.HubName 어트리뷰트는 Hub가 클라이언트의 자바스크립트 코드에서 참조되는 방식을 지정합니다. 이 어트리뷰트를 지정하지 않으면 클라이언트에서는 기본 이름으로 클래스 이름의 카멜 표기법 버전이 사용되는데, 이 예제에서는 stockTickerHub가 이에 해당합니다.
나중에 StockTicker 클래스를 작성하면서 살펴보겠지만, StockTicker 클래스의 싱글톤 인스턴스는 이 클래스의 정적 Instance 속성을 통해서 생성됩니다. 이 StockTicker의 싱글톤 인스턴스는 얼마나 많은 클라이언트가 연결되었거나 끊어졌거나에 무관하게 메모리에 남아있게 되는데, GetAllStocks 메서드는 바로 이 인스턴스를 이용해서 주식의 현재 정보를 반환합니다.
-
프로젝트 폴더에 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); } } }
동시에 여러 스레드에서 StockTicker 클래스의 동일한 인스턴스에 접근할 수 있기 때문에, StockTicker 클래스는 스레드로부터 안전(Threadsafe)해야만 합니다.
정적 필드에 싱글톤 인스턴스 저장하기
이 코드에서는 Instance 속성의 내부 필드인, 정적 _instance 필드를 이 클래스의 인스턴스로 초기화하는데, 생성자가 private로 지정되어 있기 때문에 이 인스턴스가 생성할 수 있는 이 클래스의 유일한 인스턴스입니다. 그리고, 성능 문제 때문이 아니라 인스턴스의 생성을 스레드로부터 안전하게 만들기 위해서, _instance 필드에 지연된 초기화(Lazy Initialization)가 사용되었습니다.
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 클래스에서 살펴본 것처럼, 클라이언트가 서버에 연결될 때마다, 개별 스레드에서 실행되는 StockTickerHub 클래스의 새로운 인스턴스가 StockTicker.Instance 정적 속성으로부터 StockTicker 싱글톤 인스턴스의 참조를 얻습니다.
ConcurrentDictionary에 주식 데이터 저장하기
이 클래스의 생성자에서는 약간의 샘플 주식 데이터로 _stocks 컬렉션을 초기화하는데, 바로 이 데이터가 GetAllStocks 메서드를 통해서 반환되는 데이터입니다. 이미 살펴봤던 것처럼, 이 주식 정보 컬렉션은 클라이언트에서 호출할 수 있는 Hub 클래스의 서버 메서드 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의 인스턴스가 해제될 때 이 데이터가 사라져도 무방합니다. 그러나, 대부분 실제 응용 프로그램에서는 데이터베이스 같은 기반 데이터 저장소를 이용해서 작업하는 것이 일반적일 것입니다.
주기적으로 주가를 갱신하기
이 클래스의 생성자에서는 주기적으로 무작위로 주가를 갱신하는 메서드를 호출하는 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 메서드는 Timer에 의해서 호출되며 state 매개변수에는 null이 전달됩니다. 그리고, 메서드 내부에서 가격을 갱신하기 전에 _updateStockPricesLock 개체를 잠급니다. 이 코드는 다른 스레드에서 이미 가격을 갱신 중인지 점검한 다음, 목록에 존재하는 각각의 Stock 개체들을 대상으로 TryUpdateStockPrice 메서드를 호출합니다. 이 TryUpdateStockPrice 메서드는 주가를 변경할지 말지, 그리고 얼마나 변경할지 결정합니다. 만약, 주가가 변경되면 BroadcastStockPrice 메서드가 호출되어 변경된 주가를 연결되어 있는 모든 클라이언트들에게 브로드캐스트 합니다.
또한, _updatingStockPrices 플래그는 volatile로 지정되어 스레드로부터 안전하게 접근할 수 있음을 나타냅니다.
private volatile bool _updatingStockPrices = false;
대부분 실제 응용 프로그램에서 TryUpdateStockPrice 메서드는 주가를 조회하는 웹 서비스를 호출하겠지만, 이 예제 코드에서는 무작위로 값을 변경하기 위해서 랜덤 숫자 생성기를 이용합니다.
StockTicker 클래스가 클라이언트로 브로드캐스트 할 수 있도록 SignalR 컨텍스트 가져오기
주가 변경이 StockTicker 개체 내에서 이뤄지기 때문에, 이 개체에서 연결된 모든 클라이언트들을 대상으로 updateStockPrice 메서드를 호출할 수 있어야 합니다. 그러나, Hub 클래스에서는 클라이언트 메서드 호출을 위한 API가 제공되지만, StockTicker 클래스는 Hub 클래스를 상속 받지도 않았고 Hub 개체에 대한 어떠한 참조도 갖고 있지 않습니다. 따라서, 연결된 클라이언트들에 브로드캐스트하기 위해서는 StockTicker 클래스가 StockTickerHub 클래스에 대한 SignalR 컨텍스트 인스턴스를 갖고 있어야만, 이를 이용해서 클라이언트의 메서드를 호출할 수 있습니다.
이 예제에서는 StockTicker 클래스의 싱글톤 인스턴스를 생성할 때, 클래스 생성자에 컨텍스트의 참조를 전달하고, 생성자는 이 참조를 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 속성의 참조를 얻어서 이를 같은 이름의 StockTicker.Client 속성에 저장해 놓으면 Hub 클래스에서 클라이언트 메서드를 호출하기 위해서 작성했던 코드와 비슷한 형태의 코드를 작성할 수 있습니다. 가령, 모든 클라이언트에 브로드캐스트하려면 Clients.All.updateStockPrice(stock)와 같이 코드를 작성하면 됩니다.
그리고, BroadcastStockPrice 메서드에서 호출하고 있는 updateStockPrice 메서드는 아직 작성되지 않았는데, 이 메서드는 잠시 뒤에 클라이언트에서 동작하는 코드를 작성할 때 추가할 것입니다. 그러나, 지금도 전혀 문제 없이 updateStockPrice 메서드를 참조할 수 있으며, 그 이유는 Clients.All 속성이 표현식이 런타임 시에 평가되는 동적 속성이기 때문입니다. 이 메서드 호출이 실행되면, SignalR이 메서드 이름과 매개변수 값을 클라이언트로 전송합니다. 클라이언트에 updateStockPrice라는 이름의 메서드가 존재하면 메서드가 호출되고 매개변수 값이 여기에 전달됩니다.
마지막으로 Clients.All은 모든 클라이언트로 전송됨을 의미합니다. SignalR은 어떤 클라이언트나 클라이언트들의 그룹을 대상으로 전송할지 지정할 수 있는 다양한 옵션들을 제공해줍니다. 더 자세한 정보는 HubConnectionContext를 살펴보시기 바랍니다.
SignalR 라우트 등록하기
서버는 어떤 URL을 가로채서 직접 SignalR로 연결해야 할지 알아야 할 필요가 있습니다. 그러려면 OWIN 시작 클래스를 추가해야 합니다.
-
솔루션 탐색기에서 프로젝트를 마우스 오른쪽 버튼으로 클릭한 다음, 추가 (Add)를 선택하고 새 항목... (New Item...)을 선택합니다. 대화 상자에서 OWIN 시작 클래스(OWIN Startup Class)를 선택하고, 새 클래스의 이름을 Startup.cs라고 지정합니다.
-
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 파일을 생성하고, 이름을 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.0.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은 다섯 개의 컬럼으로 이루어진 헤더 로우와 다섯 개의 컬럼이 합쳐진 단일 셀 데이터 로우로 구성된 테이블을 생성합니다. 데이터 로우에는 "loading..."이라는 문장이 출력되는데, 이 문장은 응용 프로그램이 시작될 때만 잠시 나타납니다. 자바스크립트 코드가 이 로우를 제거하고, 그 대신 서버에서 받아온 주식 데이터들을 보여주는 로우들로 재구성할 것이기 때문입니다.
그리고, script 태그들은 jQuery 스크립트 파일, SignalR 코어 스크립트 파일, SignalR 프록시 스크립트 파일, 그리고 잠시 뒤에 작성할 StockTicker 스크립트 파일을 참조합니다. "/signalr/hubs" URL을 가리키고 있는, 동적으로 생성되는 SignalR 프록시 스크립트 파일은 Hub 클래스에 정의된 메서드들에 대한 프록시를 정의해주는데, 여기에서는 StockTickerHub.GetAllStocks 메서드가 바로 그 메서드입니다. 만일 원한다면 SignalR 유틸리티를 이용해서 이 자바스크립트 파일을 수작업으로 생성하고 MapHubs 메서드 호출에서 동적 파일 생성을 막을 수도 있습니다.
-
중요: StockTicker.html 의 자바스크립트 파일 참조가 올바른지 확인하시기 바랍니다. script 태그의 jQuery 버전(여기에서는 1.10.2)과 프로젝트의 Scripts 폴더의 jQuery 버전이 일치하는지, 그리고 script 태그의 SignalR 버전과 프로젝트의 Scripts 폴더의 SignalR 버전이 일치하는지 확인해야 합니다. 그리고, 필요하다면 script 태그의 파일명을 변경해서 버전을 일치시킵니다.
-
솔루션 탐색기에서 StockTicker.html 파일을 마우스 오른쪽 버튼으로 클릭한 다음, 시작 페이지로 설정(Set as Start Page) 메뉴를 클릭합니다.
-
프로젝트 폴더에 새로운 자바스크립트 파일을 생성하고, 이름을 StockTicker.js 라고 지정합니다.
-
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의 start 함수를 호출해서 SignalR 연결을 초기화시킵니다. 이 start 함수는 비동기적으로 실행되고 jQuery 지연된 개체(jQuery Deferred Object)를 반환하므로, done 함수를 이용해서 비동기 작업이 완료됐을 때 호출될 함수를 지정할 수 있습니다.
$.connection.hub.start().done(init);
이 init 함수는 서버의 getAllStocks 메서드를 호출하고 서버에서 반환된 정보를 이용해서 주식 정보 테이블을 갱신합니다. 비록, 서버의 메서드 이름이 파스칼 표기법으로 작성되어 있더라도 클라이언트에서는 기본적으로 카멜 표기법을 사용해야 한다는 점에 주의하시기 바랍니다. 이 카멜 표기법 규칙은 메서드에만 해당될 뿐, 개체에는 적용되지 않습니다. 즉, stock.symbol이나 stock.price가 아닌, 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(); }
만약, 클라이언트에서도 파스칼 표기법을 사용하고 싶다거나, 또는 완전히 다른 메서드 이름을 사용하고 싶다면, Hub 클래스 자체에서 HubName 어트리뷰트를 지정했던 것과 같은 방식으로 Hub 메서드에 HubMethodName 어트리뷰트를 지정합니다.
이 init 메서드는 서버에서 받아온 stock 개체들에 대해서 각각 formatStock 함수를 호출해서 stock 개체의 속성들의 포멧팅을 수행한 다음, supplant 함수(StockTicker.js 파일의 가장 상단에 정의되어 있는)를 호출해서 rowTemplate 변수의 위치 지정자들을 stock 개체의 속성 값들로 치환해서 테이블 로우들의 HTML을 만들어 냅니다. 이렇게 만들어진 결과 HTML은 주식 테이블에 추가됩니다.
또한, init 함수는 비동기 start 함수가 완료된 뒤에 실행되는 콜백 함수의 형태로 호출되는데, 만약 이런 방식을 사용하지 않고 start 함수 호출 뒤에 별도 자바스크립트 구문으로 init 함수를 직접 호출하면, 이 함수는 실패할 것입니다. 왜냐하면, start 함수가 연결을 얻는 작업을 마칠 때까지 기다리지 않고 바로 실행되기 때문입니다. 즉, 서버 연결 과정이 끝나기도 전에 getAllStocks 함수를 호출하려고 시도할 것이기 때문입니다.
서버에서 주식의 가격이 변경되면, 서버는 연결된 클라이언트들의 updateStockPrice 함수를 호출합니다. 이 함수는 서버에서 호출할 수 있도록 stockTicker 프록시의 client 속성에 추가됩니다.
ticker.client.updateStockPrice = function (stock) { var displayStock = formatStock(stock), $row = $(rowTemplate.supplant(displayStock)); $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']') .replaceWith($row); }
이 updateStockPrice 함수는 서버로부터 전달된 stock 개체를 init 함수에서 처리하는 방식과 동일한 방식으로 테이블 로우 형태로 만듭니다. 그러나 이번에는 테이블에 로우를 추가하는 대신, 테이블에서 주식의 현재 로우를 찾아 새로운 로우로 이를 교체합니다.
응용 프로그램 테스트하기
-
F5를 눌러서 응용 프로그램을 디버그 모드로 실행합니다.
그러면, 주식 정보 테이블에 잠시 "loading..."이라는 문구가 나타났다가, 잠시 뒤에 초기 주식 데이터가 출력된 다음, 주가가 변경되기 시작할 것입니다.
-
브라우저 주소 창에서 URL을 복사해서 하나 이상의 새 브라우저 창에 붙여 넣어보시기 바랍니다.
그러면, 첫 번째 브라우저와 동일한 최초 주식 정보가 나타난 다음, 동시에 변경이 일어날 것입니다.
-
모든 브라우저를 닫고 새로운 브라우저를 연 다음, 다시 동일한 URL로 이동해봅니다.
그 동안 서버에서는 StockTicker 싱글톤 개체가 계속 실행되고 있었기 때문에, 주식 테이블의 출력은 계속 변경된 값들을 보여줍니다. (즉, Chage 컬럼의 값이 0인 최초의 테이블 상태는 출력되지 않을 것입니다.)
-
브라우저를 닫습니다.
로깅 활성화하기
SignalR은 클라이언트에서 활성화시켜서 문제해결에 활용할 수 있는 내장 로깅 기능을 제공해줍니다. 이번 절에서는 이 로깅 기능을 활성화시키고 SignalR이 다음 전송방법들 중 어떤 방법을 사용하고 있는지 로그를 통해서 확인해보겠습니다:
- WebSockets, IIS 8과 최신 브라우저에서 지원됩니다.
- Server-sent events, 인터넷 익스플로러 이외의 브라우저들에서 지원됩니다.
- 영구 프레임(Forever Frame), 인터넷 익스플로러에서 지원됩니다.
- Ajax 롱 폴링(Long Polling), 모든 브라우저에서 지원됩니다.
SignalR은 주어진 연결에 대해, 서버와 클라이언트가 모두 지원하는 최선의 전송방법을 선택하려고 시도합니다.
-
StockTicker.js 파일을 열고, 파일 끝 부분에 위치한 연결 초기화 코드 바로 앞에, 로깅을 활성화하는 코드 라인을 추가합니다:
// Start the connection $.connection.hub.logging = true; $.connection.hub.start().done(init);
-
F5를 눌러서 프로젝트를 시작합니다.
-
브라우저의 개발자 도구 창을 열고, 콘솔(Console) 탭을 열어서 로그를 살펴봅니다. 아마 페이지를 새로 고침해야만 SignalR이 새로운 연결에 대해 전송방식을 협상하는 로그를 확인할 수 있을 것입니다.
만약, Windows 8(IIS 8)에서 인터넷 익스플로러 10을 사용하고 있다면, 전송방식은 WebSockets일 것입니다.
그리고, Windows 7(IIS 7.5)에서 인터넷 익스플로러 10을 사용하고 있다면, 전송방식은 iframe일 것입니다.
파이어폭스에서는 콘솔(Console) 창을 보려면 파이어버그(Firebug) 애드-인을 설치해야 합니다. 만약, Windows 8(IIS 8)에서 파이어폭스 19를 사용하고 있다면, 전송방식은 WebSockets일 것입니다.
그리고, Windows 7(IIS 7.5)에서 파이어폭스 19를 사용하고 있다면, 전송방식은 Server-sent events일 것입니다.
최종본 StockTicker 샘플을 설치하고 살펴보기
Microsoft.AspNet.SignalR.Sample NuGet 패키지를 이용해서 설치할 수 있는 StockTicker 응용 프로그램에는 지금까지 작성한 간략한 버전의 응용 프로그램보다 더 많은 기능들이 포함되어 있습니다. 이번 절에서는 이 NuGet 패키지를 설치하고 새로운 기능과 이를 구현하기 위해 작성된 코드들을 살펴보도록 하겠습니다.
SignalR.Sample NuGet 패키지 설치하기
-
솔루션 탐색기에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 NuGet 패키지 관리(Manage NuGet Packages) 메뉴를 선택합니다.
-
NuGet 패키지 관리(Manage NuGet Packages) 대화 상자에서 온라인(Online)노드를 클릭한 다음, 온라인 검색(Search Online) 박스에 SignalR.Sample 을 입력하고 SignalR.Sample 패키지의 설치(Install) 버튼을 클릭합니다.
-
솔루션 탐색기에서 SignalR.Sample 패키지를 추가하면서 생성된 SignalR.Sample 폴더를 확장합니다.
-
SignalR.Sample 폴더에서 StockTicker.html 파일을 마우스 오른쪽 버튼으로 클릭한 다음, 시작 페이지로 설정(Set as Start Page) 메뉴를 클릭합니다.
노트: 아마 SignalR.Sample NuGet 패키지가 설치되면서 Scripts 폴더에 위치한 jQuery의 버전이 변경되었을 것입니다. 패키지가 설치한 SignalR.Sample 폴더의 새로운 StockTicker.html 파일은 패키지가 설치한 jQuery 버전과 동기화되어 있겠지만, 여러분이 작성한 원본 StockTicker.html 파일을 다시 실행해보고 싶다면, 먼저 script 태그의 jQuery 참조를 업데이트해야 합니다.
응용 프로그램 실행하기
-
F5를 눌러서 응용 프로그램을 실행합니다.
이 최종본 주식 시세 표시기 응용 프로그램에는 지금까지 살펴본 주식 시세 그리드뿐만 아니라 동일한 주식 시세 데이터를 보여주는 수평으로 스크롤되는 창이 존재합니다. 응용 프로그램을 처음 실행시키면, "마켓(Market)"은 "닫혀(Closed)" 있고, 그리드는 멈춰있으며, 시세 표시기 창도 스크롤되지 않습니다.
이 상태에서 Open Market 버튼을 클릭하면, Live Stock Ticker 창이 수평으로 스크롤되기 시작하고, 서버가 주기적으로 무작위로 변경된 주식 가격을 브로드캐스트하기 시작합니다. 매번 주가가 변경될 때마다 Live Stock Table 그리드와 Live Stock Ticker 상자가 함께 갱신될 것입니다. 주식의 가격이 오르면 해당 주식이 녹색 배경색으로 나타나고, 주식의 가격이 내리면 해당 주식이 붉은 배경색으로 나타납니다.
다시 Close Market 버튼을 클릭하면 변경이 중지되고 시세 표시기 창의 스크롤도 멈춥니다. 그리고, Reset 버튼을 클릭하면 가격 변경이 시작되기 전 상태로 모든 주식 정보가 재설정됩니다. 브라우저 창을 더 열어서 동일한 URL로 이동해보면, 모든 브라우저들이 동시에 동일한 데이터로 동적으로 갱신되는 것을 확인할 수 있을 것입니다. 또한, 버튼들 중 하나를 클릭하면, 모든 브라우저들이 동일한 방식으로 동시에 반응합니다.
Live Stock Ticker 표시기
Live Stock Ticker 표시기는 DIV 요소 내부에 위치한 정렬되지 않은 목록(역주, UL 요소) 요소로, CSS 스타일을 이용해서 한 줄로 나오도록 구성되어 있습니다. 이 표시기는 테이블과 같은 요령으로 초기화되고 갱신됩니다. 다시 말해서, <li> 템플릿 문자열 내의 위치 지정자들을 치환하고, 이 <li> 요소들을 동적으로 <ul> 요소에 추가합니다. 표시기가 스크롤되는 동작은 jQuery의 animate 함수를 이용해서 DIV 요소 내의 정렬되지 않은 목록의 좌측 마진값을 변경함으로서 수행됩니다.
Live Stock Ticker의 HTML:
<h2>Live Stock Ticker</h2> <div id="stockTicker"> <div class="inner"> <ul> <li class="loading">loading...</li> </ul> </div> </div>
Live Stock Ticker의 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 클래스는 스레드로부터 안전해야 하기 때문에 마켓의 상태를 변경하는 메서드들은 lock 블럭 내부에서 작업을 수행합니다:
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 속성의 내부를 구성하는 _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 응용 프로그램을 구현하는 방법을 살펴봤습니다. 서버의 상태를 관리하기 위해서 멀티-스레드 싱글톤 인스턴스를 활용하는 패턴은 다중-사용자 온라인 게임 시나리오에도 적용 가능합니다. 그 사례로 SignalR 기반의 ShootR 게임을 살펴보시기 바랍니다.
피어-투-피어 통신 시나리오에 관해서는 ASP.NET SignalR 2.0: SignalR 소개 문서와 ASP.NET SignalR 2.0: 자습서: SignalR 2.0을 이용한 고속 실시간 기능 문서를 참고하시기 바랍니다.
보다 고급의 SignalR 개발 개념들을 살펴보고 싶다면, SignalR 소스 코드와 리소스들이 제공되는 다음의 사이트들을 방문해보시기 바랍니다:
본 자습서의 예제 응용 프로그램이나 다른 SignalR 응용 프로그램들을 호스팅 공급자에 배포해서 인터넷 상에서 사용할 수도 있습니다. 마이크로소프트는 최대 10개까지 웹 사이트를 무료로 호스팅 할 수 있는 Windows Azure trial account를 제공해주고 있습니다. Visual Studio 웹 프로젝트를 Windows Azure 웹 사이트에 배포하는 방법에 대한 더 자세한 정보는 Deploying an ASP.NET Application to a Windows Azure Web Site를 참고하시기 바랍니다. 그리고, 예제 SignalR 응용 프로그램을 배포하는 과정은 Publish the SignalR Getting Started Sample as a Windows Azure Web Site 포스트를 참고하시기 바랍니다.
노트: 현재 Windows Azure 웹 사이트에서는 WebSocket 전송방식이 지원되지 않습니다. WebSocket 전송방식을 사용할 수 없는 경우, SignalR은 ASP.NET SignalR 2.0: SignalR 소개 문서의 전송방식들(Transports)과 그 대안(Fallbacks) 절에서 설명한 것처럼 사용 가능한 다른 전송방식들을 사용하게 됩니다. *
* 이제 Windows Azure 웹 사이트에서도 .NET 프레임워크 버전이 4.5로 설정되어 있고, 사이트 구성 페이지의 웹 소켓 항목이 활성화되어 있으면 WebSocket을 사용할 수 있습니다
- 자습서: SignalR 시작하기 (C#) 2013-03-07 12:03
- ASP.NET SignalR 2.0: SignalR 소개 2014-01-06 08:00
- ASP.NET SignalR 2.0: 지원되는 플랫폼 2014-01-07 08:00
- ASP.NET SignalR 2.0: SignalR 1.x 프로젝트를 2.0으로 업그레이드하기 2014-01-08 08:00
- ASP.NET SignalR 2.0: 자습서: SignalR 2.0 시작하기 2014-01-09 08:00
- ASP.NET SignalR 2.0: 자습서: SignalR 2.0과 MVC 5 시작하기 2014-01-10 08:00
- ASP.NET SignalR 2.0: 자습서: SignalR 자체-호스트(Self-Host) 2014-01-13 08:00
- ASP.NET SignalR 2.0: 자습서: SignalR 2.0을 이용한 고속 실시간 기능 2014-01-14 08:00
- ASP.NET SignalR 2.0: 자습서: SignalR 2.0을 이용한 서버 브로드캐스트 2014-01-15 08:00
- ASP.NET SignalR 2.0: Windows Azure 웹 사이트에서 SignalR 사용하기 2014-01-16 08:00