SignalR系列教程:服務器廣播與主動數據推送


本篇是本系列入門篇的最后一遍,由於工作關系,接觸SignalR的時間不是很多。等下次有空的話我會寫一個利用“SignalR”開發一個在線聊天室的系列博文。近期的話我更偏向於更新框架設計相關的文章,到時候我會在文章中分享我在工作中開發的“日志框架”、“緩存框架”,“分布式下載框架”等。有興趣的朋友可以關注我,一起交流。

本篇博文參考:https://www.asp.net/signalr/overview/getting-started/tutorial-server-broadcast-with-signalr

本教程演示如何創建一個 web 應用程序使用 ASP.NET SignalR 2 提供的服務器廣播功能。服務器廣播意味着發送到客戶端的通信由服務器啟動。我們之前聊天室的項目是一個用戶提交數據后,服務器接收到消息,然后把消息廣播給當前所有的用戶。如下圖

 

本教程所講的恰恰相反,我們是由服務器自動把消息推送給當前所有用戶。如股票信息顯示:

 

 

概述

在本教程中,我們將會創建一個股票行情自動收錄的實時應用程序,在其中您想要定期"推"送數據,通知從服務器到所有連接的客戶端。在本教程的第一部分,你將從頭開始創建該應用程序的簡化的版本。在本教程的其余部分中,您會安裝 NuGet 包,其中包含額外的功能,並審查這些功能的代碼。

 

創建項目

我們依然新建一個空項目,並使用“程序包管理控制台”執行“Install-PackAge Microsoft.AspNet.SignaLR”安裝最新版本的SignaLR。

安裝完成后會自動打開“readme.txt”文件,文中告訴我們要新建一個Startup並注冊SignalR。原文如下

To enable SignalR in your application, create a class called Startup with the following:

using Microsoft.Owin;
using Owin;
using MyWebApplication;

namespace MyWebApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
} 

我們按照它的來,新建一個Startup然后在Configuration中注冊SignalR路由。

 

創建代碼

我們創建Stock用來存放股票詳細信息

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

創建StockTicker和StockTickerHub的類

    我們將在StockTickerHub類中定義和JS交互的代碼。我們需要維護股票數據的更新和刪除,但是我們不能在StockTickerHub類中進行操作,因為StockTickerHub類是不保存數據的,如果我們把股票的CURD代碼放在StockTickerHub中可能會造成我們的數據丟失。我們利用VS添加新項選擇集線器V2,並替換成以下代碼

    [HubName("stockTickerMini")]
    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();
        }
    }

StockTickerHub類中,我們公開了一個GetAllStocks的方法,當客戶端連接成功后我們將調用“GetAllStocks”方法用來顯示股票信息。HubName標簽是一個別名,按照傳統我們連接需要用StockTickerHub,但是我們加了HubName后,前段就可以通過stockTickerMini來創建連接。

我們新建一個StockTicker類,把替換成以下代碼

 public class StockTicker
    {
        private static readonly Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

        /// <summary>
        /// ConcurrentDictionary 表示可以由多個線程訪問的安全集合
        /// 用來存放股票代碼以及股票價格
        /// </summary>
        private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

        private readonly object _updateStockPricesLock = new object();

        //stock can go up or down by a percentage of this factor on each change
        private readonly double _rangePercent = .002;

        private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
        private readonly Random _updateOrNotRandom = new Random();

        private readonly Timer _timer;
        private volatile bool _updatingStockPrices = false;

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

            _stocks.Clear();
            var stocks = new List<Stock>
            {
                new Stock { Symbol = "MSFT", Price = 30.31m },
                new Stock { Symbol = "APPL", Price = 578.18m },
                new Stock { Symbol = "GOOG", Price = 570.30m }
            };
            stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));

            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);

        }

        public static StockTicker Instance
        {
            get
            {
                return _instance.Value;
            }
        }

        private IHubConnectionContext<dynamic> Clients
        {
            get;
            set;
        }

        public IEnumerable<Stock> GetAllStocks()
        {
            return _stocks.Values;
        }

        private void UpdateStockPrices(object state)
        {
            lock (_updateStockPricesLock)
            {
                if (!_updatingStockPrices)
                {
                    _updatingStockPrices = true;

                    foreach (var stock in _stocks.Values)
                    {
                        if (TryUpdateStockPrice(stock))
                        {
                            BroadcastStockPrice(stock);
                        }
                    }

                    _updatingStockPrices = false;
                }
            }
        }

        private bool TryUpdateStockPrice(Stock stock)
        {
            var r = _updateOrNotRandom.NextDouble();
            if (r > .1)
            {
                return false;
            }
            var random = new Random((int)Math.Floor(stock.Price));
            var percentChange = random.NextDouble() * _rangePercent;
            var pos = random.NextDouble() > .51;
            var change = Math.Round(stock.Price * (decimal)percentChange, 2);
            change = pos ? change : -change;

            stock.Price += change;
            return true;
        }

        private void BroadcastStockPrice(Stock stock)
        {
            Clients.All.updateStockPrice(stock);
        }

    }

由於多個線程通過要訪問StockTicker,所以StockTicker必須是線程安全的。

好了,現在完成了基本的配置,我們創建一個名為index.html的文件並把它設置為啟動項。index.html的代碼如下

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>ASP.NET SignalR Stock Ticker</title>
    <style>
        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;
        }
    </style>
</head>
<body>
    <h1>ASP.NET SignalR Stock Ticker Sample</h1>

    <h2>Live Stock Table</h2>
    <div id="stockTable">
        <table border="1">
            <thead>
                <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
            </thead>
            <tbody>
                <tr class="loading"><td colspan="5">loading...</td></tr>
            </tbody>
        </table>
    </div>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="/signalr/hubs"></script>
    <script src="StockTicker.js"></script>
</body>
</html>

在項目新建一個名為StockTicker.js的文件,並用以下代碼替換

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

$(function () {

    var ticker = $.connection.stockTickerMini, 
        up = '▲',
        down = '▼',
        $stockTable = $('#stockTable'),
        $stockTableBody = $stockTable.find('tbody'),
        rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';

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

    function init() {
        //建立連接成功后走的代碼
        ticker.server.getAllStocks().done(function (stocks) {
            $stockTableBody.empty();
            $.each(stocks, function () {
                var stock = formatStock(this);
                $stockTableBody.append(rowTemplate.supplant(stock));
            });
        });
    }

   
    ticker.client.updateStockPrice = function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock));

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
    }

    $.connection.hub.start().done(init);

});

$.connection是指SignalR的代理,StockTickerMiniStockTickerHubHubName所設置的別名。

在所有的變量和函數定義后,會在最后一行啟動SignalR連接,並進行初始化。運行起來即可看到效果

請大家注意一下StockTicker類中的BroadcastStockPrice方法,這個方法最終會獲取當前所有的連接用戶,並觸發updateStockPrice方法。在前兩章的時候我們都是由客戶端主動調用Hub類的某一個方法,然后在由方法內部進行觸發前台JS代碼。在本章中,我們定義了一個定時器用來定時更新數據,每當數據發生修改時就會主動觸發updateStockPrice,這也是本章與前兩章不同的地方。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM