SignalR的协议协商
协议协商
1、SignalR支持多种服务器推送方式:Websocket、Server-Sent Events、长轮询。默认按顺序尝试。
2、F12查看协商过程。
3、websocket和HTTP是不同的协议,为什么能用同一个端口。
4、在【开发人员工具】的【网络】页签中看WebSocket通信过程。
协议协商的问题
1、集群中协议协商的问题:“协商”请求被服务器A处理,而接下来的WebSocket请求却被服务器B处理。
2、解决方法:粘性会话和禁用协商。
3、 “粘性会话”(Sticky Session):把来自同一个客户端的请求都转发给同一台服务器上。缺点:因为共享公网IP等造成请求无法被平均的分配到服务器集群;扩容的自适应性不强。
4、“禁用协商”:直接向服务器发出WebSocket请求。WebSocket连接一旦建立后,在客户端和服务器端直接就建立了持续的网络连接通道,在这个WebSocket连接中的后续往返WebSocket通信都是由同一台服务器来处理。缺点:无法降级到“服务器发送事件”或“长轮询”,不过不是大问题。
禁用协议协商的方式
const options = { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets };
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:3000/Hubs/ChatRoomHub', options)
.withAutomaticReconnect().build();
SignalR的分布式部署
1、四个客户端被连接到不同的两个服务器上,会发生什么呢?
2、解决方案:所有服务器连接到同一个消息中间件。
3、官方方案:Redis backplane。(NuGet:Microsoft.AspNetCore.SignalR.StackExchangeRedis)
builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0.1", options => {
options.Configuration.ChannelPrefix = "SignalRStudy_";//加上适合的前缀
});
如下图
c1与c2都连接了server1 如果此时server1发送所有通知,只会发送消息给c1与c2。并不会发送给c3与c4,同样的server2发送所有消息也一样。c1与c2收不到。是因为这两天服务器之间的ChatRoomHub没有通讯,为了解决这个问题,我们可以让多台服务器上的集线器连接到一个消息队列中,通过这个消息队列完成跨服务器的消息投递。
官方解决方案就是使用Redis来解决SignalR部署在分布式环境中的数据同步的方案Redis backplane,按照上面2、3步骤即可。
SignalR的身份认证
1、目前SignalR问题:谁都能连。讲JWT方案。
2、配置SigningKey、ExpireSeconds。创建配置类JWTOptions。
3、NuGet:Microsoft.AspNetCore.Authentication.JwtBearer
4、app.UseAuthorization()这行代码之前添加app.UseAuthentication()。
5、在控制器类Test1Controller中增加登录并且创建JWT令牌的操作方法Login。返回JWT。
6、在需要登录才能访问的集线器类上或者方法上添加[Authorize]。也支持角色等设置,可以设置到Hub或者方法上。
Program
//从配置文件中获取key
builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
var secKey = new SymmetricSecurityKey(keyBytes);
x.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = secKey
};
x.Events = new JwtBearerEvents
{
//websocket 不支持自定义报文头 只能自己获取之后然后把http的请求内容赋值给 access_token
OnMessageReceived = context =>
{
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;
}
};
});
builder.Services.AddDataProtection();
//builder.Services.AddIdentity 是MVC 用的 提供了登录以及用户管理的相关页面
builder.Services.AddIdentityCore<MyUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
var idBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
idBuilder.AddEntityFrameworkStores<MyDbContext>()
.AddDefaultTokenProviders()
.AddRoleManager<RoleManager<MyRole>>()
.AddUserManager<UserManager<MyUser>>();
appsettings.json
"JWT": {//配置key
"SigningKey": "fasdfad&9045dafz222#fadpio@0232",
"ExpireSeconds": "86400"
}
4、代码(添加用户的适合 username 存汉字的时候有问题)
private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
{
//过期时间
var expires = DateTime.Now.AddSeconds(options.ExpireSeconds);
var keyBytes = Encoding.UTF8.GetBytes(options.SigningKey);
var secKey = new SymmetricSecurityKey(keyBytes);
//证书
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
//生成token
var tokenDescriptor = new JwtSecurityToken(expires: expires,
signingCredentials: credentials, claims: claims);
var res = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
return res;
}
/// <summary>
/// 新增用户
/// </summary>
/// <param name="req">用户模型</param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> AddUser(AddNewUserRequest req)
{
//这里 添加用户
MyUser user = new MyUser();
user.UserName = req.UserName;
user.PasswordHash = req.Password;
user.CreationTime = DateTime.UtcNow;
var result = await _userManager.CreateAsync(user);
var res = CheckResult(result, req.UserName);
if (res.EndsWith("成功"))
{
//这是给公司全员发消息
await _hubContext.Clients.All.SendAsync("UserAdded", req.UserName);
}
return Ok(res);
}
/// <summary>
/// 登录接口
/// </summary>
/// <param name="req"></param>
/// <param name="jwtOptions"></param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(AddNewUserRequest req, [FromServices]IOptions<JWTOptions> jwtOptions)
{
var userName = req.UserName;
var password = req.Password;
var user = await _userManager.FindByNameAsync(userName);
if (user == null)
{
return NotFound("没有这个用户");
}
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, userName));
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
var jwtToken = BuildToken(claims, jwtOptions.Value);
return Ok(jwtToken);
}
接口测试结果
创建用户
生成Token
添加用户并给所有的人发送消息
前端
<template>
<input type="text" v-model="state.userMessage" v-on:keypress="txtMsgOnkeypress"/>
<div><ul>
<li v-for="(msg,index) in state.messages" :key="index">{{msg}}</li>
</ul></div>
</template>
<script>
import { reactive, onMounted } from 'vue';
import * as signalR from '@microsoft/signalr';
let connection;
export default {name: 'Login',
setup() {
const state = reactive({ userMessage: "", messages: [] });
const txtMsgOnkeypress = async function (e) {
if (e.keyCode != 13) return;
await connection.invoke("SendPublicMessage", state.userMessage);
state.userMessage = "";
};
onMounted(async function () {
connection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:7298/Hubs/ChatRoomHub')
.withAutomaticReconnect().build();
await connection.start();
connection.on('ReceivePublicMessage', msg => {
state.messages.push(msg);
});
});
return { state, txtMsgOnkeypress };
},
}
</script>
本文内容大部分都为杨中科老师《ASP.NET Core技术内幕与项目实战》一书中内容,此文只是做学习记录,如有侵权,联系立马删除。