使用SignalR實現服務端消息推送

来源:http://www.cnblogs.com/isrocking/archive/2016/12/21/Server-Broadcast-with-SignalR2.html
-Advertisement-
Play Games

這篇教程通過實現一個股票報價的小程式來講解如何使用SignalR進行伺服器端的推送,伺服器會模擬股票價格的波動,並把最新的股票價格推送給所有連接的客戶端。該文章參考的是[Server Broadcast with SignalR 2]這篇教程,很不錯的一篇教程,如果有興趣的話可以查看原文,今天記錄下... ...


概述

這篇文章參考的是Server Broadcast with SignalR 2這篇教程,很不錯的一篇教程,如果有興趣的話可以查看原文,今天記錄下來作為一個學習筆記,這樣今後翻閱會更方便一點。

這篇教程通過實現一個股票報價的小程式來講解如何使用SignalR進行伺服器端的推送,伺服器會模擬股票價格的波動,並把最新的股票價格推送給所有連接的客戶端,最終的運行效果如下圖所示。

運行結果

教程篇

創建項目

1.打開Visual Studio,然後選擇新建項目。
2.在New Project對話框中,點擊Visual C#下的Web,然後新建一個名為SignalR.StockTickerASP.NET Web Application項目。
3.在New ASP.NET視窗中,選擇Empty模板,然後點擊OK來創建項目。

伺服器端代碼

新建一個名為Stock.cs的實體類,用來作為伺服器端推送消息的載體,具體代碼如下。

using System;

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

這個實體類只有SymbolPrice這兩個屬性需要設置,其它屬性將會依據Price自動進行計算。

創建StockTicker和StockTickerHub類

我們將會使用SignalR Hub API來處理伺服器與客戶端的交互,所以新建一個繼承自SignalR Hub的StockTickerHub類來處理客戶端的連接及調用。除此之外,我們還需要維護股票的價格數據以及新建一個Timer對象來定期的更新價格,而這些都是獨立於客戶端的連接的。由於Hub的生命周期很短暫,只有在客戶端連接和調用的時候才會創建新的實例(沒有研究過SignalR的源代碼,我覺得更確切一點兒應該是每當有新的客戶端連接成功時,伺服器就會創建一個新的Hub實例,並通過該實例來與客戶端進行通信,就像Socket通信中伺服器端會將所有的客戶端Socket放到一個統一的集合中進行維護),所以不要把與客戶端連接及調用無關的代碼放置到SignalR Hub類中。在這裡,我們將維護股票數據、模擬更新股票價格以及向客戶端推送股票價格的代碼放置到一個名為StockTicker的類中。

StockTicker與Hub的實例圖解

我們只需要在伺服器端運行一個StockTicker類的實例(單例模式),由於這個StockTicker類維護著股票的價格,所以它也要能夠將最新的股票價格推送給所有的客戶端。為了達到這個目的,我們需要在這單個實例中引用所有的StockTickerHub實例,而這可以通過SignalR Hub的Context對象來獲得。

==Ps: 渣英語,上面2段出處的原文帖在下麵,留做以後查看吧。==

You'll use the SignalR Hub API to handle server-to-client interaction. A StockTickerHub class that derives from the SignalR Hub class will handle receiving connections and method calls from clients. You also need to maintain stock data and run a Timer object to periodically trigger price updates, independently of client connections. You can't put these functions in a Hub class, because Hub instances are transient. A Hub class instance is created for each operation on the hub, such as connections and calls from the client to the server. So the mechanism that keeps stock data, updates prices, and broadcasts the price updates has to run in a separate class, which you'll name StockTicker.

You only want one instance of the StockTicker class to run on the server, so you'll need to set up a reference from each StockTickerHub instance to the singleton StockTicker instance. The StockTicker class has to be able to broadcast to clients because it has the stock data and triggers updates, but StockTicker is not a Hub class. Therefore, the StockTicker class has to get a reference to the SignalR Hub connection context object. It can then use the SignalR connection context object to broadcast to clients.

