博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SignalR与自托管Windows服务
阅读量:3524 次
发布时间:2019-05-20

本文共 18645 字,大约阅读时间需要 62 分钟。

目录


介绍

请注意,本文主要源自Tom DykstraTom FitzMacken撰写的文章

你也可以阅读我之前的文章了解更多内容。

SignalR通常托管在IIS中的ASP.NET应用程序中,但它也可以在控制台,WPFWindows服务应用程序中自托管。如果要创建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() { } }}

创建JavaScript客户端

这里,客户端可能与连接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;       }
    • {Symbol}{Price}{PercentChange}
    • #stockTicker .symbol { font-weight: bold; } #stockTicker .change { font-style: italic; }

    添加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,您需要添加符号(例如,MicrosoftMSFT)和价格。

    创建StockTickerStockTickerHub

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

    获取SignalR上下文,以便StockTicker类可以向客户端广播

    这是关键代码,以便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/

    你可能感兴趣的文章
    Scala-面向对象后章
    查看>>
    iOS蓝牙原生封装,助力智能硬件开发
    查看>>
    iOS 代码的Taste(品位)
    查看>>
    iOS开发代码规范
    查看>>
    iOS组件化实践(基于CocoaPods)
    查看>>
    【iOS学习】RxSwift从零入手 - 介绍
    查看>>
    数据结构之栈
    查看>>
    Elastic Stack简介
    查看>>
    关于deepin系统安装design compiler的问题解答
    查看>>
    Java Agent简介及使用Byte Buddy和AspectJ LTW监控方法执行耗时
    查看>>
    记录一下最近的学习经历
    查看>>
    hadoop3.0+spark2.0两台云服务器集群环境配置。
    查看>>
    记第一次面试经历
    查看>>
    网站实现qq登录(springboot后台)
    查看>>
    简单的用户头像修改功能(springboot后台)
    查看>>
    springboot+mybatis实现分页
    查看>>
    leetcode332. 重新安排行程
    查看>>
    为什么局域网网段不同不能通信?
    查看>>
    itchat微信助手,kaggle 电影数据集分析,基于内容的电影推荐
    查看>>
    认识和使用JWT
    查看>>