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

使用 Task Factory 提高 Web 应用性能,并使用 SignalR 通过 WebSockets 将数据从服务器广播到客户端

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2014年12月15日

CPOL

11分钟阅读

viewsIcon

35308

downloadIcon

803

在本文中,我将演示两件事:首先是一个非常基础简单的代码片段,展示如何使用任务工厂使我们的 Web 应用程序更具响应性;接下来的部分将介绍如何使用 SignalR 将消息从 Web 服务器广播到所有连接的客户端。

引言

促使我写这篇文章的是我八年前的一个项目。那是一个股票交易 Web 应用程序,其中每个页面都包含三个甚至更多的网格。为了让网格更新实时数据,我们使用了 AJAX 轮询:每两秒钟,每个网格都会向服务器发送一个请求,询问是否有新变化,然后用新数据更新自身。现在,我可以想到在类似场景中可以做很多事情来提高性能,减少网络传输的数据包数量,并通过消除客户端的轮询请求并用服务器广播替换它们来减轻 Web 服务器的压力。

背景

本文包含两部分,一部分展示如何使用任务工厂将服务器端的工作分配给不同的线程,从而减少获取客户端数据所需的时间。另一部分将展示如何构建一个实时股票应用程序,该应用程序将接收来自服务器广播的更新并将其渲染到客户端。

对于第二部分,我将使用 ASP.net SignalR 库,该库允许服务器将数据推送到客户端,而不是等待客户端请求数据。那么标题中为什么会提到WebSocket呢?这是因为 SignalR 将使用新的 WebSocket 传输,前提是 WebSocket 条件可用(IIS 版本、Web 服务器 OS、浏览器兼容性……),否则它将恢复到旧的数据传输方法。微软构建了一个示例,展示了如何使用新的 SignalR,它也是一个股票应用程序,并且有一个非常好的详细文章关于这个主题,链接是 Introduction to SignalR。我使用这篇文章学习了我需要的所有知识,我的示例是通过使用提供的代码片段构建的。但是,我添加了新功能,并以更简单的风格重写了应用程序。您必须安装 Visual Studio 2012 或更高版本。

第一部分

在本节中,我们将构建一个小型 Web 应用程序,其中有一个页面包含三个网格。这些网格将读取模拟 2 秒延迟的方法。首先创建一个新的 ASP.net Web 窗体应用程序,并将其命名为 WebParallelProgramming。

在解决方案资源管理器中,添加一个新的项目类库,命名为 DataLayer。

在此库中,创建一个新类并命名为 Stock。将以下代码复制并粘贴到此类中。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DataLayer
{
    public class Stock
    {
        public string Name { get; set; }
        public string Symbol { get; set; }
        public double LastValue { get; set; }
        public double Change { get; set; }
        public string Currency { get; set; }
        public string ImageName { get; set; }

        public Stock(string name, string symbol, double lastValue, double change, string currency, string imageName)
        {
            this.Name = name;
            this.Symbol = symbol;
            this.LastValue = lastValue;
            this.Change = change;
            this.Currency = currency;
            this.ImageName = imageName;
        }

        public static List<Stock> GetPreciousMetals()
        {
            System.Threading.Thread.Sleep(2000);
            List<Stock> preciousMetals = new List<Stock>();

            preciousMetals.Add(new Stock("First Majestic Silver", "TSX FR", 5.23, 12.72, "CAD", "CA"));
            preciousMetals.Add(new Stock("Newmont", "NYSE NEM", 20.01, 4.87, "USD", "US"));
            preciousMetals.Add(new Stock("Endeavour Silver", "TSX EDR", 2.88, 2.86, "CAD", "CA"));
            preciousMetals.Add(new Stock("Freeport-McMoRan", "NYSE FCX", 25.03, 0, "USD", "US"));
            preciousMetals.Add(new Stock("Petaquilla Minerals", "TSX PTQ", 0.04, 33.33, "CAD", "CA"));

            return preciousMetals;

        }

        public static List<Stock> GetStocks()
        {
            System.Threading.Thread.Sleep(2000);

            List<Stock> stocks = new List<Stock>();

            stocks.Add(new Stock("Bank Audi GDR", "BAG Bank", 15.12, 11, "LBP", "LB"));
            stocks.Add(new Stock("National American Bank", "NAM Bank", 22.1, 3.21, "USD", "US"));
            stocks.Add(new Stock("Mono Software", "MS", 2.3, 7.1, "EURO", "GE"));
            stocks.Add(new Stock("Funds Trusts", "FT", 14.04, 2.9, "USD", "US"));
            stocks.Add(new Stock("Food and Beverages CO", "FB CO", 22.17, 22.12, "CAD", "CA"));

            return stocks;

        }

        public static List<Stock> GetMoneyStocks()
        {
            System.Threading.Thread.Sleep(2000);

            List<Stock> moneyStocks = new List<Stock>();

            moneyStocks.Add(new Stock("European Euro", "EURO", 1.2395, 0.1, "USD", "EU"));
            moneyStocks.Add(new Stock("United Kingdom Pound", "Pound", 1.5709, 3.21, "USD", "GB"));
            moneyStocks.Add(new Stock("Japanese Yen", "Yen", 0.0084, 1.2, "USD", "JA"));
            moneyStocks.Add(new Stock("Canadian Dollar", "CAD", 0.87, 1.2, "USD", "CA"));

            return moneyStocks;

        }
    }
}

