本文共 18645 字,大约阅读时间需要 62 分钟。
目录
请注意,本文主要源自Tom Dykstra和Tom FitzMacken撰写的文章。
你也可以阅读我之前的文章了解更多内容。
SignalR通常托管在IIS中的ASP.NET应用程序中,但它也可以在控制台,WPF或Windows服务应用程序中自托管。如果要创建WPF或控制台SignalR应用程序,则必须为自托管。SignalR建立在OWIN 之上,它定义了.NET Web服务器和Web应用程序之间的抽象层。
此应用程序将使用构建,因此我们无需了解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);
还要添加这两个类(此代码已从文章):
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 Simple Chat
请注意,如果您选择调试Windows服务而不是从“服务”窗口运行它,最好先启动服务项目并确保它正在运行,然后在另一个Visual Studio实例中启动客户端项目。
以下调用实际上是异步启动Windows服务中的SignalR服务器:
WebApp.Start(url);
上述代码使用对等通信功能,其中发送给客户端的通信由一个或多个客户端发起。如果要将通信推送到由服务器启动的客户端,则需要添加服务器广播功能。
对于本文的这一部分,我将构建第一个点对点演示应用程序,为了使其更清晰,请查看第二个名为SignalRBroadcastSample的演示应用程序。
首先,创建一个空的ASP.NET网站项目。
将以下Stock.cs文件和两个JavaScript文件添加到SignalRBroadcastSample项目中(此代码直接来自文章):
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(此代码直接来自文章):
////// /*! 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 pluginjQuery.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")] 属性中找到的(此代码直接来自文章):
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(此代码直接来自文章):
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,它可以保存库存数据,更新价格,广播价格更新,并运行计时器以独立于客户端连而接定期触发更新(此代码直接来自文章):
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类,并将处理来自客户端的接收连接和方法调用(此代码直接来自文章):
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 IEnumerableGetAllStocks() { 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<IEnumerable<Stock>>作为启用异步处理的返回值。有关详细信息,请参阅。
HubName 属性指示Hub将如何在客户端上的JavaScript代码中引用。
每次客户端连接到服务器时,StockTickerHub在单独的线程上运行的类的新实例都会获得StockTicker单例。
另外,更新你的jQuery包:
PM> Install-Package jQuery -Version 1.10.2
最后,添加一个Startup类,告诉服务器哪个URL被拦截和指向 SignalR(此代码直接来自文章):
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类可以向所有客户端广播(此代码直接来自文章):
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服务应用程序中。
原文地址:
转载地址:http://fszhj.baihongyu.com/