【问题标题】:How to implement real time data for a web page如何实现网页的实时数据
【发布时间】:2014-11-07 21:00:57
【问题描述】:

(这是一个问答式问题,旨在成为提出类似问题的人的首选资源。很多人似乎偶然发现了这样做的最佳方法,因为他们不知道所有选项。许多答案将是 ASP.NET 特定的,但 AJAX 和其他技术在其他框架中确实有等价物,例如 socket.io 和 SignalR。)

我有一个在 ASP.NET 中实现的数据表。我想在页面上实时或接近实时地显示对这些基础数据的更改。我该怎么办?

我的模特:

public class BoardGame
    {
    public int Id { get; set;}
    public string Name { get; set;}
    public string Description { get; set;}
    public int Quantity { get; set;}
    public double Price { get; set;}

    public BoardGame() { }
    public BoardGame(int id, string name, string description, int quantity, double price)
        {
        Id=id;
        Name=name;
        Description=description;
        Quantity=quantity;
        Price=price;
        }
    }

在此示例中,我只是将数据存储在 Application 变量中,而不是实际的数据库。我将在我的 Global.asax.cs 的 Application_Start 函数中播种它。

var SeedData = new List<BoardGame>(){
    new BoardGame(1, "Monopoly","Make your opponents go bankrupt!", 76, 15),
    new BoardGame(2, "Life", "Win at the game of life.", 55, 13),
    new BoardGame(3, "Candyland", "Make it through gumdrop forrest.", 97, 11)
    };
Application["BoardGameDatabase"] = SeedData;

如果我使用 Web 表单,我会使用转发器显示数据。

<h1>Board Games</h1>
        <asp:Repeater runat="server" ID="BoardGameRepeater" ItemType="RealTimeDemo.Models.BoardGame">
            <HeaderTemplate>
                <table border="1">
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                        <th>Description</th>
                        <th>Quantity</th>
                        <th>Price</th>
                    </tr>
            </HeaderTemplate>
            <ItemTemplate>
                <tr>
                    <td><%#: Item.Id %></td>
                    <td><%#: Item.Name %></td>
                    <td><%#: Item.Description %></td>
                    <td><%#: Item.Quantity %></td>
                    <td><%#: Item.Price %></td>
                </tr>
            </ItemTemplate>
            <FooterTemplate></table></FooterTemplate>
        </asp:Repeater>

并在后面的代码中加载该数据:

protected void Page_Load(object sender, EventArgs e)
    {
    BoardGameRepeater.DataSource = Application["BoardGameDatabase"];
    BoardGameRepeater.DataBind();
    }

如果这是使用 Razor 的 MVC,它只是对模型的简单 foreach:

@model IEnumerable<RealTimeDemo.Models.BoardGame>
<h1>Board Games</h1>
<table border="1">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Id)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Description)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Quantity)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
    </tr> 
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Id)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Description)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Quantity)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
    </tr>
} 
</table>

让我们使用 Web 表单创建一个用于添加数据的小页面,以便我们可以实时查看数据更新。我建议您创建两个浏览器窗口,以便您可以同时查看表单和表格。

<h1>Create</h1>
<asp:Label runat="server" ID="Status_Lbl" /><br />
Id: <asp:TextBox runat="server" ID="Id_Tb" /><br />
Name: <asp:TextBox runat="server" ID="Name_Tb" /><br />
Description: <asp:TextBox runat="server" ID="Description_Tb" /><br />
Quantity: <asp:TextBox runat="server" ID="Quantity_Tb" /><br />
Price: <asp:TextBox runat="server" ID="Price_Tb" /><br />
<asp:Button runat="server" ID="SubmitBtn" OnClick="SubmitBtn_Click" Text="Submit" />

以及背后的代码:

protected void SubmitBtn_Click(object sender, EventArgs e)
    {
    var game = new BoardGame();
    game.Id = Int32.Parse(Id_Tb.Text);
    game.Name = Name_Tb.Text;
    game.Description = Description_Tb.Text;
    game.Quantity = Int32.Parse(Quantity_Tb.Text);
    game.Price = Int32.Parse(Price_Tb.Text);
    var db = (List<BoardGame>)Application["BoardGameDatabase"];
    db.Add(game);
    Application["BoardGameDatabase"] = db;
    //only for SignalR
    /*var context = GlobalHost.ConnectionManager.GetHubContext<GameHub>();
    context.Clients.All.addGame(game); */           
    }