在这里,我们创建了一个简单的股票类,包含一些属性和数据检索方法。还要注意 Thread.Sleep(2000) 语句,它将强制该方法在返回数据之前等待 2 秒。

现在下载并解压此 zip 文件: flags.zip。它包含一些国家国旗的图像,将这些图像复制到您的 Web 应用程序的 Images 文件夹中。现在右键单击您的 Web 应用程序,选择 Add-> Skin File。接受 Visual Studio 建议的 Skin1 默认名称,然后单击 OK。当提示将此文件放在 App-Themes 文件夹内时,单击 Yes。复制并粘贴以下内容到皮肤文件中。

<asp:GridView  runat="server" AutoGenerateColumns="false" HeaderStyle-HorizontalAlign="Center" AlternatingRowStyle-BackColor="LightSteelBlue"  > </asp:GridView>

用以下代码替换 Default.aspx 页面中的代码。

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" Theme="Skin1" CodeBehind="Default.aspx.cs" Inherits="WebParallelProgramming._Default" %>

<asp:Content runat="server" ID="FeaturedContent" ContentPlaceHolderID="FeaturedContent">
</asp:Content>
<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
    <asp:Label ID="lblServerResponseTime" runat="server" />
    <h2>Market
    </h2>
    <asp:GridView ID="gvStocks" runat="server">
        <Columns>
            <asp:ImageField DataImageUrlField="ImageName"
                DataImageUrlFormatString="~\Images\{0}.gif"
                AlternateText="Country Photo"
                NullDisplayText="No image on file."
                HeaderText=""
                ReadOnly="true" />
            <asp:BoundField DataField="Name" HeaderText="Company" />
            <asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
            <asp:BoundField DataField="LastValue" HeaderText="Price" />
            <asp:BoundField DataField="Change" HeaderText="Change" />
            <asp:BoundField DataField="Currency" HeaderText="Currency" />

        </Columns>
    </asp:GridView>

    <br />
    <h2>Precious Metals</h2>
    <asp:GridView ID="gvMetals" runat="server">
        <Columns>
            <asp:ImageField DataImageUrlField="ImageName"
                DataImageUrlFormatString="~\Images\{0}.gif"
                AlternateText="Country Photo"
                NullDisplayText="No image on file."
                HeaderText=""
                ReadOnly="true" />
            <asp:BoundField DataField="Name" HeaderText="Company" />
            <asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
            <asp:BoundField DataField="LastValue" HeaderText="Price" />
            <asp:BoundField DataField="Change" HeaderText="Change" />
            <asp:BoundField DataField="Currency" HeaderText="Currency" />

        </Columns>
    </asp:GridView>

    <br />
    <h2>Currenct Stock Exchange</h2>
    <asp:GridView ID="gvMoney" runat="server">
        <Columns>
            <asp:ImageField DataImageUrlField="ImageName"
                DataImageUrlFormatString="~\Images\{0}.gif"
                AlternateText="Country Photo"
                NullDisplayText="No image on file."
                HeaderText=""
                ReadOnly="true" />
            <asp:BoundField DataField="Name" HeaderText="Company" />
            <asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
            <asp:BoundField DataField="LastValue" HeaderText="Price" />
            <asp:BoundField DataField="Change" HeaderText="Change" />
            <asp:BoundField DataField="Currency" HeaderText="Currency" />

        </Columns>
    </asp:GridView>

