背景
由于最近公司要做微信小程序聊天, 所以. NetFramwork 版本的 SignalR 版本的不能用了. 因为小程序里没有 windows 对象, 导致 JQuery 无法使用. 而 Signalr 的 js 客户端是依赖 JQuery 的.
所以看下了 Core 版本的 SignarlR, 经过测试, 发现可以在微信中运行, 不过要将 JS 客户端中的 webscoekt 改为微信自家的. 如有需要改后的版本, 可以楼下评论.
目的
本文的主要目的是为了介绍下使用. NetCore 版本 SignalR 的一些坑, 并提供了解决方式. 主要是以前的大部分文章只是简单的官方 demo 介绍. 没有真正投入使用, 其中一些细小问题没有进行深入挖掘并进行处理.
跨域问题
.Net Frmawork 版本很简单, 引用相应的包, 只要加上 AddCores() 就行了, 而 Core 版本的则控制的更加精确. 如下 ConfigureServices 添加如下代码
- services.AddCors(options => options.AddPolicy("SignalR",
- builder =>
- {
- builder.AllowAnyMethod() // 允许任意请求方式
- .AllowAnyHeader() // 允许任意 header
- .AllowAnyOrigin() // 允许任意 origin
- .AllowCredentials();// 允许验证
- //.WithOrigins(domins) // 指定特定域名才能访问
- }));
然后在 Configure 使用定义好的跨域策略
app.UseCors("SignalR");
使用 Redis Scale Out
和. Net Framwork 一样,.NetCore 版本 SignalR 可以使用 Redis 在多台服务器间通信. 但是如果 redis 没有连接成功, 程序不会报错, 但是通讯不能正常使用. 而. Net Framwork 版本的话, SignalR 的地址直接 404.
所以我想在启动时候就监控 Redis 是否连接成功. 但 SignalR 的官方文档只有简单使用, 连 Redis 怎么进行配置都没有. 所以只能去最大的交友网站去找. 一条条翻看 issue, 终于发现怎么监控了.
戳我看明细 https://github.com/aspnet/SignalR/issues/2468#issuecomment-414431147
要用以下代码进行配置, 就可以监控 Redis 是否连接成功了.
- services.AddSignalR()
- .AddMessagePackProtocol()
- .AddRedis(o =>
- {
- o.ConnectionFactory = async writer =>
- {
- var config = new ConfigurationOptions
- {
- AbortOnConnectFail = false
- };
- config.EndPoints.Add(IPAddress.Loopback, 0);
- config.SetDefaultPorts();
- var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
- connection.ConnectionFailed += (_, e) =>
- {
- Console.WriteLine("Connection Redis failed.");
- };
- if (!connection.IsConnected)
- {
- Console.WriteLine("Connection did not connect.");
- }
- return connection;
- };
- });
但是发现用这种方式, Redis 连接了 2 次, 按道理不应该额. 加上我事情多, 没空研究源代码. 所以就在这条 issue 里直接问作者. 到现在还没找到原因. 详情可以看上面的链接.
WebSocket 负载均衡配置
使用负载均衡对请求转发的话, 需要对 WebSocket 请求需要特殊配置.
找运维同学配置了下, 配置完后告诉我这个链接以后只能进行 GET 请求, 不能进行 POST 请求了. 手动黑人问号...
这样的话只能用 WebSocket 方式了, 像 LongPollin 及 SSE 协议都不能用了.
我去, 这么坑吗? 于是让运维把配置代码发我, 如下
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_connect_timeout 300;
- proxy_read_timeout 300;
- proxy_send_timeout 300;
于是我把应用发布到本地虚拟机里, 并用 docker 方式运行. 然后把配置写进 nginx 配置文件里.
发现真的不能进行 POST 请求了, 返回 400.400 的意是思请求异常. 肯定是这个配置有问题额. 于是又去交友网站找 issue, 果然又让我找到了. 在一个 issue 里面, 提供的配置如下
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $http_connection;
不同点在于 proxy_set_header Connection, 没有写死, 于是我把配置改了下, 果然好了.
原来 proxy_set_header Connection 不能写死, 要从请求头里面获取. 这样其他请求方式也就没啥问题了.
ConnectionId 获取
在 JS 客户端代码里, 没有再提供 ConnectionId 的获取. 也就是如果要用的话, 需要自己改源码加上. 改是没问题, 但是微软那群大神不应该犯这么低级的错误. ConnectionId 明明在 negotiate 请求时候返回了, 为什么不开放呢? 难道是 bug? 不应该有这么低级的 bug 吧.
于是又去看 issues, 果然, 里面也有人问, 作者也有解释.
去交友网站看看 https://github.com/aspnet/SignalR/issues/2729
大体意思 ConnectonId 是服务端使用, 客户端不应该使用这种不可控的方式进行通信. 可以采用 Group 或者 User 这种可控方式通信, 并且也有例子给出.
这里插一句, 在使用. Net Framwork 版本时候, 我们网站是使用 ConnectionId 进行通信, 经常出现重连导致 ConnectionId 变掉, 进而通信失败.
所以我也调整了下设计思路, 改使用 Group 进行通信.
以上都搞定了, 辛苦了这么久, 按道理应该没问题了吧! 那么发布上线!
大坑来了
应用我本地测试一切正常, 测试机也没有问题, 于是就发到生产环境, 结果问题出现了.
因为本地和测试环境都是单台服务器, 测试没问题. 而到了生产环境, 服务器有多台. 不管我 JS 怎么设置, 总会在执行完 negotiate 请求后, 接下来的连接请求肯定 404, 并且返回 No Connection with that Id.
如下图
看到这个错误, 第一个反应, 我的想法是难道是 Redis 没连接成功, 所以只能单机跑? 所以我就在上面 Redis 代码加上各种监控, 发现连接成功了. 代码 Review 了 n 遍代码, 实在没有地方可以改了.
于是官方文档一个个过. 终于发现 Js 可以进行以下配置
- let connection = new signalR.HubConnectionBuilder()
- .withUrl("/myhub", {
- skipNegotiation: true,
- transport: signalR.HttpTransportType.WebSockets
- });
- .build();
上面代码意思是跳过 negotiate 握手操作, 直接使用 WebSocket 进行连接.
按照文档配置了, 我去, 还真的可以. 因为只发送了一条请求就建立了通信连接.
这下我就不淡定了, 难道只能部署一台服务器吗? 这下稳定性怎么保证? 这个还是用在微信小程序里的 (js 客户端进行了修改), 低版本不能用 Websocket, 难道低版本就不管了吗? 流量大了不加机器怎么抗的住? 难道要换方案自己撸一套通讯吗?
没办法, 只能上大招. 把源码 clone 下来, 花了点时间看了下, 找到如下代码
- private async Task<HttpConnectionContext> GetConnectionAsync(HttpContext context)
- {
- var connectionId = GetConnectionId(context);
- if (StringValues.IsNullOrEmpty(connectionId))
- {
- // There's no connection ID: bad request
- context.Response.StatusCode = StatusCodes.Status400BadRequest;
- context.Response.ContentType = "text/plain";
- await context.Response.WriteAsync("Connection ID required");
- return null;
- }
- if (!_manager.TryGetConnection(connectionId, out var connection))
- {
- // No connection with that ID: Not Found
- context.Response.StatusCode = StatusCodes.Status404NotFound;
- context.Response.ContentType = "text/plain";
- await context.Response.WriteAsync("No Connection with that ID");
- return null;
- }
- return connection;
- }
这段代码啥意思呢? 就是 connection 在本地没找到的话, 就返回 404!
我去, 难道是代码 bug?
额外补充一下
在. Net Framwork 版本里, 源码里面会对 ConnectionId 进行验证. 验证通过, 但本地找不到 connection 的话, 就会新建一个 connection, 从而实现多台服务器间的通讯. 所以我才有上面的疑问. 但这样有个弊端, 就是无法监控客户端何时断开.
所以我提了个 issue 问作者. 戳我看明细
得到的回复是
It's not a bug it's by design.ASP.NETCore SignalR requires sticky sessions when using scale out. This means you need to pin a connection to a particular serve
啥意思呢? 就是这不是 bug, 就是这么设计的. 使用 SignalR 时, 要进行会话保持, 请求要一直落到同一台服务器上. 这样更稳定, 并且还可以实时监控客户端的情况.
于是找运维同学在负载上配置了下会话保持, 再次测试, 终于可以了.
总结
在此次使用 SignalR 的过程中, 遇到了太多的坑. 花了几个小时整理并记录下来, 与各位进行分享. 希望能帮到那些准备或者有打算使用. Net http://xn--klqpvu8ccmp6zgyh2gy9zypa109aqfn47nyqrsfodndnz0g.net/ Core 的. Neter
来源: https://www.cnblogs.com/cgyqu/p/9563193.html