背景 由於最近公司要做微信小程式聊天,所以.NetFramwork版本的SignalR版本的不能用了。因為小程式里沒有windows對象,導致JQuery無法使用。而Signalr的 js客戶端是依賴JQuery的。 所以看下了Core版本的SignarlR,經過測試,發現可以在微信中運行,不過要將 ...
背景
由於最近公司要做微信小程式聊天,所以.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,終於發現怎麼監控了。
要用以下代碼進行配置,就可以監控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請求特殊配置。
找運維同學配置了下,配置完後告訴我這個SingllR的通訊地址以後只能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,果然,裡面也有人問,作者也有解釋。
大體意思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.NET Core SignalR requires sticky sessions when using scale out. This means you need to pin a connection to a particular serve
啥意思呢?就是這不是bug,就是這麼設計的。使用SignalR
時,要進行會話保持,請求要一直落到同一臺伺服器上。這樣更穩定,並且還可以實時監控客戶端的情況。
於是找運維同學在負載上配置了下會話保持,再次測試,終於可以了。
總結
在此次使用SignalR
的過程中,遇到了太多的坑。花了幾個小時整理並記錄下來,與各位進行分享。希望能幫到那些準備或者有打算使用.Net Core的.Neter
作者:cgyqu
出處:https://www.cnblogs.com/cgyqu/p/9563193.html
本站使用「署名 4.0 國際」創作共用協議,轉載請在文章明顯位置註明作者及出處。