目录
介绍
创建服务器
创建JavaScript客户端
服务器广播功能
创建StockTicker和StockTickerHub类
获取SignalR上下文,以便StockTicker类可以向客户端广播
结论
参考
请注意,本文主要源自Tom Dykstra和Tom FitzMacken撰写的文章教程:使用SignalR 2实现服务广播 。
你也可以阅读我之前的文章SignalR简介了解更多内容。
SignalR通常托管在IIS中的ASP.NET应用程序中,但它也可以在控制台,WPF或Windows服务应用程序中自托管。如果要创建WPF或控制台SignalR应用程序,则必须为自托管。SignalR建立在OWIN (.NET的开放Web接口)之上,它定义了.NET Web服务器和Web应用程序之间的抽象层。
此应用程序将使用Topshelf构建,因此我们无需了解Windows Service类的复杂性,使用InstallUtil.exe执行安装。它还允许我们像调试控制台应用程序一样调试应用程序。
首先在Visual Studio中创建Windows服务,确保您的项目使用.NET 4.5或更高版本:
然后在包管理器控制台中键入:
PM> Install-Package Microsoft.AspNet.SignalR.SelfHost
PM> Install-Package TopShelf
PM> Install-Package TopShelf.NLog
PM> Install-Package Microsoft.Owin.Cors
后者是跨域支持所必需的,对于应用程序托管SignalR和不同域中的网页的情况——在此示例中,SignalR服务器和客户端将位于不同的端口上。
确保Program.cs具有以下代码,允许您从Visual Studio中调试服务,或者在安装时像普通服务一样运行它:
using ServiceProcess.Helpers;
using System;
using System.Collections.Generic;
using System.Data;
using System.ServiceProcess;
namespace SelfHostedServiceSignalRSample
{
static class Program
{
///
/// The main entry point for the application.
///
static void Main()
{
HostFactory.Run(serviceConfig =>
{
serviceConfig.Service(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"); // All subsequent failures
});
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();
});
}
}
}
在您的OnStart方法中,添加以下代码:
string url = "http://localhost:8090"; WebApp.Start(url);
还要添加这两个类(此代码已从文章教程: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);
}
}
}
其中Startup类包含了配置SignalR服务器的配置和映射SignalR的调用,后者为项目中的任何Hub对象创建路由。
以下是实际服务本身的C#源代码:
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();
public SignalRServiceChat()
{
}
public void OnStart(string[] args)
{
Log.InfoFormat("SignalRServiceChat: In OnStart");
// This will *ONLY* bind to localhost, if you want to bind to all addresses
// use http://*:8080 to bind to all addresses.
// See http://msdn.microsoft.com/en-us/library/system.net.httplistener.aspx
// for more information.
string url = "http://localhost:8090";
WebApp.Start(url);
}
public void OnStop()
{
Log.InfoFormat("SignalRServiceChat: In OnStop");
}
public void Dispose()
{
}
}
}
这里,客户端可能与连接URL不在同一个地址,因此需要明确指出。创建一个新的ASPNET Web应用程序,然后选择Empty模板。
然后,使用包管理器控制台添加以下内容,确保将“默认项目”设置为“客户端”。
PM> Install-Package Microsoft.AspNet.SignalR.JS
现在添加一个包含此代码的HTML页面(此代码直接来自文章教程:SignalR 2入门):
SignalR Simple Chat
请注意,如果您选择调试Windows服务而不是从“服务”窗口运行它,最好先启动服务项目并确保它正在运行,然后在另一个Visual Studio实例中启动客户端项目。
以下调用实际上是异步启动Windows服务中的SignalR服务器:
WebApp.Start(url);
上述代码使用对等通信功能,其中发送给客户端的通信由一个或多个客户端发起。如果要将通信推送到由服务器启动的客户端,则需要添加服务器广播功能。
对于本文的这一部分,我将构建第一个点对点演示应用程序,为了使其更清晰,请查看第二个名为SignalRBroadcastSample的演示应用程序。
首先,创建一个空的ASP.NET网站项目。
将以下Stock.cs文件和两个JavaScript文件添加到SignalRBroadcastSample项目中(此代码直接来自文章教程: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);
}
}
}
}
添加SignalR.StockTicker.js(此代码直接来自文章教程:SignalR 2入门):
///
///
/*!
ASP.NET SignalR Stock Ticker Sample
*/
// Crockford's supplant method (poor man's templating)
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;
}
);
};
}
// A simple background color flash effect that uses jQuery Color plugin
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, // the generated client-side hub proxy
up = '?',
down = '?',
$stockTable = $('#stockTable'),
$stockTableBody = $stockTable.find('tbody'),
rowTemplate = '{Symbol}{Price}{DayOpen}{DayHigh}{DayLow}{Direction}
{Change}{PercentChange}',
$stockTicker = $('#stockTicker'),
$stockTickerUl = $stockTicker.find('ul'),
liTemplate = '{Symbol} {Price}
{Direction} {Change} ({PercentChange}) ';
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));
});
});
}
// Add client-side hub methods that the server will call
$.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' // red
: '154,240,117'; // green
$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();
}
});
// Start the connection
$.connection.hub.start()
.then(init)
.then(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();
});
});
});
在上面的代码中,$.connection 指的是SignalR代理。它获取对StockTickerHub类的代理的引用并将其放入ticker变量中,其中代理名称是在[HubName{"stockTickerMini")] 属性中找到的(此代码直接来自文章教程:SignalR 2入门):
var ticker = $.connection.stockTickerMini
添加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;
}
添加StockTicker.html(此代码直接来自文章教程:SignalR 2入门):
ASP.NET SignalR Stock Ticker
ASP.NET SignalR Stock Ticker Sample
Live Stock Table
Symbol Price Open
High Low Change %
loading...
Live Stock Ticker
- loading...
对于每个stock,您需要添加符号(例如,Microsoft的MSFT)和价格。
添加StockTicker.cs,它可以保存库存数据,更新价格,广播价格更新,并运行计时器以独立于客户端连而接定期触发更新(此代码直接来自文章教程: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
{
// Singleton instance
private readonly static Lazy _instance = new Lazy(
() => new StockTicker
(GlobalHost.ConnectionManager.GetHubContext().Clients));
private readonly object _marketStateLock = new object();
private readonly object _updateStockPricesLock = new object();
private readonly ConcurrentDictionary _stocks = new ConcurrentDictionary();
// Stock can go up or down by a percentage of this factor on each change
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 clients)
{
Clients = clients;
LoadDefaultStocks();
}
public static StockTicker Instance
{
get
{
return _instance.Value;
}
}
private IHubConnectionContext Clients
{
get;
set;
}
public MarketState MarketState
{
get { return _marketState; }
private set { _marketState = value; }
}
public IEnumerable 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
{
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)
{
// This function must be re-entrant as it's running as a timer interval handler
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 udpate this stock or not
var r = _updateOrNotRandom.NextDouble();
if (r > 0.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() > 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
}
}
该StockTicker.cs类必须是线程安全的,这是由延迟初始化完成的。
添加StockTickerHub.cs,它派生自SignalR Hub类,并将处理来自客户端的接收连接和方法调用(此代码直接来自文章教程: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 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();
}
}
}
Hub 上面的类用于定义客户端可以调用的服务器上的方法。
如果任何方法需要等待,那么您可以指定,例如,Task
HubName 属性指示Hub将如何在客户端上的JavaScript代码中引用。
每次客户端连接到服务器时,StockTickerHub在单独的线程上运行的类的新实例都会获得StockTicker单例。
另外,更新你的jQuery包:
PM> Install-Package jQuery -Version 1.10.2
最后,添加一个Startup类,告诉服务器哪个URL被拦截和指向 SignalR(此代码直接来自文章教程: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)
{
// For more information on how to configure your application using OWIN startup,
// visit http://go.microsoft.com/fwlink/?LinkID=316888
app.MapSignalR();
}
}
}
这是关键代码,以便StockTicker类可以向所有客户端广播(此代码直接来自文章教程:SignalR 2入门):
private readonly static Lazy _instance =
new Lazy(() =>
new StockTicker(GlobalHost.ConnectionManager.GetHubContext().Clients));
private StockTicker(IHubConnectionContext clients)
{
Clients = clients;
// Remainder of ctor ...
}
private IHubConnectionContext Clients
{
get;
set;
}
private void BroadcastStockPrice(Stock stock)
{
Clients.All.updateStockPrice(stock);
}
由于价格变化源自StockTicker对象,因此该对象需要在所有连接的客户端上调用updateStockPrice方法。在Hub类中,有一个用于调用客户端方法的API,但StockTicker不是从Hub类派生的,也没有对Hub对象的任何引用。这就是为什么StockTicker类必须为StockTickerHub类获取SignalR上下文的实例,以便它可以调用客户端上的方法。
在上面的代码中,StockTicker类在创建单例类时获取对SignalR上下文的引用,然后将该引用传递给其构造函数,该构造函数将其存储在Clients属性中。
另请注意,上面代码中的updateStockPrice 调用在SignalR.StockTicker.js JavaScript文件中调用该名称的函数。
Clients.All 意味着发送给所有客户。要了解如何指定哪些客户端或客户端组,请参阅此处。
接下来,按F5测试应用程序。
在本文中,我讨论了创建一个Windows服务,该服务演示了使用SignalR的对等通信,并且SignalR还能够在单独的演示项目中提供从服务器到所有客户端的广播。在我的下一篇文章中,我计划演示如何将该广播SignalR功能放入Windows服务应用程序中。
原文地址:https://www.codeproject.com/Articles/881511/SignalR-with-Self-hosted-Windows-Service