SignalR与自托管Windows服务

目录

介绍

创建服务器

创建JavaScript客户端

服务器广播功能

创建StockTicker和StockTickerHub类

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

结论

参考


  • 下载源代码 - 1.6 MB

介绍

请注意,本文主要源自Tom DykstraTom FitzMacken撰写的文章教程使用SignalR 2实现服务广播

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

SignalR通常托管在IIS中的ASP.NET应用程序中,但它也可以在控制台,WPFWindows服务应用程序中自托管。如果要创建WPF或控制台SignalR应用程序,则必须为自托管。SignalR建立在OWIN .NET的开放Web接口之上,它定义了.NET Web服务器和Web应用程序之间的抽象层。

此应用程序将使用Topshelf构建,因此我们无需了解Windows Service类的复杂性,使用InstallUtil.exe执行安装。它还允许我们像调试控制台应用程序一样调试应用程序。

创建服务器

首先在Visual Studio中创建Windows服务,确保您的项目使用.NET 4.5或更高版本:

SignalR与自托管Windows服务_第1张图片

然后在包管理器控制台中键入:

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()
        {
        }
    }
}

创建JavaScript客户端

这里,客户端可能与连接URL不在同一个地址,因此需要明确指出。创建一个新的ASPNET Web应用程序,然后选择Empty模板。

SignalR与自托管Windows服务_第2张图片

然后,使用包管理器控制台添加以下内容,确保将默认项目设置为客户端

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;   
        }
    • {Symbol}{Price}{PercentChange}
    • #stockTicker .symbol { font-weight: bold; } #stockTicker .change { font-style: italic; }

    添加StockTicker.html(此代码直接来自文章教程:SignalR 2入门):

    
    
    
        ASP.NET SignalR Stock Ticker
        
    
    
        

    ASP.NET SignalR Stock Ticker Sample

    Live Stock Table

    SymbolPriceOpen HighLowChange%
    loading...

    Live Stock Ticker

    • loading...

    对于每个stock,您需要添加符号(例如,MicrosoftMSFT)和价格。

    创建StockTickerStockTickerHub

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

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

    这是关键代码,以便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服务应用程序中。

    参考

    • Learn About ASP.NET SignalR
    • Sample SignalR Game ShootR
    • SignalR Source Code
    • Building Web Apps with ASP.NET Jump Start: (08) Real-time Communication with SignalR
    • OWIN and Katana
    • Using SignalR in WinForms and WPF
    • Server Broadcast with SignalR
    • HubConnectionContext Class
    • Topshelf

     

    原文地址:https://www.codeproject.com/Articles/881511/SignalR-with-Self-hosted-Windows-Service

    你可能感兴趣的:(CSharp.NET,架构及框架)