1.在Solution Explorer中,右鍵項目,通過Add | SignalR Hub Class(V2)新建一個名為StockTickerHub.cs的文件。

2.在StockTickerHub類中輸入下麵這段代碼。

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

這個Hub類用來定義客戶端可以調用的服務端方法,當客戶端與伺服器建立連接後,將會調用GetAllStocks()方法來獲得股票數據以及當前的價格,因為這個方法是直接從記憶體中讀取數據的,所以會立即返回IEnumerable<Stock>數據。如果這個方法是通過其它可能會有延時的方式來調用最新的股票數據的話,比如從資料庫查詢,或者調用第三方的Web Service,那麼就需要指定Task<IEnumerable<Stock>>來作為返回值,從而實現非同步通信,更多信息請參考ASP.NET SignalR Hubs API Guide - Server - When to execute asynchronously

HubName屬性指定了該Hub的別名,即客戶端腳本調用的Hub名,如果不使用HubName屬性指定別名的話,預設將會使用駱駝命名法,那麼它在客戶端調用的名稱將會是stockTickerHub。

接下來我們將會創建StockTicker類,並且創建一個靜態實例屬性。這樣不管有多少個客戶端連接或者斷開,記憶體中都只有一個StockTicker類的實例,並且還可以通過該實例的GetAllStocks方法來獲得當前的股票數據。

4.在項目中創建一個名為StockTicker的新類,併在類中輸入下麵這段代碼。

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

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 = 0.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 > 0.1)
            {
                return false;
            }

            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 BroadcastStockPrice(Stock stock)
        {
            Clients.All.updateStockPrice(stock);
        }
    }
}

由於StockTicker類的實例涉及到多線程,所以該類需要是線程安全的。

將單個實例保存在一個靜態欄位中

在這個類中,我們新建了一個名為_instance的欄位用來存放該類的實例,並且將構造函數的訪問許可權設置成私有狀態,這樣其它的類就只能通過Instance這個靜態屬性來獲得該類的實例,而無法通過關鍵字new來創建一個新的實例。在這個_instance欄位上面,我們使用了Lazy特性,雖然會損失一點兒性能,但是它卻可以保證以線程安全的方式來創建實例。

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

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

每當有客戶端與伺服器建立連接的時候,一個新的StockTickerHub實例將會在一個獨立的線程中被創建,並通過SockTicker.Instance屬性來獲得唯一的StockTicker實例,就像之前介紹的那樣。

使用ConcurrentDictionary來存放股票數據

這個類定義了一個_stocks欄位來存放測試用的股票數據,並且通過GetAllStocks這個方法來進行獲取。我們前面講過客戶端會通過StockTickerHub.GetAllStocks來獲取當前的股票數據,其實就是這裡的股票數據。

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

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 IEnumerable<Stock> GetAllStocks()
{
    return _stocks.Values;
}

為了線程安全,我們使用了ConcurrentDictionary來存放股票數據,當然你也可以使用Dictionary對象來進行存儲,但是在更新數據之前需要進行鎖定。

在這個測試程式中,我們將數據存直接存放在記憶體中,這樣做並沒有什麼問題,但在實際的應用場景中,則需要將數據存放在資料庫之類的文件中以便長久的保存。

定期的更新股票價格

在這個類中,我們定義了一個Timer對象來定期的更新股票的價格。

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

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

Timer對象通過調用UpdateStockPrices方法,並向該方法傳遞一個null來更新股票的價格。在更新之前,我們使用了_updateStockPricesLock對象將需要更新的部份進行鎖定,並通過_updatingStockPrices變數來確定是否有其它線程已經更新了股票的價格。然後通過對每一個股票代碼執行TryUpdateStockPrice方法來確定是否更新股票價格以及股票價格的波動幅度。如果檢測到股票價格變動,將會通過BroadcastStockPrice方法將最新的股票價格推送給每一個連接的客戶端。

我們使用了volatile修飾符來標記_updatingStockPrices變數,該修飾符指示一個欄位可以由多個同時執行的線程修改,聲明為volatile的欄位不受編譯器優化(假定由單個線程訪問)的限制,這樣可以確保該欄位在任何時間呈現的都是最新的值。該修飾符通常用於由多個線程訪問但不使用lock語句對訪問進行序列化的欄位。