【问题讨论】:

  • 来自@mason 的 1 个问题和 4 个答案 :)

标签: c# asp.net ajax asp.net-web-api signalr


【解决方案1】:

信号R

这是我最愿意分享的答案,因为它代表了一种更简洁的实现,它是轻量级的,并且在当今的移动(数据受限)环境中运行良好。

多年来有多种方法可以提供从服务器到客户端的“实时”数据推送(或推送数据的外观)。快速短轮询(类似于我基于 AJAX 的答案)、Long PollingForever FrameServer Sent EventsWebSockets 是用于实现此目的的不同传输机制。 SignalR 是一个抽象层,能够根据客户端和服务器的能力选择适当的传输机制。使用 SignalR 最好的部分是它很简单。您不必担心传输机制,编程模型易于理解。

我将定义一个 SignalR 集线器,但将其留空。

public class GameHub : Hub
    {
    }

当我将数据添加到“数据库”时,我将运行以下代码。如果您阅读该问题,您会看到我在“创建”表单中将其注释掉。您需要取消注释。

var context = GlobalHost.ConnectionManager.GetHubContext<GameHub>();
context.Clients.All.addGame(game);

这是我的页面代码:

<h1>SignalR</h1>
        <asp:Repeater runat="server" ID="BoardGameRepeater" ItemType="RealTimeDemo.Models.BoardGame">
            <HeaderTemplate>
                <table border="1">
                    <thead>
                        <tr>
                            <th>Id</th>
                            <th>Name</th>
                            <th>Description</th>
                            <th>Quantity</th>
                            <th>Price</th>
                        </tr>
                    </thead>
                    <tbody id="BoardGameTblBody">
            </HeaderTemplate>
            <ItemTemplate>
                <tr>
                    <td><%#: Item.Id %></td>
                    <td><%#: Item.Name %></td>
                    <td><%#: Item.Description %></td>
                    <td><%#: Item.Quantity %></td>
                    <td><%#: Item.Price %></td>
                </tr>
            </ItemTemplate>
            <FooterTemplate></tbody></table></FooterTemplate>
        </asp:Repeater>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
        <script src="Scripts/jQuery-1.6.4.min.js"></script>
        <script src="Scripts/jquery.signalR-2.1.1.min.js"></script>
        <script src="signalr/hubs"></script>
        <script type="text/javascript">
            var hub = $.connection.gameHub;
            hub.client.addGame = function (game) {
                $("#BoardGameTblBody").append("<tr><td>" + game.Id + "</td><td>" + game.Name + "</td><td>" + game.Description + "</td><td>" + game.Quantity + "</td><td>" + game.Price + "</td></tr>");
            };
            $.connection.hub.start();
        </script>

以及背后的代码:

protected void Page_Load(object sender, EventArgs e)
        {
        BoardGameRepeater.DataSource = Application["BoardGameDatabase"];
        BoardGameRepeater.DataBind();
        }

注意这里发生了什么。当服务器调用context.Clients.All.addGame(game); 时,它正在为连接到GameHub 的每个客户端执行分配给hub.client.addGame 的函数。 SignalR 负责为我连接事件,并自动将服务器上的 game 对象转换为客户端上的 game 对象。最重要的是,没有每隔几秒来回的网络流量,因此非常轻量级。

优点:

  • 网络流量非常少
  • 易于开发,但仍然很灵活
  • 不随请求发送视图状态
  • 不连续轮询服务器。

注意,您可以在客户端为editedGame 添加一个函数,以便将更改的数据轻松推送到客户端(删除相同)。

【讨论】:

  • 是否可以根据 SignalR 中的用户 ID 将数据(每秒)从服务器推送到客户端(数据因用户而异)?
  • 有什么办法吗?你能给我任何参考链接吗?
  • @Anusha 是的,有办法做到这一点。这就是我在之前的评论中所说的。您可以从了解 SignalR 的工作原理开始。我建议您阅读文档等。
  • 非常好。感谢您的比较。需要在repeater的html表中添加id="BoardGameTblBody"
【解决方案2】:

定时器/更新面板

如果您使用的是 Web 窗体,则可以使用称为 UpdatePanel 的控件。 UpdatePanel 能够异步刷新页面的各个部分,而不会导致整个页面的回发。与 asp:Timer 结合使用,您可以根据需要随时更新表。代码如下:

