SignalR and self-hosted Windows services

table of Contents

Introduction

Create a server

Create a JavaScript client

Broadcast server feature

Creating and StockTickerHub class StockTicker

Get SignalR context, so that the class can StockTicker client broadcasts

in conclusion

reference


Introduction

Please note, this article from Tom Dykstra and Tom FitzMacken articles written tutorial : Use SignalR 2 achieve service broadcasting .

You can also read my previous articles SignalR Introduction to learn more.

SignalR usually hosted on IIS in ASP.NET applications, but it can also be in the console, WPF , or Windows self-managed services applications. If you want to create WPF or Console SignalR application, it must be self-hosting. SignalR established in Owin  ( the .NET open Web Interface ) above, which defines the .NET Web server and Web abstraction layer between applications.

This application will use Topshelf building, so we need to understand Windows Service complexity classes, use InstallUtil.exe perform the installation. It also allows us to debug console applications like the same debug the application.

Create a server

First in Visual Studio to create the Windows service, ensure that your project using .NET 4.5 or later:

Then type in 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 to support cross-domain, for application hosting SignalR circumstances and different domains of the page - in this example, SignalR server and client are on different ports.

Ensure Program.cs with the following code that allows you to from Visual Studio debugging services, or when you install the same service as an ordinary run it:

using ServiceProcess.Helpers;
using System;
using System.Collections.Generic;
using System.Data;
using System.ServiceProcess;

namespace SelfHostedServiceSignalRSample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        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"); // 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();
            });
        }
    }
}

In your OnStart method, add the following code:

string url = "http://localhost:8090"; WebApp.Start(url);

Also add two classes (the code from the article Tutorial: SignalR 2 Getting Started modified ):

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);
        }
    }  
}

Which Startup class contains the configuration SignalR server configuration and mapping SignalR calls , the latter for any project in the Hub to create a route object.

The following is the actual service itself C # source code:

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");

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

Create a JavaScript client

Here, clients may connect with the URL is not the same address, and therefore need to be clearly stated. Create a new ASPNET Web application, and then select Empty template.

Then, use the Package Manager Console to add the following, and make sure the " Default project " is set to " clients " .

PM> Install-Package Microsoft.AspNet.SignalR.JS

Now add this code contains a HTML page (code directly from this article Tutorial: SignalR 2 entry ):

<!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 references. -->
    <!--Reference the jQuery library. -->
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="Scripts/jquery.signalR-2.1.0.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="http://localhost:8080/signalr/hubs"></script>
    <!--Add script to update the page and send messages.-->
    <script type="text/javascript">
        $(function () {
        //Set the hubs URL for the connection
            $.connection.hub.url = "http://localhost:8080/signalr";
            
            // Declare a proxy to reference the hub.
            var chat = $.connection.myHub;
            
            // Create a function that the hub can call to broadcast messages.
            chat.client.addMessage = function (name, message) {
                // Html encode display name and message.
                var encodedName = $('<div />').text(name).html();
                var encodedMsg = $('<div />').text(message).html();
                // Add the message to the page.
                $('#discussion').append('<li><strong>' + encodedName
                    + '</strong>:  ' + encodedMsg + '</li>');
            };
            // Get the user name and store it to prepend to messages.
            $('#displayname').val(prompt('Enter your name:', ''));
            // Set initial focus to message input box.
            $('#message').focus();
            // Start the connection.
            $.connection.hub.start().done(function () {
                $('#sendmessage').click(function () {
                    // Call the Send method on the hub.
                    chat.server.send($('#displayname').val(), $('#message').val());
                    // Clear text box and reset focus for next comment.
                    $('#message').val('').focus();
                });
            });
        });
    </script>
</body>
</html>

Please note that if you choose to debug Windows services rather than " service " run window it is best to start a service project and make sure it is running, and then another Visual Studio launch the client project instance.

以下调用实际上是异步启动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入门):

/// <reference path="../Scripts/jquery-1.10.2.js">
/// <reference path="../Scripts/jquery.signalR-2.1.1.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 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 = '<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));
            });
        });
    }

    // 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入门):

<!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>

对于每个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<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="">();

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

Hub 上面的类用于定义客户端可以调用的服务器上的方法。

如果任何方法需要等待,那么您可以指定,例如,Task<IEnumerable<Stock>>作为启用异步处理的返回值。有关详细信息,请参阅此处

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<stockticker> _instance =
    new Lazy<stockticker>(() => 
    new StockTicker(GlobalHost.ConnectionManager.GetHubContext<stocktickerhub>().Clients));

private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    // Remainder of ctor ...
}

private IHubConnectionContext<dynamic> 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

Guess you like

Origin blog.csdn.net/mzl87/article/details/91379373