private volatile bool _updatingStockPrices = false;

在實際的場景中,TryUpdateStockPrice方法通常會通過調用第三方的Web Service來獲取最新的股票價格,而在這個程式中,我們則是通過隨機數來進行模擬該實現。

通過SignalR Hub的Context對象來實現服務端的推送

因為股票價格變動是在StockTicker對象中,所以這個對象需要調用客戶端的updateStockPrice回調方法來推送數據。在Hub類中,我們可以直接使用API來調用客戶端的方法,但是這個StockTicker類並沒有繼承自Hub,所以無法直接使用這些對象。為了能夠向客戶端廣播數據,StockTicker類需要使用SignalR Hub的Context對象來獲得StokTickerHub類的實例,並用它來調用客戶端的方法。

下麵這段代碼演示了在創建StockTicker類靜態實例的時候,把SignalR的Context引用通過構造函數傳遞給Clients這個屬性。在這裡只需要獲取一次SignalR.Context,這樣做有2個好處,首先是因為獲取SignalR.Context很耗費資源,其次是獲取一次SignalR.Context可以保留消息發送到客戶端的預定義順序(The intended order of messages sent to clients is preserved)。

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

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

    // 構造函數的餘下代碼...
}

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

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

使用Clients屬性,可以使您和在Hub類中一樣,通過它來調用客戶端的方法。在BroadcastStockPrice方法中調用的updateStockPrice方法實際並不存在,呆會我們將會在客戶端的腳本中實現該方法。因為Clients.All是dynamic類型的,也就是說在程式運行的時候會對這個表達式進行動態賦值,所以這裡可以直接使用它。當這個方法被調用的時候,SignalR將會把這個方法的名稱以及參數一併發給客戶端,如果客戶端存在一個名稱為updateStockPrice的方法的話,那麼就會將參數傳遞給該方法並調用它。

Clients.All意味著發送給所有的客戶端,同時SignalR還提供了用來指定具體的客戶端或組的屬性,具體信息可以參考HubConnectionContext

註冊SignalR路由

伺服器需要知道把哪些請求交由SignalR進行操作,為了實現這個功能,我們需要在OWIN的Startup文件中進行相應的設置。

1.首先打開vs的Solution Explorer,在項目上右擊,然後依次點擊Add | OWIN Startup Class按鈕,添加一個名為Startup.cs的類。

2.在Startup.cs類中輸入下麵這段代碼。

using Owin;
using Microsoft.Owin;

[assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]

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

現在我們已經完成了服務端的編碼工作,接下來我們需要完成客戶端的代碼。

編寫客戶端的代碼

1.在項目的根目錄下,創建一個名為StockTicker.html的HTML文件。

2.在HTML文件中輸入下麵這段代碼。

<!DOCTYPE html>
<html>
<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.10.2.js"></script>
    <script src="/Scripts/jquery.signalR-2.1.2.js"></script>
    <script src="/signalr/hubs"></script>
    <script src="StockTicker.js"></script>
</body>
</html>

上面的HTML代碼創建了一個2行5列的表格,因為預設並沒有數據,所以第2行顯示的是“loading”,當程式運行的時候,這個提示行將被實際的數據覆蓋掉。接下來則分別引入了jQuery、SignalR、SignalR代理,以及StockTicker腳本文件。SignalR代理文件(/signalr/hubs)將會根據伺服器端編寫的Hub文件動態的生成相應的腳本(生成關於StockTickerHub.GetAllStocks的相關代碼),如果你願意,你還可以通過SignalR Utilities來手動生成腳本文件,但是需要在MapHubs方法中禁用動態文件創建的功能。

註意:請確保StockTicker.html文件中引入的腳本文件在你的項目中是實際存在的。

3.在Solution Explorer中,右擊StockTicker.html,然後點擊Set as Start Pagae菜單。

4.在項目的根目錄下創建一個名為StockTicker.js的腳本文件。

