SignalR的协议协商

协议协商

1、SignalR支持多种服务器推送方式:Websocket、Server-Sent Events、长轮询。默认按顺序尝试。
2、F12查看协商过程。
3、websocket和HTTP是不同的协议,为什么能用同一个端口。
4、在【开发人员工具】的【网络】页签中看WebSocket通信过程。

image-1660152165251

协议协商的问题

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步骤即可。

image-1660152202665

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);

        }
接口测试结果
创建用户

image-1660152232642

生成Token

image-1660152296147

添加用户并给所有的人发送消息

image-1660152274935

前端
<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技术内幕与项目实战》一书中内容,此文只是做学习记录,如有侵权,联系立马删除。