<asp:ScriptManager runat="server" />
        <h1>Board Games (using Update Panel)</h1>
        <asp:Timer runat="server" ID="UP_Timer" Interval="5000" />
        <asp:UpdatePanel runat="server" ID="Game_UpdatePanel">
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="UP_Timer" EventName="Tick" />
            </Triggers>
            <ContentTemplate>
                <asp:Repeater runat="server" ID="BoardGameRepeater" ItemType="RealTimeDemo.Models.BoardGame">
                    <HeaderTemplate>
                        <table border="1">
                            <tr>
                                <th>Id</th>
                                <th>Name</th>
                                <th>Description</th>
                                <th>Quantity</th>
                                <th>Price</th>
                            </tr>
                    </HeaderTemplate>
                    <ItemTemplate>
                        <tr>
                            <td><%#: Item.Id %></td>
                            <td><%#: Item.Name %></td>
                            <td><%#: Item.Description %></td>
                            <td><%#: Item.Quantity %></td>
                            <td><%#: Item.Price %></td>
                        </tr>
                    </ItemTemplate>
                    <FooterTemplate></table></FooterTemplate>
                </asp:Repeater>
            </ContentTemplate>
        </asp:UpdatePanel>

以及背后的代码:

    protected void Page_Load(object sender, EventArgs e)
        {
        BoardGameRepeater.DataSource = Application["BoardGameDatabase"];
        BoardGameRepeater.DataBind();
        }

让我们来谈谈这个是如何工作的。每 5 秒,计时器将触发一个 Tick 事件。这在 UpdatePanel 中注册为异步回发服务器,因此发生部分回发,整个页面生命周期再次运行,因此它在 Page Load 事件上重新加载数据,然后将 UpdatePanel 的内容模板的全部内容替换为新的从服务器生成的数据。让我们看看网络流量的样子:

+5s Client => Server | Run the page lifecycle, send me the contents of the ContentPanel.
Server => Client | Here's the entire contents of the ContentPanel.
+10s Client => Server | Run the page lifecycle, send me the contents of the ContentPanel.
Server => Client | Here's the entire contents of the ContentPanel.
+15s Client => Server | Run the page lifecycle, send me the contents of the ContentPanel.
Server => Client | Here's the entire contents of the ContentPanel.

优点:

  • 易于实施。只需添加一个计时器、一个脚本管理器,然后将转发器包装在更新面板中。

缺点:

  • Heavy:ViewState 随每个请求一起发送到服务器。但是,如果您禁用 ViewState(无论如何都应该这样做),则可以减轻这种影响。
  • Heavy:无论数据是否已更改,您每 5 秒通过线路发送所有数据。这是一大块带宽。
  • 慢:每次部分回发都需要很长时间,因为所有数据都通过网络传输。
  • 难以处理:当您开始添加更多功能时,正确处理部分回发可能会很棘手。
  • 不聪明:即使之前的请求没有完成,由于计时器,它会继续发回。
  • 不聪明:没有简单的方法来处理网络中断。

