Introduction
Please note that this article is derived from and based primarily on the article, Tutorial: Server Broadcast with SignalR 2 by Tom Dykstra and Tom FitzMacken where in this article, we are going to build a chat application.
This article covers introductory information on a self-hosted service using SignalR. Please see my previous article on SignalR with web applications here that also contain valuable introductory information.
SignalR is normally hosted in an ASP.NET application in IIS, but it can also be self-hosted in a Console, WPF or Windows Service application. If you want to create a WPF or Console SignalR application, then it must be Self-Hosted. SignalR is built on top of OWIN (Open Web Interface for .NET), which defines an abstraction layer between .NET web servers and web applications.
This application will be built using Topshelf so that we don't need to understand the complexities of Windows Service classes, perform installation using InstallUtil.exe. It will also allow us to debug the application as simply as debugging a Console application. If you actually want to install the application as a Windows service, then Topshelf allows you to simply type this in a command prompt that you have run as administrator.
C:\Users\myUserId> SignalRSelfHostedService install
Please download the sample project source code here and here.
Background
Originally, the internet worked by a user putting in a URL into their browser and it downloaded the web page content. Nothing changed on the client page until the user performed a refresh and made another request to the Web server.
Then came AJAX which allowed the web page to be updated asynchronously so the user doesn't need to reload an entire web page if only a small part needs to be updated. It does so by using an XMLHttpRequest
object. The problem with this methodology is that the server cannot initiate contact with the client, while with SignalR, the server can.
Also, technologies were developed called polling and long polling. This caused the client to periodically make requests to the server to update the web page contents. The problem with this methodology is that it makes inefficient use of computing resources to repeatedly perform these polling actions.
Another methodology used to improve the responsiveness of a web browser is Server Sent Events (SSE) but the problem with this is that it is only supported by a few browsers.
SignalR uses multiple technologies, including WebSockets and JavaScript, and it will even use polling and long polling if WebSockets are not available on a particular browser. It abstracts away these details and lets the developer focus on the logic of the application.
Creating the Server
Start by creating a Console Application application (which can be done if you are using Topshelf), or if you are not using Topshelf, then create a Windows service in Visual Studio, ensuring that your project uses .NET 4.5 or greater:
Then type this at the package manager console:
PM> Install-Package Microsoft.AspNet.SignalR.SelfHost
PM> Install-Package TopShelf
PM> Install-Package TopShelf.NLog
PM> Install-Package Microsoft.Owin.Cors
The latter is required for cross-domain support, for the case where applications host SignalR and a web page in different domains--in this example, the SignalR server and client will be on different ports.
Ensure that your Program.cs has the following code, which allows you to debug the service from within Visual Studio or run it like a normal service when installed:
using System;
using System.Collections.Generic;
using System.Data;
using Topshelf;
namespace SelfHostedServiceSignalRSample
{
static class Program
{
static void Main()
{
HostFactory.Run(serviceConfig =>
{
serviceConfig.Service<SignalRServiceChat>(serviceInstance =>
{
serviceConfig.UseNLog();
serviceInstance.ConstructUsing(
() => new SignalRServiceChat());
serviceInstance.WhenStarted(
execute => execute.OnStart(null));
serviceInstance.WhenStopped(
execute => execute.OnStop());
});
TimeSpan delay = new TimeSpan(0, 0, 0, 60);
serviceConfig.EnableServiceRecovery(recoveryOption =>
{
recoveryOption.RestartService(delay);
recoveryOption.RestartService(delay);
recoveryOption.RestartComputer(delay,
System.Reflection.Assembly.GetExecutingAssembly().GetName().Name +
" computer reboot");
});
serviceConfig.SetServiceName
(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
serviceConfig.SetDisplayName
(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
serviceConfig.SetDescription
(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name +
" is a simple web chat application.");
serviceConfig.StartAutomatically();
});
}
}
}
In your OnStart
method, add the following code:
string url = "http://localhost:8090"; WebApp.Start(url);
Also add these two classes (this code is modified from the article, Tutorial: Getting Started with SignalR 2):
using Microsoft.Owin.Cors;
using Owin;
namespace SelfHostedServiceSignalRSample
{
class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR();
}
}
}
using Microsoft.AspNet.SignalR;
namespace SelfHostedServiceSignalRSample
{
public class MyHub : Hub
{
public void Send(string name, string message)
{
Clients.All.addMessage(name, message);
}
}
}
Where the Startup
class contains the configuration for the SignalR
server and the call to map SignalR
, which creates routes for any Hub
objects in the project.
Here is the C# source code for the actual service itself:
using System;
using Microsoft.Owin;
using Microsoft.Owin.Hosting;
using Topshelf.Logging;
[assembly: OwinStartup(typeof(SelfHostedServiceSignalRSample.Startup))]
namespace SelfHostedServiceSignalRSample
{
public partial class SignalRServiceChat : IDisposable
{
public static readonly LogWriter Log = HostLogger.Get<SignalRServiceChat>();
public SignalRServiceChat()
{
}
public void OnStart(string[] args)
{
Log.InfoFormat("SignalRServiceChat: In OnStart");
string url = "http://localhost:8090";
WebApp.Start(url);
}
public void OnStop()
{
Log.InfoFormat("SignalRServiceChat: In OnStop");
}
public void Dispose()
{
}
}
}
Creating the JavaScript Client
Here, the client may not be at the same address as the connection URL, so it needs to be indicated explicitly. Create a new ASPNET Web Application, and select the Empty template.
Then, add the following using the package manager console, ensuring that the Default Project is set to Client
.
PM> Install-Package Microsoft.AspNet.SignalR.JS
Now add an HTML page with this code (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
<!DOCTYPE html>
<html>
<head>
<title>SignalR Simple Chat</title>
<style type="text/css">
.container {
background-color: #99CCFF;
border: thick solid #808080;
padding: 20px;
margin: 20px;
}
</style>
</head>
<body>
<div class="container">
<input type="text" id="message" />
<input type="button" id="sendmessage" value="Send" />
<input type="hidden" id="displayname" />
<ul id="discussion"></ul>
</div>
<!--
<!--
<script src="Scripts/jquery-1.6.4.min.js"></script>
<!--
<script src="Scripts/jquery.signalR-2.1.0.min.js"></script>
<!--
<script src="http://localhost:8080/signalr/hubs"></script>
<!--
<script type="text/javascript">
$(function () {
$.connection.hub.url = "http://localhost:8080/signalr";
var chat = $.connection.myHub;
chat.client.addMessage = function (name, message) {
var encodedName = $('<div />').text(name).html();
var encodedMsg = $('<div />').text(message).html();
$('#discussion').append('<li><strong>' + encodedName
+ '</strong>: ' + encodedMsg + '</li>');
};
$('#displayname').val(prompt('Enter your name:', ''));
$('#message').focus();
$.connection.hub.start().done(function () {
$('#sendmessage').click(function () {
chat.server.send($('#displayname').val(), $('#message').val());
$('#message').val('').focus();
});
});
});
</script>
</body>
</html>
If you choose to create a Windows Service instead of a Console Application with Topshelf, then this is how you would install the Windows Service:
Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved.
C:\> installutil SelfHostedServiceSignalRSample.exe
Please note that if you choose to debug the Windows service instead of running it from the Services window, it is better to first start up just the service project and make sure it is running, and then start the Client
project in another instance of Visual Studio.
The following call is what actually asynchronously starts the SignalR server in your Windows Service:
WebApp.Start(url);
Here is what the chat application should look like when running:
Server Broadcast Functionality
The above code uses peer-to-peer communication functionality, where communications sent to clients are initiated by one or more of the clients. If you want to have communications pushed to clients that are initiated by the server, then you need to add Server Broadcast functionality.
For this part of the article, I will build on the first peer-to-peer demo application, so to make it more clear, please take a look at the second demo application called SignalRBroadcastSample
.
First, create an empty ASP.NET Website project.
Add the following Stock.cs file and two JavaScript files to the SignalRBroadcastSample
project (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Client
{
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);
}
}
}
}
Add SignalR.StockTicker.js (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
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;
}
);
};
}
jQuery.fn.flash = function (color, duration) {
var current = this.css('backgroundColor');
this.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2)
.animate({ backgroundColor: current }, duration / 2);
};
$(function () {
var ticker = $.connection.stockTicker,
up = '?',
down = '?',
$stockTable = $('#stockTable'),
$stockTableBody = $stockTable.find('tbody'),
rowTemplate = '{Symbol}{Price}{DayOpen}{DayHigh}{DayLow}{Direction}
{Change}{PercentChange}',
$stockTicker = $('#stockTicker'),
$stockTickerUl = $stockTicker.find('ul'),
liTemplate = '<li data-symbol="{Symbol}">{Symbol} {Price}
{Direction} {Change} ({PercentChange})</li>';
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,
DirectionClass: stock.Change === 0 ? 'even' : stock.Change >= 0 ? 'up' : 'down'
});
}
function scrollTicker() {
var w = $stockTickerUl.width();
$stockTickerUl.css({ marginLeft: w });
$stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}
function stopTicker() {
$stockTickerUl.stop();
}
function init() {
return ticker.server.getAllStocks().done(function (stocks) {
$stockTableBody.empty();
$stockTickerUl.empty();
$.each(stocks, function () {
var stock = formatStock(this);
$stockTableBody.append(rowTemplate.supplant(stock));
$stockTickerUl.append(liTemplate.supplant(stock));
});
});
}
$.extend(ticker.client, {
updateStockPrice: function (stock) {
var displayStock = formatStock(stock),
$row = $(rowTemplate.supplant(displayStock)),
$li = $(liTemplate.supplant(displayStock)),
bg = stock.LastChange < 0
? '255,148,148'
: '154,240,117';
$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();
}
});
$.connection.hub.start()
.then(init)
.then(function () {
return ticker.server.getMarketState();
})
.done(function (state) {
if (state === 'Open') {
ticker.client.marketOpened();
} else {
ticker.client.marketClosed();
}
$("#open").click(function () {
ticker.server.openMarket();
});
$("#close").click(function () {
ticker.server.closeMarket();
});
$("#reset").click(function () {
ticker.server.reset();
});
});
});
In the above code, the $.connection
refers to SignalR proxies. It gets a reference to the proxy for the StockTickerHub
class and puts it in the ticker
variable, where the proxy name is what is found in the [HubName{"stockTickerMini")]
attribute (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
var ticker = $.connection.stockTickerMini
Add StockTicker.css:
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;
}
#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;
}
{Symbol}{Price}{PercentChange}
#stockTicker .symbol { font-weight: bold; } #stockTicker .change { font-style: italic; }
Add StockTicker.html (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET SignalR Stock Ticker</title>
<link href="StockTicker.css" rel="stylesheet" />
</head>
<body>
<h1>ASP.NET SignalR Stock Ticker Sample</h1>
<input type="button" id="open" value="Open Market" />
<input type="button" id="close" value="Close Market" disabled="disabled" />
<input type="button" id="reset" value="Reset" />
<h2>Live Stock Table</h2>
<div id="stockTable">
<table border="1">
<thead>
<tr><th>Symbol</th><th>Price</th><th>Open</th>
<th>High</th><th>Low</th><th>Change</th><th>%</th></tr>
</thead>
<tbody>
<tr class="loading"><td colspan="7">loading...</td></tr>
</tbody>
</table>
</div>
<h2>Live Stock Ticker</h2>
<div id="stockTicker">
<div class="inner">
<ul>
<li class="loading">loading...</li>
</ul>
</div>
</div>
<script src="jquery-1.10.2.min.js"></script>
<script src="jquery.color-2.1.2.min.js"></script>
<script src="../Scripts/jquery.signalR-2.2.0.js"></script>
<script src="../signalr/hubs"></script>
<script src="SignalR.StockTicker.js"></script>
</body>
</html>
For each stock, you need to add the symbol (e.g., MSFT for Microsoft) and the Price.
Create the StockTicker and StockTickerHub Classes
Add StockTicker.cs, which keeps stock data, updates prices, broadcasts the price updates, and runs a timer to periodically trigger updates independently of client connections (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace SelfHostedServiceSignalRSample
{
public class StockTicker
{
private readonly static Lazy<stockticker> _instance = new Lazy<stockticker>(
() => new StockTicker
(GlobalHost.ConnectionManager.GetHubContext<stocktickerhub>().Clients));
private readonly object _marketStateLock = new object();
private readonly object _updateStockPricesLock = new object();
private readonly ConcurrentDictionary<string,
stock=""> _stocks = new ConcurrentDictionary<string, stock="">();
private readonly double _rangePercent = 0.002;
private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
private readonly Random _updateOrNotRandom = new Random();
private Timer _timer;
private volatile bool _updatingStockPrices;
private volatile MarketState _marketState;
private StockTicker(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
LoadDefaultStocks();
}
public static StockTicker Instance
{
get
{
return _instance.Value;
}
}
private IHubConnectionContext<dynamic> Clients
{
get;
set;
}
public MarketState MarketState
{
get { return _marketState; }
private set { _marketState = value; }
}
public IEnumerable<stock> GetAllStocks()
{
return _stocks.Values;
}
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();
}
}
private void LoadDefaultStocks()
{
_stocks.Clear();
var stocks = new List<stock>
{
new Stock { Symbol = "MSFT", Price = 41.68m },
new Stock { Symbol = "AAPL", Price = 92.08m },
new Stock { Symbol = "GOOG", Price = 543.01m }
};
stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
}
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)
{
var r = _updateOrNotRandom.NextDouble();
if (r > 0.1)
{
return false;
}
var random = new Random((int)Math.Floor(stock.Price));
var percentChange = random.NextDouble() * _rangePercent;
var pos = random.NextDouble() > 0.51;
var change = Math.Round(stock.Price * (decimal)percentChange, 2);
change = pos ? change : -change;
stock.Price += change;
return true;
}
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();
}
private void BroadcastStockPrice(Stock stock)
{
Clients.All.updateStockPrice(stock);
}
}
public enum MarketState
{
Closed,
Open
}
}
The StockTicker.cs class must be threadsafe which is accomplished by the Lazy initialization.
Add StockTickerHub.cs, which derives from the SignalR Hub
class and will handle receiving connections and method calls from clients (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SelfHostedServiceSignalRSample
{
[HubName("stockTicker")]
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();
}
public string GetMarketState()
{
return _stockTicker.MarketState.ToString();
}
public void OpenMarket()
{
_stockTicker.OpenMarket();
}
public void CloseMarket()
{
_stockTicker.CloseMarket();
}
public void Reset()
{
_stockTicker.Reset();
}
}
}
The Hub
class above is used to define methods on the server that clients can call.
If any of the methods require waiting, then you could specify, for example, Task<IEnumerable<Stock>>
as the return value to enable async processing. For details, please see here.
The HubName
attribute indicates how the Hub
will be referenced in the JavaScript code on the client.
Each time a client connects to the server, a new instance of the StockTickerHub
class running on a separate thread gets the StockTicker
singleton.
Also, update your jQuery package:
PM> Install-Package jQuery -Version 1.10.2
Finally, add a Startup
class, which tells the server which URL to intercept and direct to SignalR
(this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(Microsoft.AspNet.SignalR.StockTicker.Startup))]
namespace Microsoft.AspNet.SignalR.StockTicker
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
Get SignalR Context so StockTicker Class Can Broadcast to Clients
This is the key code so that the StockTicker
class can broadcast to all clients (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):
private readonly static Lazy<stockticker> _instance =
new Lazy<stockticker>(() =>
new StockTicker(GlobalHost.ConnectionManager.GetHubContext<stocktickerhub>().Clients));
private StockTicker(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
}
private IHubConnectionContext<dynamic> Clients
{
get;
set;
}
private void BroadcastStockPrice(Stock stock)
{
Clients.All.updateStockPrice(stock);
}
Since the price changes originate in the StockTicker
object, this object needs to call an updateStockPrice
method on all connected clients. In the Hub
class, there is an API for calling client methods, but StockTicker
does not derive from the Hub
class, and does not have any reference to a Hub
object. That is why the StockTicker
class has to get the SignalR
context instance for the StockTickerHub
class, so that it can call methods on the clients.
In the above code, the StockTicker
class gets a reference to the SignalR
context when it creates the singleton class, and then passes that reference to its constructor, which stores that in the Clients
property.
Notice also that the call to updateStockPrice
in the code above calls the function of that name in the SignalR.StockTicker.js JavaScript file.
Clients.All
means to send to all clients. To learn how to specify which clients or groups of clients, see here.
Next, press F5 to test the application.
Conclusion
In this article, I discussed creating a Windows Service that demonstrated peer-to-peer communication using SignalR
, and the ability in SignalR
to also provide broadcasting from the server to all clients on a separate demo project. In my next article, I plan on demonstrating how to put that broadcasting SignalR
functionality into the Windows Service application.
Acknowledgements
Please note that I got most of the ideas for this article from Patrick Fletcher's article here and Tom Dykstra and Tom FitzMacken's article here and from a pluralsight course on Topshelf.
History
- 2019-06-04: Updated application to use Topshelf
- 2015-03-02: Added details on using
SignalR
for broadcasting from the server to all clients - 2019-07-10: Added details on how to use
Topshelf
which provides a simpler way to get started in winservice development and also allows for debugging as a console app
References