什么是 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來展示圖標的效果。

