[TOC]html
ASP.NET Core SignalR 是一个开源的实时框架,它简化了向应用中添加实时 Web 功能的过程。
实时 Web 功能是服务器端可以即时的将数据推送到客户端,而无需让服务器等待客户端请求后才返回数据。git
SignalR 主要适用于:github
SignalR 支持下面几种底层传输技术:web
SignalR 封装了这些底层传输技术,会从服务器和客户端支持的功能中自动选择最佳传输方法,让咱们只关注业务问题而不是底层传输技术问题.redis
能够只使用WebSocket,具体参考WebSockets support in ASP.NET Core浏览器
使用 SignalR 会涉及到服务端和客户端.服务器
Hub
.作一个小例子演练一下:websocket
建立一个空白的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 受权策略等.
在 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方法中, 添加 AddStackExchangeRedis services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");