65.9K
CodeProject 正在变化。 阅读更多。
Home

SignalR 与自托管 Windows 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (34投票s)

2019年6月4日

Apache

8分钟阅读

viewsIcon

110661

downloadIcon

829

两个 SignalR 演示项目:自托管 Windows 服务和广播应用程序。

引言

请注意,本文主要源自并基于 Tom Dykstra 和 Tom FitzMacken 的文章 教程:SignalR 2 服务器广播,我们将在本文中构建一个聊天应用程序。

本文介绍了使用 SignalR 的自托管服务的入门信息。请参阅我之前关于 SignalR 与 Web 应用程序的文章 此处,其中也包含有价值的入门信息。

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

此应用程序将使用 Topshelf 构建,这样我们就无需了解 Windows 服务类的复杂性,也无需使用 InstallUtil.exe 执行安装。它还将允许我们像调试控制台应用程序一样简单地调试应用程序。如果您确实想将应用程序安装为 Windows 服务,那么 Topshelf 允许您在以管理员身份运行的命令提示符中简单地键入以下内容。

C:\Users\myUserId> SignalRSelfHostedService install

请在此处下载示例项目源代码 此处此处

背景

最初,互联网的工作方式是用户在浏览器中输入 URL,然后下载网页内容。在用户执行刷新并向 Web 服务器发出另一个请求之前,客户端页面上没有任何变化。

然后出现了 AJAX,它允许网页异步更新,因此如果只需要更新一小部分,用户无需重新加载整个网页。它通过使用 XMLHttpRequest 对象来实现。这种方法的问题是服务器无法主动与客户端建立联系,而使用 SignalR,服务器可以。

此外,还开发了轮询和长轮询等技术。这导致客户端定期向服务器发出请求以更新网页内容。这种方法的问题是它重复执行这些轮询操作会低效地利用计算资源。

另一种用于提高 Web 浏览器响应速度的方法是服务器发送事件 (SSE),但问题是它只被少数浏览器支持。

SignalR 使用多种技术,包括 WebSockets 和 JavaScript,如果特定浏览器上没有 WebSockets,它甚至会使用轮询和长轮询。它抽象了这些细节,让开发人员专注于应用程序的逻辑。

创建服务器

首先创建一个控制台应用程序(如果您使用 Topshelf,可以这样做),或者如果您不使用 Topshelf,则在 Visual Studio 中创建一个 Windows 服务,确保您的项目使用 .NET 4.5 或更高版本

SelfHostedService

然后在包管理器控制台中键入此内容

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 System;
using System.Collections.Generic;
using System.Data;
using Topshelf;

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

在您的 OnStart 方法中,添加以下代码

string url = "https://: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<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 = "https://:8090";
            WebApp.Start(url);
        }

        public void OnStop()
        {
            Log.InfoFormat("SignalRServiceChat: In OnStop");
        }

        public void Dispose()
        {
        }
    }
}

创建 JavaScript 客户端

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

881511/AspProject.png

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

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

现在添加一个带有此代码的 HTML 页面(此代码直接取自文章 教程:SignalR 2 入门

<!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="https://: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 = "https://: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>

如果您选择创建 Windows 服务而不是使用 Topshelf 的控制台应用程序,那么您将按如下方式安装 Windows 服务

Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved.

C:\> installutil SelfHostedServiceSignalRSample.exe

请注意,如果您选择调试 Windows 服务而不是从“服务”窗口运行它,最好先启动服务项目并确保它正在运行,然后在一个单独的 Visual Studio 实例中启动 Client 项目。

以下调用实际上是异步启动 Windows 服务中的 SignalR 服务器的代码

WebApp.Start(url); 

这是聊天应用程序运行时应有的样子

Chat Client

服务器广播功能

上面的代码使用点对点通信功能,其中发送到客户端的通信由一个或多个客户端发起。如果您希望将由服务器发起的通信推送到客户端,则需要添加服务器广播功能。

对于本文的这一部分,我将基于第一个点对点演示应用程序进行构建,因此为了更清楚,请查看第二个名为 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>    

对于每只股票,您需要添加代码(例如,微软的 MSFT)和价格。

创建 StockTicker 和 StockTickerHub 类

添加 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 update 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 服务应用程序中。

致谢

请注意,本文的大部分想法都来自 Patrick Fletcher 的文章 此处 和 Tom Dykstra 与 Tom FitzMacken 的文章 此处,以及 pluralsight 上关于 Topshelf 的课程。

历史

  • 2019-06-04:更新应用程序以使用 Topshelf
  • 2015-03-02:添加了有关使用 SignalR 从服务器向所有客户端广播的详细信息
  • 2019-07-10:添加了有关如何使用 Topshelf 的详细信息,它提供了一种更简单的方式来开始 Windows 服务开发,并且还允许作为控制台应用程序进行调试

参考文献

© . All rights reserved.