</asp:Content>


通过右键单击您的 webapp 并选择 Add Reference,为您的项目添加对 DataLayer 库的引用。

用以下代码替换 Default.aspx.cs 文件中的代码。

using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebParallelProgramming
{
    public partial class _Default : Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            DateTime startDate = DateTime.Now;

            gvStocks.DataSource = Stock.GetStocks();
            gvStocks.DataBind();

            gvMetals.DataSource = Stock.GetPreciousMetals();
            gvMetals.DataBind();

            gvMoney.DataSource = Stock.GetMoneyStocks();
            gvMoney.DataBind();

            TimeSpan span = (DateTime.Now - startDate);
            lblServerResponseTime.Text = string.Format("Server reponser time was: {0} seconds", span.Seconds);

        }
    }
}

在这里,我们只是将网格绑定到其数据源,我们还用服务器响应所需的时间填充了一个标签。当然,这不是计算服务器响应时间的正确方法,只是为了简单起见,我们这样写。运行您的应用程序,您应该会看到类似下面的内容,请注意服务器响应时间为 6 秒。

现在我们将使用任务并行编程将这三个网格的数据检索分配给三个不同的线程。用以下代码替换 Load 方法中的代码。

  DateTime startDate = DateTime.Now;

            List<Stock> stocks = new List<Stock>();
            List<Stock> metals = new List<Stock>();
            List<Stock> money = new List<Stock>();

            Task t1 = new Task
(

    () =>
    {
        stocks = Stock.GetStocks();
    }
);

            Task t2 = new Task
(

    () =>
    {
        metals = Stock.GetPreciousMetals();
    }
);

            Task t3 = new Task
(

    () =>
    {
        money = Stock.GetMoneyStocks();
    }
);

            t1.Start();
            t2.Start();
            t3.Start();
            Task.WaitAll(t1, t2, t3);

            gvStocks.DataSource = stocks;
            gvStocks.DataBind();

            gvMetals.DataSource = metals;
            gvMetals.DataBind();

            gvMoney.DataSource = money;
            gvMoney.DataBind();

            TimeSpan span = (DateTime.Now - startDate);
            lblServerResponseTime.Text = string.Format("Server reponser time was: {0} seconds", span.Seconds);


在这里,我们声明了三个新的任务,将每个任务分配给获取特定网格的数据。然后,我们在绑定返回数据到网格之前等待这三个任务完成。运行您的应用程序,注意响应时间,现在已降至两秒。

 

第二部分

在本节中,我们将使用 SignalR 从服务器向所有客户端广播股票更新。要了解更多关于 SignalR 的信息,请查看此链接:  Introduction to SignalR。SignalR 将尽可能使用 WebSocket,如果 WebSocket 的条件不可用,则将使用旧方法。 

现在在我们的应用程序中,服务器将向客户端广播更新,而不是由客户端每隔指定时间轮询服务器以获取更新。这将大大减少服务器和客户端的负载。它还将减少服务器端和客户端之间的网络流量。 

为了稍微理解 WebSocket 的强大之处,websocket.org 进行了一个实验,在一个用例中,有100,000个客户端每秒通过 HTTP 请求进行轮询,总网络吞吐量为665 Mb/秒。当相同数量的客户端每秒通过 WebSocket 从服务器接收一条消息时,总网络吞吐量为1.526 Mb/秒!非常惊人,您可以在此处找到链接: http://www.websocket.org/quantum.html。下图来自同一链接,显示了“轮询和 WebSocket 应用程序之间不必要的网络吞吐量开销的比较”。

其中

  • 用例 A:1,000 个客户端每秒接收 1 条消息。
  • 用例 B:10,000 个客户端每秒接收 1 条消息。
  • 用例 C:100,000 个客户端每秒接收 1 条消息。

在微软构建的示例中,服务器将逐个广播每只股票的更新,而我们将一次广播整个更新列表,服务器将广播更新的股票列表,客户端将消化这些更新。我们还将根据股票的变动更改更新股票的颜色。

首先,通过打开 Tools->Library Package Manager->Package Manager Console 并运行命令:install-package Microsoft.AspNet.SignalR 来将 SignalR 添加到您的项目中。

