ASP.NET Core 中的实时框架 SingalR
SignalR 是什么?
ASP.NET Core SignalR 是一个开源的实时框架,它简化了向应用中添加实时 Web 功能的过程。
实时 Web 功能是服务器端能够即时的将数据推送到客户端,而无需让服务器等待客户端请求后才返回数据。
SignalR 主要适用于:
- 从服务器获取数据并高频更新的应用。比如股票,GPS应用等。
- 仪表板和监视应用。比如状态实时更新等。
- 需要通知的应用。比如即时聊天工具,以及社交网络里面的通知等。
- 协作应用。比如团体会议软件。
SignalR 支持下面几种底层传输技术:
- Web Socket 是不同于HTTP的另一种TCP协议。它是全双工的通信协议,浏览器和服务器之间可以相互通信。它会保持长连接状态只到被主动关闭。它支持文本和二进制的消息传输,也支持流媒体。其实正常的HTTP请求也是使用TCP Socket. Web Socket标准使用了握手机制把用于HTTP的Socket升级为使用WS协议的 WebSocket socket.
- 服务器发送事件 (Server Sent Events) 服务器可以在任何时间把数据发送到浏览器,而浏览器则会监听进来的信息,并使用一个叫做EventSource的对象用来处理传过来的信息。这个连接一直保持开放,直到服务器主动关闭它。它是单向通信,只能发生文本信息,而且很多浏览器都有最大并发连接数的限制。
- 长轮询(Long Polling) 客户端会定期的向服务器发送HTTP请求,如果服务器没有新数据的话,那么服务器会继续保持连接,直到有新的数据产生, 服务器才把新的数据返回给客户端。如果请求发出后一段时间内没有响应, 那么请求就会超时。这时,客户端会再次发出请求。
SignalR 封装了这些底层传输技术,会从服务器和客户端支持的功能中自动选择最佳传输方法,让我们只关注业务问题而不是底层传输技术问题.
可以只使用WebSocket,具体参考WebSockets support in ASP.NET Core
在 ASP.NET Core 中使用 SignalR
使用 SignalR 会涉及到服务端和客户端.
- Hub 是SignalR服务端最关键的组件, 它作为通信中心, 接受从客户端发来的消息, 也能把消息发送给客户端. 它是服务器端的一个类, 自己创建的Hub类需要继承于基类
Hub. - 客户端 微软目前官方支持JavaScript, .NET 和 Java客户端. 具体参考ASP.NET Core SignalR 支持的平台.
做一个小例子演练一下:
创建一个空白的Web项目, 然后添加 Hub 类
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined");
} public override async Task OnDisconnectedAsync(Exception ex)
{
await Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId} left");
} public Task Send(string message)
{
return Clients.All.SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
} public Task SendAllExceptMe(string message)
{
return Clients.AllExcept(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
} public Task SendToGroup(string groupName, string message)
{
return Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId}@{groupName}: {message}");
} public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} joined {groupName}");
} public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); await Clients.Group(groupName).SendAsync("ReceiveMessage", $"{Context.ConnectionId} left {groupName}");
} public Task Echo(string message)
{
return Clients.Client(Context.ConnectionId).SendAsync("ReceiveMessage", $"{Context.ConnectionId}: {message}");
}
}
添加配置代码
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
} public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
}
}
添加客户端
在wwwroot目录下创建一个名为chat.html的Html静态文件,内容如下:<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1 id="head1"></h1>
<div>
<input type="button" id="connect" value="Connect" />
<input type="button" id="disconnect" value="Disconnect" />
</div> <h4>To Everybody</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="message-text" placeholder="Type a message" />
<input type="button" id="broadcast" class="btn" value="Broadcast" />
<input type="button" id="broadcast-exceptme" class="btn" value="Broadcast (All Except Me)" />
</div>
</form> <h4>To Me</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="me-message-text" placeholder="Type a message" />
<input type="button" id="sendtome" class="btn" value="Send to me" />
</div>
</form> <h4>Group</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="group-text" placeholder="Type a group name" />
<input type="button" id="join-group" class="btn" value="Join Group" />
<input type="button" id="leave-group" class="btn" value="Leave Group" />
</div>
</form> <h4>Private Message</h4>
<form class="form-inline">
<div class="input-prepend input-append">
<input type="text" id="group-message-text" placeholder="Type a message" />
<input type="text" id="group-name" placeholder="Type the group name" /> <input type="button" id="sendgroupmsg" class="btn" value="Send to group" />
</div>
</form> <ul id="message-list"></ul>
</body>
</html>
<script src="signalr.js"></script>
<script>
let connectButton = document.getElementById('connect');
let disconnectButton = document.getElementById('disconnect');
disconnectButton.disabled = true;
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build(); document.getElementById("connect").addEventListener("click", function (event) { connectButton.disabled = true;
disconnectButton.disabled = false; connection.on('ReceiveMessage', msg => {
addLine(msg);
}); connection.onClosed = e => {
if (e) {
addLine('Connection closed with error: ' + e, 'red');
}
else {
addLine('Disconnected', 'green');
}
} connection.start()
.then(() => {
addLine('Connected successfully', 'green');
})
.catch(err => {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("disconnect").addEventListener("click", function (event) { connectButton.disabled = false;
disconnectButton.disabled = true; connection.stop(); event.preventDefault();
}); document.getElementById("broadcast").addEventListener("click", function (event) { var message = document.getElementById('message-text').value;
connection.invoke("Send", message).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("broadcast-exceptme").addEventListener("click", function (event) { var message = document.getElementById('message-text').value;
connection.invoke("SendAllExceptMe", message).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("sendtome").addEventListener("click", function (event) { var message = document.getElementById('me-message-text').value;
connection.invoke("Echo", message).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("join-group").addEventListener("click", function (event) { var groupName = document.getElementById('group-text').value;
connection.invoke("JoinGroup", groupName).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("leave-group").addEventListener("click", function (event) { var groupName = document.getElementById('group-text').value;
connection.invoke("LeaveGroup", groupName).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); document.getElementById("sendgroupmsg").addEventListener("click", function (event) {
var groupName = document.getElementById('group-name').value;
var message = document.getElementById('group-message-text').value;
connection.invoke("SendToGroup", groupName, message).catch(function (err) {
addLine(err, 'red');
}); event.preventDefault();
}); function addLine(line, color) {
var child = document.createElement('li');
if (color) {
child.style.color = color;
}
child.innerText = line;
document.getElementById('message-list').appendChild(child);
}
</script>
编译并运行 http://localhost:port/chat.html 测试.
权限验证
SignalR 可以采用 ASP.NET Core 配置好的认证和授权体系, 比如 Cookie 认证, Bearer token 认证, Authorize授权特性和 Policy 授权策略等.
- Cookie 认证基本上不需要额外配置, 但仅限于浏览器客户端.
- Bearer token 认证适用于所有客户端. 可以参考上篇文章 ASP.NET Core WebAPI中使用JWT Bearer认证和授权 进行Token的分发和验证. 在 SignalR 中使用的时候需要注意两点:
在 WebAPI 中, bearer token 是通过 HTTP header 传输的, 但当 SignalR 使用 WebSockets 和 Server-Sent Events 传输协议的时候, 由于不支持 header, Token是通过 query string 传输的, 类似于
ws://localhost:56202/chatHub?id=2fyJlq1T5vBOwAsITQaW8Q&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, 所以需要在服务端增加额外的配置如下:services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(configureOptions =>
{
// Configure JWT Bearer Auth to expect our security key // We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
configureOptions.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"]; if (!string.IsNullOrEmpty(accessToken) && (context.HttpContext.Request.Path.StartsWithSegments("/chatHub")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
同时, 给 Hub 添加 Authorize 特性.
[Authorize]
public class ChatHub: Hub
{
}
JS 客户端使用 accessTokenFactory 创建带 Token 的连接.
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub", { accessTokenFactory: () => this.loginToken })
.build();
如果服务端认证通过, 可以使用 Context.User 获取用户信息, 它是一个 ClaimsPrinciple 对象.
横向扩展
Hub 服务器可以支持的 TCP 并发连接数是有限的. 同时由于 SignalR 连接是持久的, 甚至当客户端进入空闲状态时,SignalR 连接依然保持着打开状态。所以当连接数比较多时, 通过增加服务器来实现横向扩展是很有必要的.
但相比于 WebAPI的单向通信(只存在客户端请求,服务端响应的情景), SignalR 中可能使用双向通信协议(客户端可以请求服务端的数据, 服务端也可以向客户端推送数据), 此时服务端水平扩展的时候, 一台服务器是不知道其他服务器上连接了哪些客户端. 当在一台服务器想要将消息发送到所有客户端时,消息只是发送到连接到该服务器的客户端. 为了能够把消息发送给所有服务器都连接的客户端, 微软提供了下面两种方案:
Azure SignalR 服务 是一个代理。当客户端启动连接到服务器时,会重定向连接到 Azure SignalR 服务。

Redis 底板 当服务器想要将消息发送到所有客户端时,它将先发送到 Redis 底板, 然后使用 Redis 的发布订阅功能转发给其他所有服务器从而发送给所有客户端.

添加 NuGet 包, ASP.NET Core 2.2 及更高版本中使用
Microsoft.AspNetCore.SignalR.StackExchangeRedis, 之前版本使用Microsoft.AspNetCore.SignalR.Redis.
然后在Startup.ConfigureServices方法中, 添加 AddStackExchangeRedisservices.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
源代码
参考
- https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction
- https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/
- https://www.cnblogs.com/cgzl/p/9515516.html
- https://www.cnblogs.com/maxzhang1985/p/7118426.html
ASP.NET Core 中的实时框架 SingalR的更多相关文章
- ASP.NET Core开发-使用Nancy框架
Nancy简介 Nancy是一个轻量级的独立的框架,下面是官网的一些介绍: Nancy 是一个轻量级用于构建基于 HTTP 的 Web 服务,基于 .NET 和 Mono 平台,框架的目标是保持尽可能 ...
- ASP.NET Core 中的框架级依赖注入
https://tech.io/playgrounds/5040/framework-level-dependency-injection-with-asp-net-core 作者: Gunnar P ...
- 在Asp.Net Core中集成Kafka
在我们的业务中,我们通常需要在自己的业务子系统之间相互发送消息,一端去发送消息另一端去消费当前消息,这就涉及到使用消息队列MQ的一些内容,消息队列成熟的框架有多种,这里你可以读这篇文章来了解这些MQ的 ...
- [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置
[翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...
- ASP.Net Core 中使用Zookeeper搭建分布式环境中的配置中心系列一:使用Zookeeper.Net组件演示基本的操作
前言:马上要过年了,祝大家新年快乐!在过年回家前分享一篇关于Zookeeper的文章,我们都知道现在微服务盛行,大数据.分布式系统中经常会使用到Zookeeper,它是微服务.分布式系统中必不可少的分 ...
- ASP.NET Core 中的SEO优化(4):自定义视图路径及主题切换
系列回顾 <ASP.NET Core 中的SEO优化(1):中间件实现服务端静态化缓存> <ASP.NET Core 中的SEO优化(2):中间件中渲染Razor视图> < ...
- C# 嵌入dll 动软代码生成器基础使用 系统缓存全解析 .NET开发中的事务处理大比拼 C#之数据类型学习 【基于EF Core的Code First模式的DotNetCore快速开发框架】完成对DB First代码生成的支持 基于EF Core的Code First模式的DotNetCore快速开发框架 【懒人有道】在asp.net core中实现程序集注入
C# 嵌入dll 在很多时候我们在生成C#exe文件时,如果在工程里调用了dll文件时,那么如果不加以处理的话在生成的exe文件运行时需要连同这个dll一起转移,相比于一个单独干净的exe,这种形 ...
- Blazor——Asp.net core的新前端框架
原文:Blazor--Asp.net core的新前端框架 Blazor是微软在Asp.net core 3.0中推出的一个前端MVVM模型,它可以利用Razor页面引擎和C#作为脚本语言来构建WEB ...
- 如何在 ASP.Net Core 中使用 Serilog
记录日志的一个作用就是方便对应用程序进行跟踪和排错调查,在实际应用上都是引入 日志框架,但如果你的 日志文件 包含非结构化的数据,那么查询起来将是一个噩梦,所以需要在记录日志的时候采用结构化方式. 将 ...
随机推荐
- WinRT 中后台任务类的声明
要实现后台任务,需要实现IBackgroundTask接口 public sealed class SimpleTask : IBackgroundTask { public void Run(IBa ...
- Email feedback to product team about TFS and SharePoint Integration 2017.2.15
SharePoint与Team Foundation Server的集成,一直是许多研发团队所关注的问题. 通过这种集成,开发团队可以实现下面的几个功能: 1. 搭建一个与团队项目集成的门户网站,并 ...
- 记一次golang的实践
之前做过一个深交所股票数据的接存储软件,消息的协议是这样. 协议文档在这 https://wenku.baidu.com/view/d102cd0b4a73f242336c1eb91a37f111f ...
- 《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造
第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造 MVC默认模板的视觉设计从MVC1到MVC3都没有改变,比较陈旧了:在MVC4中做了升级,好看些,在不同的分辨率下,也能工作得 ...
- 4月第4周业务风控关注 | 网络犯罪经济每年1.5万亿美元 GDP居全球第12位
本文由 网易云发布. 易盾业务风控周报每周呈报值得关注的安全技术和事件,包括但不限于内容安全.移动安全.业务安全和网络安全,帮助企业提高警惕,规避这些似小实大.影响业务健康发展的安全风险. 1.网络 ...
- forname,newInstance的作用及使用
Forname可以获得类名对应的class对象: String classname=“java.util.Date” Class cl=Class.forName(className); newIns ...
- series dataframe 的 idxmax()
返回最大值的索引
- docker registry 私有仓库 安装配置、查询、删除
#++++++++++++++++++++++++++++++ #docker-registry 私有仓库 #搜索,下载register镜像 docker search registry docker ...
- CodeChef TWOROADS(计算几何+拉格朗日乘数法)
题面 传送门 简要题意:给出\(n\)个点,请求出两条直线,并最小化每个点到离它最近的那条直线的距离的平方和,\(n\leq 100\) orz Shinbokuow 前置芝士 给出\(n\)个点,请 ...
- Code Chef DARTSEGM(计算几何+凸包)
题面 传送门 题解 好眼熟丫-- 一月月赛最后一题--,代码都不用改-- //minamoto #include<bits/stdc++.h> #define R register #de ...