5.在腳本文件中,輸入下麵這段代碼。

// 自定義的模板方法
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,  // 客戶端的Hub代理
        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代理,下麵這行代碼表示將StockTickerHub類的代理的引用保存在變數ticker中,代理的名稱即為伺服器端通過[HubName]屬性設置的名稱。

// 客戶端引用的代碼
var ticker = $.connection.stockTickerMini

// 伺服器端的代碼
[HubName("stockTickerMini")]
public class StockTickerHub : Hub

客戶端的代碼編寫好之後,就可以通過最後的這行代碼來與伺服器建立連接,由於這個start方法執行的是非同步操作,並會返回一個jQuery延時對象,所以我們要使用jQuery.done函數來處理連接成功之後的操作。

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

init方法會調用服務端的getAllStocks方法,並將返回的數據顯示在Table中。也許你可能註意到這裡的getAllStocks方法名和伺服器端的GetAllStocks其實並不一樣,這是因為服務端我們預設會使用帕斯卡命名法,而SignalR會在生成客戶端的代理類時,自動將服務端的方法改成駱駝命名法,不過該規則只對方法名及Hub名稱有效,而對於對象的屬性名,則仍然和伺服器端的一樣,比如stock.Symbol、stock.Price,而不是stock.symbol、stock.price。

// 客戶端的代碼
function init() {
    ticker.server.getAllStocks().done(function (stocks) {
        $stockTableBody.empty();
        $.each(stocks, function () {
            var stock = formatStock(this);
            $stockTableBody.append(rowTemplate.supplant(stock));
        });
    });
}

// 伺服器端的代碼
public IEnumerable<Stock> GetAllStocks()
{
    return _stockTicker.GetAllStocks();
}

如果你想在客戶端使用與伺服器商相同的名稱(包括大小寫),或者想自己定義其它的名稱,那麼你可以通過給Hub方法加上HubMethodName標簽來實現這個功能,而HubName標簽則可以實現自定義的Hub名稱。

在這個init方法中,我們會遍歷服務端返回的股票數據,然後通過調用formatStock來格式化成我們想要的格式,接著通過supplant方法(在StockTicker.js的最頂端)來生成一條新行,並把這個新行插入到表格裡面。

這個init方法其實是在start方法完成非同步操作後作為回調函數執行的,如果你把init作為一個獨立的JavaScript語句放在start方法之後的話,那麼程式將會出錯,因為這樣會導致服務端的方法在客戶端還沒有與伺服器建立連接之前就被調用。

當伺服器端的股票價格變動的時候,它就會通過調用已連接的客戶端上的updateStockPrice方法來更新數據。為了讓伺服器能夠調用客戶的代碼,我們需要把updateStockPrice添加到stockTicker代理的client對象中,代碼如下。

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

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

該updateStockPrice方法和init方法一樣,通過調用formatStock來格式化成我們想要的格式,接著通過supplant方法(在StockTicker.js的最頂端)來生成一條新行,不過它並不是將該新行追加到Table中,而是找到Table中現有的行,然後使用新行替換它。

測試程式

1.按下F5啟動程式,就會看到Table中顯示的“loading...”,不過緊接著便會被伺服器端的股票數據替換掉,並且這些股票數據會隨著伺服器的推送而不停的發生改變。

運行結果,顯示loading...

運行結果,獲得伺服器端的股票數據

運行結果,顯示伺服器端推送數據

2.複製頁面的地址,並用其它的瀏覽器打開,就會看到相同的數據,以及相同的價格變化。

3.關閉所有的瀏覽器,然後重新在瀏覽器中打開這個頁面,就會看到這次頁面顯示的速度(股票價格推送)要比第一次快的多,而且第一次看到的股票的價格是有數據的,而不是像第一次那樣都顯示為0,這是因為伺服器端的StockTicker靜態實例仍然在運行。

輸出日誌

SignalR內置了日誌功能,你可以在客戶端選擇開啟該功能來幫助你調試程式,接下來我們將會通過開啟SignalR的日誌功能來展示一下在不同的環境下SignalR所使用的傳輸技術,大至總結如下:

在伺服器端及客戶端都支持的情況下,SignalR預設會選擇最佳的傳輸方式。

1.打開StockTicker.js,然後在客戶端與服務端建立連接之前加上下麵這段代碼。

// Start the connection
$.connection.hub.logging = true;
$.connection.hub.start().done(init);

2.重新運行程式,並打開瀏覽器的開發者工具,選擇控制台標簽,就可以看到SignalR輸出的日誌(如果想看到全部的日誌,請刷新頁面)。

如果你是在Windows 8(IIS 8)上用IE10打開的話,將會看到WebSocket的連接方式。

IE-WebSocket連接方式

如果你是在Windows 7(IIS 7.5)上用IE10打開的話,將會看到使用iframe的連接方式。

IE-IFrame連接方式

Windows 8(IIS 8)上用Firefox的話,將會看到WebSocket的連接方式。

Firefox-WebSocket連接方式

在Windows 7(IIS 7.5)上用Firefox打開的話,將會看到使用Server-sent events的連接方式。

Firefox-Server-sent連接方式

安裝並查看完整的StockTicker.Sample代碼

至此,我們已經實現了最基本的服務端推送功能,如果你想查看更多功能的話,可以通過NuGet包管理器來安裝這個程式的完整版本(Microsoft.AspNet.SignalR.Sample),如果你沒有按照上面的教程操作而是直接從NuGet伺服器上獲取這個測試程式的話,那麼你可能需要在OWIN的Startup類中進行相關的設置,具體說明可以參考文件夾內的readme.txt文件。

安裝SignalR.Sample的NuGet包

1.在Solution Explorer內右擊項目,然後點擊Manage NuGet Packages

2.在Manage NuGet Packages對話框中,選擇Online,然後在搜索框中輸入SignalR.Sample,當搜索結果出來後,在SignalR.Sample包的後面點擊Install來進行安裝。

安裝SignalR.Sample包

3.在Solution Explorer里,將會看到項目根目錄多了一個名為SignalR.Sample的文件夾,這裡便是全部的代碼。

4.在SignalR.Sample文件夾下,右擊StockTicker.html,選擇Set As Start Page,將其設置為啟動文件。

註意:安裝SignalR.Sample包可能會改變你本地原用的jQuery的版本,在啟動程式之前要註意核對文件的版本是否正確。

運行程式

運行程式後,你會看到和之前類似的表格,不過除了在表格裡顯示最新的股票價格之外,該頁面還會有一個水平的滾動條來顯示相同的股票價格。不過與前面的教程不同的是,這個頁面需要你點擊Open Market按鈕之後才會開始接收伺服器的推送數據。

SignalR.Sample運行結果

當點擊Open Market之後,Live Stock Ticker開始滾動顯示股票價格,而在Table裡面則會通過不同的顏色來區分股價的上漲以及下跌。

SignalR.Sample運行結果2

點擊Close Market按鈕以後,Live Stock TableLive Stock Ticker將會停止顯示股票價格的波動,而此時如果點擊Reset按鈕的話,那麼頁面上的股票價格將會變成初始狀態。而對於Live Stock Ticker的實現,和上面的Table類似,只不過是使用了<li>標簽,以及用到了jQuery的animate函數。

Live Stock Ticker的HTML代碼:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

Live Stock Ticker的CSS代碼:

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

使其滾動的jQuery代碼:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

在伺服器端添加客戶端可以調用的方法

和之前一樣,我們需要SignalRHub類中添加可供客戶端調用的方法。

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

OpenMarket、CloseMarket和Reset方法對應著頁面頂部的3個按鈕,只要有一個客戶端操作了這3個按鈕,那麼所有連接的客戶端就都會受到影響,因為每一個用戶都可以設置推送狀態,並將該狀態廣播給所有的客戶端。

在StockTicker類中,這個推送狀態通過MarketState屬性來維護,它是一個MarketState枚舉類型。

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

為了確保線程安全,在StockTicker類中修改推送狀態的時候,需要進行加鎖處理。

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

