使用SignalR ASP.NET Core来简单实现一个后台实时推送数据给Echarts展示图表的功能
什么是 SignalR ASP.NET Core
ASP.NET Core SignalR 是一种开放源代码库,可简化将实时 web 功能添加到应用程序的功能。 实时 web 功能使服务器端代码可以立即将内容推送到客户端。
SignalR ASP.NET Core可以做什么
• 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
• 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。
• 协作应用。 协作应用的示例包括白板应用和团队会议软件。
• 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。
SignalR ASP.NET Core特色
• 自动处理连接管理。
• 可将消息同时发送到所有连接的客户端。
• 可向特定客户端或客户端组发送消息。
• 可缩放以处理不断增加的流量。
• SignalR采用rpc来进行客户端与服务器端之间的通信。
• SignalR会自动选择服务器和客户端的最佳传输方法(WebSockets、Server-Sent事件、长轮询)SignalR可以根据当前浏览器所支持的协议来选择最优的连接方式,从而可以让我们把更多的精力放在业务上而不是底层传输技术上。
哪些浏览器支持SignalR ASP.NET Core
Apple Safari(包含IOS端)、Google Chrome(包括 Android端)、Microsoft Edge、Mozilla Firefox等主流浏览器都支持SignalR ASP.NET Core。
本次我们将实现一个通过SignalR来简单实现一个后台实时推送数据给Echarts来展示图表的功能
首先我们新建一个ASP.NET Core 3.1的web应用
随后我们引用SignalR ASP.NET Core、Jquery和Echarts的客户端库
在项目中我们新建以下目录
Class、HubInterface、Hubs
接着我们在Pages目录下新建如下目录
echarts
在Shared目录中新建一个Razor布局页(_LayoutEcharts.cshtml)
在echarts目录中新建一个Razor页面(Index.cshtml)
在Class目录中新建一个类(ClientMessageModel.cs)
在HubInterface目录中新建一个接口(IChatClient.cs)
在Hub目录中新建一个类(ChatHub.cs)
我们先实现后台逻辑代码,随后在编写前端交互代码。
在IChatClient.cs中,我们主要是定义统一的服务端调用客户端方法的统一方法名(防止每次都要手动输入调用方法是出现失误而导致调用失败的低级错误)
namespace signalr.HubInterface { public interface IChatClient { /// <summary> /// 客户端接收数据触发函数名 /// </summary> /// <param name="clientMessageModel">消息实体类</param> /// <returns></returns> Task ReceiveMessage(ClientMessageModel clientMessageModel); /// <summary> /// Echart接收数据触发函数名 /// </summary> /// <param name="data">JSON格式的可以被Echarts识别的data数据</param> /// <returns></returns> Task EchartsMessage(Array data); /// <summary> /// 客户端获取自己登录后的UID /// </summary> /// <param name="clientMessageModel">消息实体类</param> /// <returns></returns> Task GetMyId(ClientMessageModel clientMessageModel); } }
ClientMessageModel.cs中,我们主要定义的是序列化后的交互用的实体类
namespace signalr.Class { /// <summary> /// 服务端发送给客户端的信息 /// </summary> [Serializable] public class ClientMessageModel { /// <summary> /// 接收用户编号 /// </summary> public string UserId { get; set; } /// <summary> /// 组编号 /// </summary> public string GroupName { get; set; } /// <summary> /// 发送的内容 /// </summary> public string Context { get; set; } } }
在ChatHub.cs中,主要是实现SignalR集线器的核心功能,用来处理客户端<==>服务器交互代码。在这里我们继承了Hub<T>的方法,集成了我们定义的IChatClient接口,从而就可以在方法中直接调用接口名称来和客户端交互。
namespace signalr.Hubs { public class ChatHub : Hub<IChatClient> { public override async Task OnConnectedAsync() { var user = Context.ConnectionId; await Clients.Client(user).GetMyId(new ClientMessageModel { UserId = user, Context = $"回来了{DateTime.Now:yyyy-MM:dd HH:mm:ss}" }); await Clients.AllExcept(user).ReceiveMessage(new ClientMessageModel { UserId = user, Context = $"进来了{DateTime.Now:yyyy-MM:dd HH:mm:ss}" }); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception exception) { var user = Context.ConnectionId; await Clients.All.ReceiveMessage(new ClientMessageModel { UserId = user, Context = $"{user}离开了{DateTime.Now:yyyy-MM:dd HH:mm:ss}" }); await base.OnDisconnectedAsync(exception); } } }
我们重写了Hub的OnConnectedAsync方法,当有客户端连接进来的时候,我们给当前客户端发送一条“回来了”的内容,同时给所有在线的客户端发送一条“进来了”的通知,内容中会带上本次连接所分配给动态Guid编号。(类似与通知大家谁谁上线了)
在OnDisconnectedAsync方法中,当客户端断开连接的时候,会给所有在线客户端发送一条带有离线客户端的Guid的离开消息。(类似通知大家谁谁谁离开了)
在Startup.cs中,我们做以下设置(注入SignalR和注册Hub),同时先把在DEBUG模式下的XSRF禁用,否则访问接口会提示400错误
public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddRazorPages() #if DEBUG //Debug下禁用XSRF防护,方便调试 .AddRazorPagesOptions(o => { o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()); }) #endif ; }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapHub<ChatHub>("/chathub");//注册hub }); }
以上服务端的基架功能就搭建好了,下面我们会来实现后台推送数据给前台Echart的功能。
在_LayoutEcharts.cshtml布局页中,我们实现引用Jquery和Echarts的JS文件,同时编写一个请求后台接口的方法,调用这个方法后,后台就会主动推送多次数据给前台。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width" /> <script src="~/lib/echarts/dist/echarts.min.js"></script> <script src="~/lib/jquery/dist/jquery.js"></script> <title>@ViewBag.Title</title> <script> function Test() { var chartDom = document.getElementById('main'); var myChart = window.echarts.init(chartDom); $.ajax({ url:'/echarts', type:'POST', dateType: 'json', data: { user: user}, beforeSend: function (XHR) { console.log('I am ' + user); myChart.showLoading({ text: '加载中。。。', effect: 'whirling' }); }, success:function(data) { var option = { series: [{ data: data.data }] }; myChart.setOption(option); }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert(errorThrown); }, complete:function(XHR, TS) { myChart.hideLoading(); } }); } </script> </head> <body> <div> @RenderBody() </div> @await RenderSectionAsync("Scripts", required: false) </body> </html>
在echarts目录的Index.cshtml中,我们实现引用Echarts组件,来渲染图表,引用SignalR来实现和服务器端数据实时交互。
@page @model signalr.Pages.echarts.IndexModel @{ ViewBag.Title = "Echarts图标展示(https://www.cnblogs.com/wdw984)"; Layout = "_LayoutEcharts"; } <div id="main" style="width: 800px;height:600px;"></div> <button onclick="Test()">测试</button> <script type="text/javascript"> var app = {}; var chartDom = document.getElementById('main'); var myChart = echarts.init(chartDom); var option; var posList = [ 'left', 'right', 'top', 'bottom', 'inside', 'insideTop', 'insideLeft', 'insideRight', 'insideBottom', 'insideTopLeft', 'insideTopRight', 'insideBottomLeft', 'insideBottomRight' ]; app.configParameters = { rotate: { min: -90, max: 90 }, align: { options: { left: 'left', center: 'center', right: 'right' } }, verticalAlign: { options: { top: 'top', middle: 'middle', bottom: 'bottom' } }, position: { options: posList.reduce(function (map, pos) { map[pos] = pos; return map; }, {}) }, distance: { min: 0, max: 100 } }; app.config = { rotate: -25, align: 'left', verticalAlign: 'middle', position: 'bottom', distance: 15, onChange: function () { var labelOption = { normal: { rotate: app.config.rotate, align: app.config.align, verticalAlign: app.config.verticalAlign, position: app.config.position, distance: app.config.distance } }; myChart.setOption({ series: [{ label: labelOption }, { label: labelOption }, { label: labelOption }, { label: labelOption }] }); } }; var labelOption = { show: true, position: app.config.position, distance: app.config.distance, align: app.config.align, verticalAlign: app.config.verticalAlign, rotate: app.config.rotate, formatter: '{c} {name|{a}}', fontSize: 16, rich: { name: { } } }; option = { title: { text: '验证情况统计' }, tooltip: {}, legend: { }, xAxis: { data: ['数据一','数据二', '数据三','', '数据四', '数据五','', '数据六', '数据七', '数据八','数据九','', '数据十','数据十一','数据十二','数据十三','数据十四'], axisTick: {show: false}, axisLabel:{rotate: -25,interval: 0} }, yAxis: {}, series: [{ type: 'bar', label: { show: true, position: 'outside' }, itemStyle: { normal: { color: function(params) { var colorList = [ "Blue", "Blue", "Blue", "", "LightSkyBlue", "LightSkyBlue", "", "Gold", "Gold", "Gold", "Gold", "", "LightGrey", "LightGrey", "LightGrey", "LightGrey", "LightGrey" ]; return colorList[params.dataIndex]; } } }, data: ['0','0','0','', '0', '0', '', '0','0','0','0','', '0','0','0','0','0'] }] }; option && myChart.setOption(option); </script> @section Scripts { <script src="~/js/signalr/dist/browser/signalr.js"></script> <script src="~/js/echartchat.js"></script> }
在Index后台代码中,我们响应一个POST请求,请求中带上SignalR分配的唯一编号,后台模拟数据统计,推送给前台,这里用Task.Factory来创建一个任务执行这个操作。
private readonly IHubContext<ChatHub, IChatClient> _hubContext; public IndexModel(IHubContext<ChatHub, IChatClient> hubContext) { _hubContext = hubContext; }
public async Task<JsonResult> OnPostAsync(string user) { if (string.IsNullOrWhiteSpace(user)) { return new JsonResult(new { status = "fail", message = "NoUser" }); } await Task.Factory.StartNew(async () => { var rnd = new Random(DateTime.Now.Millisecond); for (var i = 0; i < 10; i++) { await _hubContext.Clients.Client(user) .EchartsMessage( new[] { $"{rnd.Next(100,300)}", $"{rnd.Next(100,320)}" , $"{rnd.Next(100,310)}", "", $"{rnd.Next(10,30)}", $"{rnd.Next(10,30)}", "", $"{rnd.Next(130,310)}", $"{rnd.Next(130,310)}", $"{rnd.Next(13,31)}", $"{rnd.Next(13,31)}", "", $"{rnd.Next(130,310)}", $"{rnd.Next(130,310)}", $"{rnd.Next(13,31)}", $"{rnd.Next(130,310)}", $"{rnd.Next(130,310)}"} ); await Task.Delay(2000); } }, TaskCreationOptions.LongRunning); return new JsonResult(new { status = "ok" }); }
随后我们访问以下这个页面,就可以看到目前这种效果
下面我们来编写前端js,用来和后端服务通过SignalR通信,在wwwroot/js下新建一个echartchat.js
"use strict"; var connection = new signalR.HubConnectionBuilder() .withUrl("/chatHub") .withAutomaticReconnect() .configureLogging(signalR.LogLevel.Debug) .build(); var user = ""; var chartDom = document.getElementById('main'); var myChart = window.echarts.init(chartDom); connection.on("GetMyId", function (data) { user = data.userId;//SignalR返回的数据字段开头是小写 console.log(user); }); connection.on("ReceiveMessage", function (data) { console.log(data.userId + data.context); }); connection.on("EchartsMessage", function (data) { console.log(data); var option = { series: [{ data: data }] }; myChart.setOption(option);//更新Echarts数据 }); connection.start().then(function () { console.log("服务器已连接"); }).catch(function (err) { return console.error(err.toString()); });
保存后我们再次访问页面,并点击按钮,就可以实现后台推送数据给前台echarts来展示图标的效果。