ASP.NET和SignalR簡單實現股票行情實時展示和價格變動推送

龐順龍發表於2019-05-11

ASP.NET C# SignalR 簡單實現股票行情實時展示和價格變動推送

本文將從SignalR的簡介、專案新建和實現入手,簡單演示SignalR功能的一部分。

效果見第11步,demo見附件。

1、開發環境

  1. Visual Studio 2013
  2. .Net Framework 4.5
  3. SignalR-2.0.0

2、SignalR簡介

    ASP.NET SignalR 是為 ASP.NET 開發人員提供的一個庫,可以簡化開發人員將實時 Web 功能新增到應用程式的過程。實時 Web 功能是指這樣一種功能:當所連線的客戶端變得可用時伺服器程式碼可以立即向其推送內容,而不是讓伺服器等待客戶端請求新的資料。

    ASP .NET SignalR 是一個ASP .NET 下的類庫,可以在ASP .NET 的Web專案中實現實時通訊。什麼是實時通訊的Web呢?就是讓客戶端(Web頁面)和伺服器端可以互相通知訊息及呼叫方法,當然這是實時操作的。

    WebSockets是HTML5提供的新的API,可以在Web網頁與伺服器端間建立Socket連線,當WebSockets可用時(即瀏覽器支援Html5)SignalR使用WebSockets,當不支援時SignalR將使用其它技術來保證達到相同效果。

    SignalR當然也提供了非常簡單易用的高階API,使伺服器端可以單個或批量呼叫客戶端上的JavaScript函式,並且非常 方便地進行連線管理,例如客戶端連線到伺服器端,或斷開連線,客戶端分組,以及客戶端授權,使用SignalR都非常 容易實現。


    SignalR 將與客戶端進行實時通訊帶給了ASP .NET 。當然這樣既好用,而且也有足夠的擴充套件性。以前使用者需要重新整理頁面或使用Ajax輪詢才能實現的實時顯示資料,現在只要使用SignalR,就可以簡單實現了。

    最重要的是您無需重新建立專案,使用現有ASP .NET專案即可無縫使用SignalR。



3、建立專案

    

    

4、建立stock.cs類,用於股票資訊實體類

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace SignalR.StockTicker
{
    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);
            }
        }
    }
}
stock.cs實體類中的兩個屬性,用於儲存股票名稱程式碼,股票價格,其他剩餘屬性用於決定如何和何時來設定價格欄位,會根據dayopen之間的差異計算各個屬性值的變化


5、使用SignaR集線器的API來處理伺服器到客戶端的互動。一個stocktickerhub類派生類將處理signalr接收到的client連線和呼叫方法。專案中還需要維護股票資料,並執行一個計時器物件,以週期性地觸發價格更新,這個計時器獨立於客戶端連線。而且也不能把這些函式放在一個集線器類中,因為集線器例項是暫時的。一個集線器類例項是在集線器上的每個操作建立的,比如客戶端到伺服器的連線和呼叫。這樣的機制,使股票價格資料、更新,和server推送到client的價格更新已經執行在一個單獨的類,就是stockticker。

    

6、右鍵專案新增SignalR Hub Class (v2)模板類 StockTickerHub.cs,

    

using System.Collections.Generic;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR.StockTicker
{
    [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();
        }
    }
}
7、建立一個新的StockTicker.cs類,用於判斷如何,何時進行股票價格的變動處理


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;


namespace SignalR.StockTicker
{
    public class StockTicker
    {
        // 單一例項物件
        private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

        private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

        private readonly object _updateStockPricesLock = new object();

        // 股票可以按比例上升或下降的百分比
        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 clients)
        {
            Clients = clients;

            _stocks.Clear();
            var stocks = new List<Stock>
            {
                new Stock { Symbol = "微軟", Price = 30.31m },
                new Stock { Symbol = "蘋果", Price = 578.18m },
                new Stock { Symbol = "谷歌", 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 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;
            }

            // 注:這裡應該是進行資料庫實時資料讀取
            // 暫時使用random隨機數值來演示股票行情報價的變動
            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);
        }
    }
}
8、右鍵專案新增啟動檔案Startup.cs,用於處理入口處理


using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
namespace SignalR.StockTicker
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        { 
            //任何一個client連線或集線器連線和配置都會先進這裡
            app.MapSignalR();
        }
    }
}
9、上面8步已經基本將server端處理好,下面處理client頁面端,建立一個StockTicker.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 股票行情實時顯示demo</h1>
    <div id="stockTable">
        <table border="1">
            <thead>
                <tr><th>股票名稱</th><th>開盤價格</th><th>當前價格</th><th>漲跌值</th><th>漲跌幅(%)</th></tr>
            </thead>
            <tbody>
                <tr class="loading"><td colspan="5">載入中...</td></tr>
            </tbody>
        </table>
    </div>
    <script src="Scripts/jquery-1.10.2.min.js"></script>
    <script src="Scripts/jquery.signalR-2.0.0.js"></script>
    <script src="/signalr/hubs"></script>
    <script src="Scripts/StockTicker.js"></script>
</body>
</html>

注意:上面的html程式碼中我用的我本地script指令碼,自行處理。


10、建立通訊指令碼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, //StockTickerHub類,[HubName("stockTickerMini")] 屬性值
        up = '▲',
        down = '▼',
        $stockTable = $('#stockTable'),
        $stockTableBody = $stockTable.find('tbody'),
        rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{DayOpen}</td><td>{Price}</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);
    }

    // 開始client和server連線
    $.connection.hub.start().done(init);

});


  1. var ticker = $.connection.stockTickerMini; 這是指定signalr連線代理,引用StockTickerHub.cs類中的 [HubName("stockTickerMini")]
  2. $.connection.hub.start().done(init); 啟動signalr,開始執行並返回推送的訊息象,這裡的call和response都是函式非同步呼叫的。
  3. function init() 此方法用於初始化顯示的表格,這裡全都是用的 camel 命名規則,如 getAllStocks()
  4. ticker.client.updateStockPrice 用於執行股票價格變動的更新操作
11,以上步驟完成後,編譯通過後,F5測試即可,在不同的多個瀏覽器同時開啟效果如下:



龐順龍最後編輯於:4年前

內容均為作者獨立觀點,不代表八零IT人立場,如涉及侵權,請及時告知。

相關文章