為了確保線程安全,我們為_marketState欄位加上volatile標識符。

private volatile MarketState _marketState;

BroadcastMarketStateChange和BroadcastMarketReset方法與之前寫的BroadcastStockPrice方法很像,只不過調用客戶端的方法有區別而已。

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

客戶端可以調用的新增函數

現在updateStockPrice方法要負責維護table和ul數據的顯示,並通過jQuery.Color來為股票價格的上漲或者下跌進行著色。

SignalR.StockTicker.js里新增的方法通過推送狀態(MarketState)來啟用或者禁用操作按鈕,同時還決定著table及ul里的數據是否刷新。我們可以通過jQuery.extend方法來把這幾個方法添加到ticker.client對象中。

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

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

建立連接後客戶端的其它設置

當客戶端與伺服器端建立連接後,還有一些額外的工作需要做,比如根據當前的推送狀態來決定調用伺服器端的方法,以及將調用伺服器端的方法綁定到相應的按鈕上。

$.connection.hub.start()
    .pipe(init)
    .pipe(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();
        });
    });

在done方法中綁定按鈕事件是為了確保客戶端在與伺服器建立連接後才能夠調用伺服器的方法。

以上就是SignalR.Sample的主要代碼,如果有興趣的話,可以通過Install-Package Microsoft.AspNet.SignalR.Sample來安裝並查看完整的代碼。



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 主要是軟體源出現了問題 我做的方式可能比較粗暴 ls -l /etc/yum.repos.d/ /*查看軟體源*/ rm -rf /etc/yum.repos.d/ /*全刪了*/ mkdir /etc/yum.repos.d/ /*新建*/ wget -O /etc/yum.repos.d/Cen ...
  • 源代碼軟體的優點: 獲得最新版,能及時修複bug; 能自行修改和定製; 源代碼打包形式: .tar.gz和.tar.bz2格式居多; 完整性校驗: md5sum校驗工具 確認源代碼編譯環境: 需安裝支持C/C++程式語言的編譯器。 編譯安裝過程: 下載並解壓—>./configure配置(安裝目錄、... ...
  • /****************************************** *效果圖(我不是給這游戲打廣告,只是隨手拿了一張圖而已) *說明:1.圖片是24位或32位bmp圖 2.屏幕是32位屏幕 3.不同的設備,可能設備文件不同 4.需要在root用戶下執行 ************* ...
  • 操作系統是電腦相關專業本科課程中最重要的課程! 操作系統是電腦相關專業本科課程中最重要的課程! 操作系統是電腦相關專業本科課程中最重要的課程! 重要的事情說三遍。大學讀了兩年半了,學習了3,4門硬體課程。我發現在我的大學中,本科硬體課程總體偏理論,旨在構建電腦硬體理論體系。之前彙編,組成原理 ...
  • 我遇到這個問題的時候查找網上都給出一堆高大上的解決辦法, 然而我的錯誤實際上是用戶名的問題, 很多人以為遠程用戶名就一定是鎖屏狀態下的登錄名, 其實不是,跟自己設置有關,所以首先應該檢查遠程用戶名是否輸入正確。 查看方式:被遠程電腦→win+Break→遠程設置→選擇用戶(在保證如下圖所示選擇的前提 ...
  • 通過以下方式之一定義方法,可以將參數發送至 Main 方法。 【備註】若要在 Windows 窗體應用程式中的 Main 方法中啟用命令行參數,必須手動修改 program.cs 中 Main 的簽名。 Windows 窗體設計器生成的代碼創建沒有輸入參數的 Main。 也可以使 用 Environ ...
  • Main方法是 C# 控制台應用程式或視窗應用程式的入口點。 (庫和服務不要求將 Main 方法作為入口點。) 應用程式啟動時,Main 方法是第一個調用的方法。 C# 程式中只能有一個入口點。 如果有多個類都包含 Main 方法,則必須使用 /main 編譯器選項編譯程式,以指定用作入口點的 Ma ...
  • 工具》選項》 確定後 如圖就可以多行顯示了。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...