现在,由于我们将使用 SignalR Hub API 来处理客户端-服务器通信,因此我们需要创建一个派生自 SignalR Hub 类的类,该类将负责接收来自客户端的连接和方法调用。右键单击您的项目并添加一个名为“StockBroadcasterHub.cs”的类。将类中的代码替换为以下内容。

using DataLayer;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebParallelProgramming
{
    //The Hub class provides methods that allow clients to communicate with the server using SignalR connections connected to a Hub.
    [HubName("stockBroadcaster")]
    public class StockBroadcasterHub : Hub//Don't forget to inherit from the Hub class
    {
        private readonly StockBroadcaster _stockBroadcaster;

        public StockBroadcasterHub() : this(StockBroadcaster.Instance) { }

        public StockBroadcasterHub(StockBroadcaster stockBroadcaster)
        {
            _stockBroadcaster = stockBroadcaster;
        }

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

注意:您也可以右键单击项目并选择 Add->SignalR Hub Class (v2),这将为您添加必要的属性。

此类用于定义客户端可以在服务器上调用的方法,例如 GetAllStocks() 方法。在本例中,我们不会实现来自客户端的调用,只进行服务器广播,但了解这一点很有用。此外,客户端也使用此 Hub 打开与服务器的连接。

HubName 属性用于指定 Hub 在客户端如何被引用。我们稍后将在 JavaScript 代码中看到如何使用它。如果您不使用此属性,则客户端上的默认名称将是类名称的驼峰式大小写版本,在本例中为 stockBroadcaster。

现在,由于每次客户端连接或调用时都会创建一个新的 Hub 类实例,因此我们需要添加一个类来存储股票数据,执行随机加减股票价格并向客户端广播更新。 右键单击您的项目并添加一个名为“StockBroadcaster.cs”的类。用以下代码替换类模板代码。

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

namespace WebParallelProgramming
{
    public class StockBroadcaster
    {
        //Declare and initialze a TimeSpan that will be used by the timer which will be scheduled to send updates to clients
        private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(2000);

        //Declare a random that will help us in deciding which stocks to update, so that we don't have to update all the stocks at the same time.
        private readonly Random _willUpdate = new Random();

        //declare the timer
        private readonly Timer _timer;

        //Used to check if other threads are also updating the same instance, the volatile keyword is used to ensure thread safety.
        private volatile bool _updatingStockPrices = false;

        //This is the lock that will be used to ensure thread safety when updating the stocks.
        private readonly object _updateStockPricesLock = new object();

        //Encapsulates all information about a SignalR connection for a Hub
        private IHubConnectionContext<dynamic> Clients
        {
            get;
            set;
        }

        //Initialize the static Singleton instance. Lazy initialization is used to ensure that the instance creation is threadsafe.
        private readonly static Lazy<StockBroadcaster> _instance = new Lazy<StockBroadcaster>(() => new StockBroadcaster(GlobalHost.ConnectionManager.GetHubContext<StockBroadcasterHub>().Clients));

        //Initialize the dictionary that will be be used to hold the stocks that will be returned to the client. We used
        //the ConcurrentDictionary for thread safety.If you used a Dictionary object make sure to explicitly lock the dictionary before making changes to it.
        private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

        //Since the constructer is marked as private, then the static _instance is the only instance that can be created from this class
        private StockBroadcaster(IHubConnectionContext<dynamic> clients)
        {
            Clients = clients;

            //Here Fill the Stocks Dictionary that will be broadcasted 
            _stocks.Clear();//Clear the dictionary
            var stocks = new List<Stock>();
            stocks = Stock.GetStocks();
            stocks.AddRange(Stock.GetMoneyStocks());
            stocks.AddRange(Stock.GetPreciousMetals());
            stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));

            _timer = new Timer(UpdateStocks, null, _refreshRate, _refreshRate); //initialize the timer

        }

        /// <summary>
        /// The singelton instance exposed as a public porperty
        /// </summary>
        public static StockBroadcaster Instance
        {
            get
            {
                return _instance.Value;
            }
        }

        /// <summary>
        /// Will return all the stocks
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Stock> GetAllStocks()
        {
            return _stocks.Values; // here we return the dictionary.
        }

        /// <summary>
        /// Will update all the stocks in the dictionary
        /// </summary>
        /// <param name="state"></param>
        private void UpdateStocks(object state)
        {
            lock (_updateStockPricesLock)
            {
                if (!_updatingStockPrices)
                {
                    List<Stock> stocks = new List<Stock>();

                    _updatingStockPrices = true;

                    foreach (var stock in _stocks.Values)
                    {
                        if (TryUpdateStockPrice(stock))
                        {
                            stocks.Add(stock);

                        }
                    }

                    BroadcastAllStocksPrices(stocks);//Broadcast the updated stocks to the clients

                    _updatingStockPrices = false;
                }
            }
        }

        /// <summary>
        /// Will update an individual stock
        /// </summary>
        /// <param name="stock">The stock to be updated</param>
        /// <returns></returns>
        private bool TryUpdateStockPrice(Stock stock)
        {
            // Randomly choose whether to update this stock or not
            var randomUpdate = _willUpdate.NextDouble();
            if (randomUpdate > 0.3)//To increase the possibility of updating this stock, replace 0.3 with a bigger number, but less than 1
            {
                return false;
            }

            // Update the stock price
            var random = new Random((int)Math.Floor(stock.LastValue));
            double percentChange = random.NextDouble() * 0.1;
            bool isChangePostivie = random.NextDouble() > .51;//To check if we will subtract or add the change value, the random.NextDouble will return a value between 0.0 and 1.0. Thus it is almost a fifty/fifty chance of adding or subtracting
            double changeValue = Math.Round(stock.LastValue * percentChange, 2);
            changeValue = isChangePostivie ? changeValue : -changeValue;

            double newValue = stock.LastValue + changeValue;
            stock.Change = newValue - stock.LastValue;
            stock.LastValue = newValue;
            return true;
        }

        /// <summary>
        /// Will broadcast a single stock to the clients
        /// </summary>
        /// <param name="stock">The stock to broadcast</param>
        private void BroadcastStockPrice(Stock stock)
        {
            Clients.All.updateStockPrice(stock);//This updateStockPrice method will be called at the client side, in the clients' Browsers, with the stock parameter inside it.
        }

        /// <summary>
        /// Will broadcast all updated stocks to the clients
        /// </summary>
        /// <param name="stocks">The stocks to broadcast</param>
        private void BroadcastAllStocksPrices(List<Stock> stocks)
        {
            Clients.All.updateAllStocksPrices(stocks);//This updateStockPrice method will be called at the client side, in the clients' Browsers, with the stock parameter inside it.
        }

    }
}