【讨论】:

    【解决方案3】:

    AJAX 轮询,更好的实现

    与其他基于 AJAX 的答案类似,您可以不断地轮询服务器。但是这一次,我们将使用数据的 ID 列表来响应,而不是响应要显示的数据。客户端将跟踪它已在数组中检索到的数据,然后当它看到已添加新 ID 时,它将向服务器发出单独的 GET 请求以获取数据。

    这是我们的页面代码:

    <h1>Board Games (AJAX Polling Good)</h1>
            <table id="BoardGameTbl" border="1">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                        <th>Description</th>
                        <th>Quantity</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody id="BoardGameTblBody">
                </tbody>
            </table>
            <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
            <script type="text/javascript">
                var loadedGames = [];
                function getListOfGames() {
                    $.ajax({
                        type: "GET",
                        url: "api/GamesApi/GetGameIds",
                        dataType: "json"
                    })
                    .done(function (data) {
                        for (i = 0; i < data.length; i++) {
                            if (loadedGames.indexOf(data[i]) == -1) {
                                loadedGames[loadedGames.length] = data[i];
                                getGame(data[i]);
                            }
                        }
                        setTimeout(getListOfGames, 5000);
                    });
                }
                function getGame(id) {
                    $.ajax({
                        type: "GET",
                        url: "api/GamesApi/GetGame/" + id,
                        dataType: "json"
                    })
                    .done(function (game) {
                        $("#BoardGameTblBody").append("<tr><td>" + game.Id + "</td><td>" + game.Name + "</td><td>" + game.Description + "</td><td>" + game.Quantity + "</td><td>" + game.Price + "</td></tr>");
                    });
                }
                getListOfGames();
            </script>
    

    这是 Web API 控制器:

    namespace RealTimeDemo.Controllers
    {
    public class GamesApiController : ApiController
        {
        [Route("api/GamesApi/GetGameIds")]
        public IEnumerable<int> GetGameIds()
            {
            var data = HttpContext.Current.Application["BoardGameDatabase"] as List<BoardGame>;
            var IDs = data.Select(x => x.Id);
            return IDs;
            }
    
        [Route("api/GamesApi/GetGame/{id}")]
        public BoardGame GetGame(int id)
            {
            var data = HttpContext.Current.Application["BoardGameDatabase"] as List<BoardGame>;
            return data.Where(x => x.Id == id).SingleOrDefault();
            }
        }
    

    现在,这比我的其他基于 AJAX 的答案和 Timer/UpdatePanel 答案要好得多。由于我们仅每 5 秒发送一次 ID,因此对网络资源的压力要小得多。处理没有网络连接的情况也相当简单,或者在加载新数据时执行某种通知,例如抛出noty

    优势

    • 不随请求发送视图状态。
    • 不执行整个页面生命周期
    • 作为轮询的一部分,仅通过网络发送 ID(如果您在请求中发送时间戳,并且仅回复自时间戳以来更改的数据,则可以改进)。仅从数据库中检索新对象。

    缺点 - 我们仍在轮询,每隔几秒生成一个请求。如果数据不经常更改,则您会不必要地耗尽带宽。

    【讨论】:

    • 我怎样才能让它实时?像 pusher.com 这样的图书馆是如何实时做到这一点的??
    【解决方案4】:

    AJAX 轮询,执行不佳

    如果您使用的是 MVC 或 Web 表单,则可以实现一种称为 AJAX 轮询的技术。这将不断向服务器发送 AJAX 请求。服务器将发送包含最新数据的响应。实现起来非常简单。您不必使用jQuery 来使用AJAX,但它使它更容易。此示例将使用Web API 来实现服务器端功能。 Web API 类似于 MVC,它使用路由和控制器来处理请求。它是ASMX Web Services 的替代品。

    这是 Web 表单代码,但它与 MVC 代码非常相似,因此我将省略:

    <h1>Board Games (AJAX Polling Bad)</h1>
            <table id="BoardGameTbl" border="1">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                        <th>Description</th>
                        <th>Quantity</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody id="BoardGameTblBody">
                </tbody>
            </table>
            <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
            <script type="text/javascript">
                function getData() {
                    $.ajax({
                        type: "GET",
                        url: "api/GamesApi/GetGameData",
                        dataType: "json"
                    })
                    .done(function (data) {
                        $("#BoardGameTblBody").empty();
                        for (i = 0; i < data.length; i++) {
                            $("#BoardGameTblBody").append("<tr><td>" + data[i].Id + "</td><td>" + data[i].Name + "</td><td>" + data[i].Description + "</td><td>" + data[i].Quantity + "</td><td>" + data[i].Price + "</td></tr>");
                        }
                        setTimeout(getData, 5000);
                    });
                }
                getData();
            </script>
    

    这是向 Web API 发出请求。 API 正在返回所有游戏的 JSON 表示形式。

    public class GamesApiController : ApiController
        {
        [Route("api/GamesApi/GetGameData")]
        public IEnumerable<BoardGame> GetGameData()
            {
            var data = HttpContext.Current.Application["BoardGameDatabase"] as List<BoardGame>;
            return data;
            }
        }
    

    该方法的整体结果与Timer/UpdatePanel 方法类似。但它不会随请求发送任何视图状态数据,也不会执行较长的页面生命周期过程。您也不必四处寻找是否处于回发状态,或者是否处于部分回发状态。所以我认为这是对 Timer/UpdatePanel 的改进。

    但是,此方法仍然具有 Timer/UpdatePanel 方法的主要缺点之一。您仍然通过每个 AJAX 请求通过网络发送所有数据。如果您查看我的其他基于 AJAX 的答案,您会发现实现 AJAX 轮询的更好方法。

    优势

    • 不随请求发送视图状态。
    • 不执行整个页面生命周期

    缺点

    • 每隔几秒生成一个请求
    • 响应包括所有数据,即使它没有更改

    【讨论】:

      猜你喜欢
      • 2015-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多