突然有個需求,需要使用普通的websocket客戶端去連接SignalR服務器。
因為使用的是.net core 版的signalr,目前對於使用非signalr客戶端連接的中文文檔幾乎為0,在gayhub折騰幾天總算折騰出來了。
首先,在startup.cs的ConfigureServices方法中添加signalr配置
services.AddSignalR(options => { // Faster pings for testing options.KeepAliveInterval = TimeSpan.FromSeconds(5);//心跳包間隔時間,單位 秒,可以稍微調大一點兒 }).AddJsonProtocol(options => { //options.PayloadSerializerSettings.Converters.Add(JsonConver); //the next settings are important in order to serialize and deserialize date times as is and not convert time zones options.PayloadSerializerSettings.Converters.Add(new IsoDateTimeConverter()); options.PayloadSerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Unspecified; options.PayloadSerializerSettings.DateParseHandling = DateParseHandling.DateTimeOffset; });
在使用微信小程序的websocket的時候,可以在websocket請求頭中加入了一個字段
app.UseSignalR(routes => { routes.MapHub<AbpCommonHub>("/signalr", options => options.WebSockets.SubProtocolSelector = requestedProtocols =>
{
return requestedProtocols.Count > 0 ? requestedProtocols[0] : null;
}); routes.MapHub<Chat.SignalR.Chat>("/chat", options => options.WebSockets.SubProtocolSelector = requestedProtocols =>
{
return requestedProtocols.Count > 0 ? requestedProtocols[0] : null;
});
然后是Chat.cs. 因為我用的是Abp.AspNetCore.SignalR,所以在寫法上會略微和aspnetcore.signalr有區別
using Abp.Dependency;
using Abp.Runtime.Session;
using Castle.Core.Logging;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
public class Chat : Hub, ITransientDependency { public IAbpSession AbpSession { get; set; } public ILogger Logger { get; set; } public Chat() { AbpSession = NullAbpSession.Instance; Logger = NullLogger.Instance; } public async Task SendMessage(string message) { await Clients.All.SendAsync("getMessage", string.Format("User {0}: {1}", AbpSession.UserId, message)); } public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId); } public override async Task OnDisconnectedAsync(Exception exception) { await base.OnDisconnectedAsync(exception); Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId); } public void log(string arg) { Logger.Info("client send:" + arg); }
這樣在瀏覽器輸入http://myhost/chat (myhost換成自己的域名 或者是ip:端口)
出現 Connection ID required 就說明signalr啟動成功了。
對於websocket客戶端來說,服務器連接地址就是把http改為ws就可以了。如http://192.168.1.100:21012/chat,則對應的連接地址就是ws://192.168.1.100:21012/chat
然后是客戶端代碼。這里我使用的是ClientWebSocket
1 ClientWebSocket client = new ClientWebSocket(); 2 client.Options.AddSubProtocol("protocol1"); 3 wait client.ConnectAsync(new Uri(BaseUrl), CancellationToken.None); 4 Console.WriteLine("Connect success"); 5 6 await client.SendAsync(new ArraySegment<byte>(AddSeparator(Encoding.UTF8.GetBytes(@"{""protocol"":""json"", ""version"":1}"))) 7 , WebSocketMessageType.Text, true, CancellationToken.None);//發送握手包 8 Console.WriteLine("Send success"); 9 var bytes = Encoding.UTF8.GetBytes(@"{ 10 ""type"": 1, 11 ""invocationId"":""123"", 12 ""target"": ""log"", 13 ""arguments"": [ 14 ""Test Message"" 15 ] 16 }"")");//發送遠程調用 log方法 17 await client.SendAsync(new ArraySegment<byte>(AddSeparator(bytes)), WebSocketMessageType.Text, true, CancellationToken.None); 18 var buffer = new ArraySegment<byte>(new byte[1024]); 19 while (true) 20 { 21 await client.ReceiveAsync(buffer, CancellationToken.None); 22 Console.WriteLine(Encoding.UTF8.GetString(RemoveSeparator(buffer.ToArray()))); 23 }
添加和刪除分隔符方法
private static byte[] AddSeparator(byte[] data) { List<byte> t = new List<byte>(data) { 0x1e };//0x1e record separator return t.ToArray(); } private static byte[] RemoveSeparator(byte[] data) { List<byte> t = new List<byte>(data); t.Remove(0x1e); return t.ToArray(); }
然后就能在服務器日志中查看到log方法調用后的結果
然后是微信小程序的連接代碼
在api.js中定義要連接的url,這里有個小技巧,http的自動替換為ws,https自動替換為wss,這樣本地調試和服務器運行都只用修改serverRoot這個url就可以了。
var _serverRoot = 'https://myhost.com/'; var websocket = (_serverRoot.startsWith("https") ? _serverRoot.replace('https', 'wss') : _serverRoot.replace('http', 'ws')) + 'signalr';
然后定義調用的連接方法。token是在登錄的時候調用abp登錄方法返回並setStorage,然后就可以在任意頁面獲取到token。
這樣連接的時候傳入token,在signalr的遠程調用的時候,abpSession中就能獲取到用戶信息,同時也避免了無關客戶端連接遠程調用。
還需要稍微修改一下ABP的驗證部分代碼,Host項目中的AuthConfigurer.cs的QueryStringTokenResolver方法,從HttpContext.Request的QueryString中獲取accesstoken。
private static Task QueryStringTokenResolver(MessageReceivedContext context) { if (!context.HttpContext.Request.Path.HasValue || !context.HttpContext.Request.Path.Value.StartsWith("/signalr")) { //We are just looking for signalr clients return Task.CompletedTask; } var qsAuthToken = context.HttpContext.Request.Query["accesstoken"].FirstOrDefault(); if (qsAuthToken == null) { //Cookie value does not matches to querystring value return Task.CompletedTask; } //Set auth token from cookie context.Token = qsAuthToken;//SimpleStringCipher.Instance.Decrypt(qsAuthToken, AppConsts.DefaultPassPhrase); return Task.CompletedTask; }
function WsConnect(){ var token = wx.getStorageSync('token'); var url = { url: api.websocket + '?accesstoken=' + token, header: { 'Abp.TenantId': 2, 'Content-Type': 'application/json' } }; wx.connectSocket(url); } function wsSend(msg){ console.log('send:'+msg); msg += String.fromCharCode(0x1e); wx.sendSocketMessage({ data: msg, }); }
發送的時候需要在后面添加一個分隔符0x1e。所以稍微封裝了一下。
然后就是接收處理和斷開重連處理。需要注意一下,signalr連接成功后要發送握手包指定協議。{"protocol":"json","version":1}就表示是使用的json
wx.onSocketClose(function () { console.log("鏈接關閉 "); setTimeout(() => util.WsConnect(), 5000);//5s后自動重連 }) wx.onSocketError(function (res) { console.log('WebSocket連接打開失敗,請檢查!'); console.log(res); setTimeout(() => util.WsConnect(), 5000);//5s后自動重連 }); wx.onSocketOpen(res => {//websocket打開連接成功回調 util.wsSend('{"protocol":"json","version":1}');//發送握手包 wx.onSocketMessage(function (res) {//接收消息回調 var data = res.data.replace(String.fromCharCode(0x1e), "");//返回時去掉分隔符 console.log("recv:"+data); } }
貼一下能運行查看的小程序代碼片段 wechatide://minicode/3YTuJZmP7BYZ
粘貼這個到微信web開發者工具--導入代碼片段中
修改連接地址
然后在控制台接收到{“type”:6} 服務器發送的心跳包,就說明signalr連接成功了
后續將會講解一下signalr core的Hub協議和遠程調用方式。未完待續