首先,我们创建了一个 TimeSpan,它将是我们的服务器广播的刷新率,将其初始化为 2000 毫秒。然后,我们将创建一个 Random 变量并命名为 _willUpdate。这将决定是否更新给定的股票。

接下来,我们将声明一个计时器,用于每 2 秒更新和广播股票数据。现在,由于多个线程将在类的同一实例上运行,我们将使用锁来确保线程安全。为什么只使用一个实例?因为这是一个 Web 应用程序,为了让所有用户都能看到相同的数据,必须存在一个单一的实例,否则我们将不得不从数据库或其他数据源读取数据(这是真实生活中的场景)。 _updateStockPricesLock 将是用于锁定机制的对象。

现在,我们将创建一个 IHubConnectionContext<dynamic> 对象并命名为 Clients。它将用于封装关于 Hub 的 SignalR 连接的所有信息。

在此下方,我们将初始化静态单例实例,该实例将提供和更新我们的数据。由于这是唯一允许创建的实例,因此我们将构造函数声明为私有。Lazy initialization 用于确保实例创建是线程安全的。此外,声明一个字典来存储我们的股票。我们还使用了一种特殊的 Dictionary,称为 ConcurrentDictionary,它用于创建“可以被多个线程并发访问的键/值对的线程安全集合。

//Initialize the static Singleton instance. Lazy initialization is used to ensure that the instance creation is threadsafe.
private readonly static Lazy<StockBroadcaster> _instance = new Lazy<StockBroadcaster>(() => new StockBroadcaster(GlobalHost.ConnectionManager.GetHubContext<StockBroadcasterHub>().Clients));

