SignalR
大约 4 分钟
SignalR
概念
- ASP.NET SignalR 是一个面向 ASP.NET 开发人员的库,可简化向应用程序添加实时 Web 功能的过程。
- SignalR 优先使用 WebSocket 连接
- WebSocket基于TCP协议,支持二进制通信,双工通信,性能和并发能力更强
- 一般把 WebSocket 服务器端部署到 Web 服务器上,可以借助HTTP协议完成初始的握手(可选),并且共享HTTP服务器的端口(主要)
- Hub(集线器),数据交换中心
协议协商
SignalR 默认按 Websocket、Server-Sent Events、长轮询的顺序进行协商
集群中协议协商的问题处理方法:粘性会话和禁用协商
“粘性会话”(Sticky Session):把来自同一个客户端的请求都转发给同一台服务器上。
- 缺点:请求无法被平均的分配到服务器集群;扩容的自适应性不强。
“禁用协商”:直接向服务器发出WebSocket请求,在这个 WebSocket 连接中的后续通信都是由同一台服务器来处理
- 缺点:无法降级到“服务器发送事件”或“长轮询”
禁用协议协商的方式:
const options = { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }; connection = new signalR.HubConnectionBuilder() .withUrl('https://localhost:7047/Hubs/ChatRoomHub', options) .withAutomaticReconnect().build();
分布式
客户端被连接到不同的两个服务器上,无法完成服务器间相互通向
解决方案:所有服务器连接到同一个消息中间件
依赖包:
Install-Package Microsoft.AspNetCore.SignalR.StackExchangeRedis
配置服务:
builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0.1", options => { options.Configuration.ChannelPrefix = "Test_"; });
向部分客户端发消息
- Hub 的 Groups 属性为
IGroupManager
属性,可以对组成员进行管理 - Hub 的 Clients 属性为
IHubCallerClients
类型,可以对连接到当前集线器的客户端进行筛选
使用步骤
服务器端创建继承自 Hub 类的集线器
program.cs
中注册SignalR
服务并添加Hub
路由中间件services.AddSignalR(); // 在 app.MapControllers() 之前添加中间件 app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
program.cs
中添加 JWT 身份验证// 添加身份验证 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { /* JWT 相关配置略 */ // 接收消息时进行身份验证的配置 // 在需要登录才能访问的集线器类上或者方法上添加 [Authorize] opt.Events = new JwtBearerEvents { OnMessageReceived = ctx => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/Hubs/ChatRoomHub"))) context.Token = accessToken; return Task.CompletedTask; } }; });
program.cs
中配置跨域并启用中间件string[] urls = new[] { "http://localhost:3000"}; builder.Services.AddCors(options => options.AddDefaultPolicy(builder => builder.WithOrigins(urls) .AllowAnyMethod().AllowAnyHeader().AllowCredentials())); app.UseCors();
编写前端项目安装 SignalR 的 JavaScript 客户端 SDK:
npm install @microsoft/signalr
前端使用 axios 发送请求时需要配置跨域
// 在 vue.config.js 中进行配置 configureWebpack:{ devServer:{ proxy:{ "/api":{ target:"https://localhost:7053/", // 服务器地址 changeOrigin:true, ws:true, pathRewrite:{"^/api":""} } } } }
创建前端项目注意事项
- 使用 vue-cli 创建项目:
vue create [options] <app-name>
- 创建项目是取消 eslintrc ,否则可能报很多错误
- 如果使用 vite 创建项目,前端跨域配置总是失败,具体原因未知
- 使用 vue-cli 创建项目:
示例:聊天室
服务器端:集线器类
public class ChatRoomHub : Hub { private readonly UserManager<User> _userManager; public ChatRoomHub(UserManager<User> userManager) { _userManager = userManager; } [Authorize] public Task SendPublicMessage(string message) { string name = Context.User!.FindFirst(ClaimTypes.Name)!.Value; string msg = $"{name} {DateTime.Now}: {message}"; return Clients.All.SendAsync("ReceivePublicMessage", msg); } [Authorize] public async Task<string> SendPrivateMessage(string destUserName, string message) { User? destUser = await _userManager.FindByNameAsync(destUserName); if (destUser == null) return $"{destUserName} not found."; string name = Context.User!.FindFirst(ClaimTypes.Name)!.Value; await Clients.Users(destUser.Id.ToString()).SendAsync("ReceivePrivateMessage", name, DateTime.Now, message); return "ok"; } }
前端
<template> <div> <fieldset> <div>用户名:<input type="text" v-model="state.loginData.username" /></div> <div>密码:<input type="password" v-model="state.loginData.password" /></div> <div><input type="button" value="登录" v-on:click="loginClick" /></div> </fieldset> <div>公屏消息: <input type="text" v-model="state.userMessage" v-on:keypress="msgOnkeypress" /> </div> <div> 向<input type="text" v-model="state.privateMsg.destUserName">发送私聊消息: <input type="text" v-model="state.privateMsg.message" v-on:keypress="privateMsgOnkeypress" /> </div> <div> <ul> <li v-for="(msg, index) in state.messages" :key="index">{{ msg }}</li> </ul> </div> </div> </template> <script> import { reactive } from 'vue'; import * as signalR from '@microsoft/signalr'; import axios from 'axios'; let connection; export default { setup() { // 绑定的模型数据 const state = reactive({ userMessage: "", messages: [], privateMsg: { message: "", destUserName: "" }, loginData: { username: "", password: "" }, accessToken: "" }); // 发送公屏消息 const msgOnkeypress = async function (e) { if (e.keyCode != 13) return; await connection.invoke("SendPublicMessage", state.userMessage); state.userMessage = ""; }; // 处理私聊点击事件 const privateMsgOnkeypress = async function (e) { if (e.keyCode != 13) return; await connection.invoke("SendPrivateMessage", state.privateMsg.destUserName, state.privateMsg.message); state.userMessage = ""; }; // 处理登录事件 const loginClick = async function () { // post 请求传递的参数需要与服务器端一致,如果服务器接收 QuerString,那么需要将数据拼接到 url 上 const response = await axios.post('https://localhost:7053/api/Users/Login', state.loginData); state.accessToken = response.data; // 设置 token 用于服务端验证身份 startConn(); }; // 连接聊天室 const startConn = async function () { // 创建连接 const options = { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }; // 跳过协商 options.accessTokenFactory = () => state.accessToken; // 将 token 添加到请求参数 const url = 'https://localhost:7053/Hubs/ChatRoomHub'; connection = new signalR.HubConnectionBuilder() .withUrl(url, options) .withAutomaticReconnect() .build(); // 监听服务器发送的消息 connection.on('ReceivePublicMessage', msg => { // 监听服务端发送的消息 console.log('receive message:', msg); // 查看接收的数据 state.messages.push(msg); // 追加到消息集合用于显示 }); // 监听服务器发送的私聊消息 connection.on('ReceivePrivateMessage', (srcUser, time, msg) => { state.messages.push(srcUser + "在" + time + "发来私信:" + msg); }); // 新连接提示 connection.on('UserAdded', userName => { state.messages.push("系统消息:欢迎" + userName + "加入聊天室."); }); // 启动连接 await connection.start() .then(() => console.log('成功登录聊天室')) .catch(err => console.log('连接失败:' + JSON.stringify(err))); }; // 暴露数据接口 return { state, msgOnkeypress, privateMsgOnkeypress, loginClick }; }, } </script>