//Initialize the dictionary that will be be used to hold the stocks that will be returned to the client. We used
        //the ConcurrentDictionary for thread safety.If you used a Dictionary object make sure to explicitly lock the dictionary before making changes to it.
        private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

        //Since the constructer is marked as private, then the static _instance is the only instance that can be created from this class
        private StockBroadcaster(IHubConnectionContext<dynamic> clients)
        {
            Clients = clients;

            //Here Fill the Stocks Dictionary that will be broadcasted 
            _stocks.Clear();//Clear the dictionary
            var stocks = new List<Stock>();
            stocks = Stock.GetStocks();
            stocks.AddRange(Stock.GetMoneyStocks());
            stocks.AddRange(Stock.GetPreciousMetals());
            stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));

            _timer = new Timer(UpdateStocks, null, _refreshRate, _refreshRate); //initialize the timer

        }

 

UpdateStocks 方法将更新字典中的所有股票,将更新的股票放入泛型列表中,然后将此列表广播给客户端。

  /// <summary>
        /// Will update all the stocks in the dictionary
        /// </summary>
        /// <param name="state"></param>
        private void UpdateStocks(object state)
        {
            lock (_updateStockPricesLock)
            {
                if (!_updatingStockPrices)
                {
                    List<Stock> stocks = new List<Stock>();

                    _updatingStockPrices = true;

                    foreach (var stock in _stocks.Values)
                    {
                        if (TryUpdateStockPrice(stock))
                        {
                            stocks.Add(stock);

                        }
                    }

                    BroadcastAllStocksPrices(stocks);//Broadcast the updated stocks to the clients

                    _updatingStockPrices = false;
                }
            }
        }

下一个方法是 TryUpdateStockPrice。基于名为 randomUpdate 的布尔值,它将决定是否更新股票。如果股票需要更新,其最后价格将根据名为 changeValue 的变量修改。我们还将更新 Stock.Change 属性。

/// <summary>
        /// Will update an individual stock
        /// </summary>
        /// <param name="stock">The stock to be updated</param>
        /// <returns></returns>
        private bool TryUpdateStockPrice(Stock stock)
        {
            // Randomly choose whether to update this stock or not
            var randomUpdate = _willUpdate.NextDouble();
            if (randomUpdate > 0.3)//To increase the possibility of updating this stock, replace 0.3 with a bigger number, but less than 1
            {
                return false;
            }

            // Update the stock price
            var random = new Random((int)Math.Floor(stock.LastValue));
            double percentChange = random.NextDouble() * 0.1;
            bool isChangePostivie = random.NextDouble() > .51;//To check if we will subtract or add the change value, the random.NextDouble will return a value between 0.0 and 1.0. Thus it is almost a fifty/fifty chance of adding or subtracting
            double changeValue = Math.Round(stock.LastValue * percentChange, 2);
            changeValue = isChangePostivie ? changeValue : -changeValue;

            double newValue = stock.LastValue + changeValue;
            stock.Change = newValue - stock.LastValue;
            stock.LastValue = newValue;
            return true;
        }

最后是广播方法,此方法将调用一个客户端函数,即驻留在客户端计算机上的 JavaScript 方法。它还将股票参数传递给该方法,然后这些股票将在客户端解析以更新必要的单元格。请注意,如果您希望更新单个股票,我也保留了一个名为 BroadcastStockPrice 的方法。

      /// <summary>
        /// Will broadcast all updated stocks to the clients
        /// </summary>
        /// <param name="stocks">The stocks to broadcast</param>
        private void BroadcastAllStocksPrices(List<Stock> stocks)
        {
            Clients.All.updateAllStocksPrices(stocks);//This updateStockPrice method will be called at the client side, in the clients' Browsers, with the stocks parameter inside it.
        }

在我们开始编写客户端代码之前,我们需要在 Web 应用程序中注册 SignalR 路由,添加一个新类并将其命名为“Startup.cs”,或者您可以右键单击项目并选择 Add->OWIN Startup Class。用以下代码替换类代码。

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

[assembly: OwinStartup(typeof(WebParallelProgramming.Startup))]
namespace WebParallelProgramming
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Any connection or hub wire up and configuration should go here
            app.MapSignalR();
        }

    }
}

打开您的 default.aspx 页面并添加以下 JavaScript 链接,请确保它们与您的 jQuery 版本和路径匹配。SignalR 代理脚本文件,在“/signalr/hubs”URL 中指定,是动态生成的,它为我们的 Hub 类上的方法定义了代理方法,在本例中是单个方法 StockBroadcasterHub.GetAllStocks()。

  <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="/Scripts/jquery-1.7.1.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="/Scripts/jquery.signalR-2.1.2.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/signalr/hubs"></script>
    <!--Reference the StockTicker script. -->
    <script src="StockManager.js"></script>

现在,我们将创建客户端引擎,它将接收来自服务器的广播并使用新数据更新我们的网格视图。右键单击您的项目并添加一个新的 JavaScript 文件,命名为“StockManager.js”。在其中,复制并粘贴以下代码。

$(function () {

    var stockBroadcaster = $.connection.stockBroadcaster; // the generated client-side hub proxy, use the same name as that used in the StockBroadCasterHub HubName attribute

    function init() {
       
    }

    // Add a client-side hub method that the server will call to deliver the client updates on a given stock
    stockBroadcaster.client.updateStockPrice = function (stock) {
        //If you want to update a single stock, write your code here
    }

    // Add a client-side hub method that the server will call to deliver the client all the updated stocks
    stockBroadcaster.client.updateAllStocksPrices = function (stocks) {
        //clear all the tds with updated background
        $("td.updatedStockP").removeClass("updatedStockP");
        $("td.updatedStockN").removeClass("updatedStockN");

        $.each(stocks, function (index, stock) {//Loop through the stocks recieved from the server
            var stockSymbol = stock.Symbol; //Get the stock Symbol, we will assume it is unique, you can use other properties to get the uninque value, like an auto generated ID
            $('td').filter(function () {
                return $(this).text() === stockSymbol;//Use this to get only the td with a text equal to the stock symbol, then update its value and background color accordingly.
            }).next().html(stock.LastValue.toFixed(2)).addClass(stock.Change > 0 ? "updatedStockP" : "updatedStockN").next().html(stock.Change.toFixed(2)).addClass(stock.Change > 0 ? "updatedStockP" : "updatedStockN");

        });

    }

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

});

注意 stockBroadcaster hub 名称,它应与 StockBroadCasterHub HubName 属性中使用的名称匹配。首先,我们将使用 connection 对象获取 hub 代理,然后使用命令打开连接

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

 之后,我们将定义我们的 updateAllStockPrices 方法,还记得这个方法吗?我们在 StockBroadcaster.BroadcastAllStocksPrices() 服务器方法中调用的那个。

在此 JavaScript 函数中,我们将首先清除所有标记为已更新股票单元格的单元格(我们稍后将创建 CSS 类)。然后,我们将遍历我们从服务器接收到的股票数组,这是我们在 StockBroadcaster.BroadcastAllStocksPrices() 方法中传递的 stock 参数。

Clients.All.updateAllStocksPrices(stocks);//This updateStockPrice method will be called at the client side, in the clients' Browsers, with the stocks parameter inside it.

这是一个 JSON 对象数组,这意味着我们可以调用此数组项的 Stock 对象的属性。第一个挑战是找到每只股票在三个网格视图中的对应行,为此,我们将假设股票代码是唯一的,并将其用于捕获特定于该股票的行。然后,我们将使用 Jquery .next() 方法获取该单元格的下一个同级元素,即 LastValue 列。之后,我们将使用 .html() 方法写入新值。.toFixed(2) 用于将数字四舍五入到最近的两位小数。之后,我们使用 .addClass() 方法为单元格设置样式,如果变化为正,则为绿色,如果变化为负,则为鲑鱼红色。然后,我们将对 Change 列做同样的事情。

最后,我们将 CSS 类添加到我们的 .css 文件中,将以下内容复制并粘贴到 Content 文件夹中的 Site.css 文件中。

.updatedStockP {
background-color:lightgreen;
}

.updatedStockN {
background-color:lightsalmon;
}

另外,在 Site.Master 页面中添加以下内容,将其链接到 CSS 文件。

 <link href="Content/Site.css" rel="stylesheet" type="text/css" />

运行您的项目并检查数据更新,如果颜色没有变化,请按 Ctrl + F5 刷新页面并加载新的 CSS 文件。价格上涨的股票将显示为绿色,价格下跌的股票将显示为红色。由于我们在 DataLayer 类中进行了 Thread.Sleep() 操作,因此数据开始更新需要一些时间。您可以从文章顶部的链接下载该应用程序的源代码。

感谢您的关注 : )。

 

© . All rights reserved.