實(shí)際項(xiàng)目講解 Signalr(.net core3.x)使用
最近認(rèn)真研究了一個(gè)微軟最新的.net core3.x版本中的signalr技術(shù),并使用之開(kāi)發(fā)了一個(gè)相對(duì)穩(wěn)定的聊天系統(tǒng)。在此作一些記錄,希望能給需要的同志作一些有益的提醒。
一、系統(tǒng)功能界面

二、主要功能代碼
1.用戶登陸(Cookie方式)
? ?public IActionResult OnPost(string InputTime, string UserId, string Password)
? ? ? ? {
? ? ? ? ? ? if (!string.IsNullOrWhiteSpace(UserId) && !string.IsNullOrWhiteSpace(Password))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? string tKey = (DateTime.Parse(InputTime).Second - 2).ToString();
? ? ? ? ? ? ? ? ModelUser user = GlobalVars.AllChatUsers.Where(e => e.Id.Equals(UserId) && (e.Pwd + tKey).Equals(Password)).FirstOrDefault();
? ? ? ? ? ? ? ? if (user != null)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Claim[] Claims = new Claim[] { new Claim(ClaimTypes.Name, user.Id) };
? ? ? ? ? ? ? ? ? ? ClaimsIdentity ClaimsIdentity = new ClaimsIdentity(Claims, CookieAuthenticationDefaults.AuthenticationScheme);
? ? ? ? ? ? ? ? ? ? ClaimsPrincipal LoginUser = new ClaimsPrincipal(ClaimsIdentity);
? ? ? ? ? ? ? ? ? ? Task.Run(async () =>
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? //登錄用戶
? ? ? ? ? ? ? ? ? ? ? ? await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, LoginUser);
? ? ? ? ? ? ? ? ? ? ?}).Wait();
? ? ? ? ? ? ? ? ? ?return RedirectToPage("./WebChat", new { Login = "First" });
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ?//"用戶名或密碼錯(cuò)誤!";
? ? ? ? ? ? return Page();
? ? ? ? }
2.客戶端連接服務(wù)器代碼
function ConnectionStart() {
? ? if (g_ReconnectCount >= g_ReconnectMaxTimes) {
? ? ? ? AddMsgToChatList.AddMessage('<span style="color:red">已重連 ' + g_ReconnectCount.toString() + ' 次,仍無(wú)法連接。請(qǐng)檢查網(wǎng)絡(luò),然后刷新重試!</span>');
? ? ? ? return;
? ? }
? ? connection.start().then(function () {
? ? ? ? g_divInputMsg.setInputState(true);
? ? ? ? //connection.state === signalR.HubConnectionState.Connected
? ? ? ? //AddMsgToChatList.AddMessage('<span style="color:red">已連接到服務(wù)器</span>');
? ? }).catch(function (err) {
? ? ? ? //connection.state === signalR.HubConnectionState.Disconnecte
? ? ? ? //AddMsgToChatList.AddMessage('<span style="color:red">連接已斷開(kāi),正在重連....(' + g_ReconnectCount.toString() + ')</span>');
? ? ? ? g_ReconnectCount++;
? ? ? ? setTimeout(() => ConnectionStart(), 500);
? ? });
}
//首次連接服務(wù)器。
ConnectionStart();
3.?客戶端發(fā)信息到服務(wù)器(使用流傳輸方式,帶發(fā)送進(jìn)度顯示,以防止客戶端發(fā)大圖時(shí)會(huì)卡死或無(wú)法發(fā)送的現(xiàn)象)
客戶端主要代碼(Javascript):
async function sendMsgToSever(willSendMsg, processElement) {
? ? processElement.innerHTML = '已發(fā)送(0/' + willSendMsg.length + ')';
? ? //客戶端到服務(wù)器的流式處理
? ? var startIndex = 0;
? ? var stopIndex = 0;
? ? var fixLength = 1024 * 20; //此大小影響數(shù)據(jù)發(fā)送速度。4096=8407,5120=6714,10240=3348,20480=1882
? ? var msgSubStr = '';
? ? //var beginTime = (new Date()).valueOf();
? ? console.log(new Date().toDateString)
? ? const subject = new signalR.Subject();
? ? connection.send("UploadStream", subject);
? ? if (willSendMsg.length <= fixLength) { stopIndex = willSendMsg.length; } else { stopIndex = fixLength; }
? ? var sendFun = function () {
? ? ? ? if (stopIndex <= willSendMsg.length) {
? ? ? ? ? ? msgSubStr = willSendMsg.substring(startIndex, stopIndex);
? ? ? ? ? ? subject.next(msgSubStr);
? ? ? ? ? ? processElement.innerHTML = '已發(fā)送(' + stopIndex.toString() + '/' + willSendMsg.length + ')';? ?//顯示發(fā)送到服務(wù)器的進(jìn)度
? ? ? ? ? ? startIndex = stopIndex;
? ? ? ? ? ? if (willSendMsg.length === stopIndex) {
? ? ? ? ? ? ? ? subject.complete();
? ? ? ? ? ? ? ? //var endTime = (new Date()).valueOf();
? ? ? ? ? ? ? ? //console.log(fixLength.toString() + '=' + (endTime - beginTime).toString());
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? ? ? if (willSendMsg.length >= (stopIndex + fixLength)) { stopIndex += fixLength; } else { stopIndex += willSendMsg.length - stopIndex; }
? ? ? ? }
? ? ? ? window.setTimeout(sendFun, 0);
? ? }
? ? sendFun();
}
服務(wù)器端主要代碼(C#):
?public async Task UploadStream(IAsyncEnumerable<string> stream)
? ? ? ? {
? ? ? ? ? ? //接收客戶端發(fā)來(lái)的信息
? ? ? ? ? ? System.Text.StringBuilder ClientNewMsg = new System.Text.StringBuilder();
? ? ? ? ? ? await foreach (var item in stream)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? ClientNewMsg.Append(item);
? ? ? ? ? ? }
? ? ? ? ? ? string message = ClientNewMsg.ToString();
? ? ? ? ? ? //發(fā)給指定用戶
? ? ? ? ? ? await Clients.Client(RUser.ConnectionId).SendAsync("ReceiveMessage", message);
? ? ? ? }
4.服務(wù)器端推送信息到客戶端
客戶端主要代碼(Javascript):
connection.on("ReceiveMessage", async function (MsgObj) {
? ? webChatLock.Reset();//鎖屏重計(jì)時(shí)
? ? if (g_PlaySound) {
? ? ? ? var SoundFile = "/images/dingdong.wav";
? ? ? ? var SoundObj = new Audio(SoundFile);
? ? ? ? SoundObj.play();
? ? }
? ? var MsgJson = JSON.parse(MsgObj);
? ? //....進(jìn)一步處理收到的數(shù)據(jù)
? ? }
服務(wù)器端主要代碼(C#):
? ??await Clients.Client(RUser.ConnectionId).SendAsync("ReceiveMessage", message);
5.需要注意的幾個(gè)問(wèn)題。
js讀取圖片內(nèi)容生成DataBase64?格式數(shù)據(jù)時(shí),<input type="file"/> 控件不能僅使用聲明,而必須添加到頁(yè)面上,否則蘋(píng)果手機(jī) safari 瀏覽器可以打開(kāi)文件選擇框,但將無(wú)應(yīng)響應(yīng)控件的 change事件。
更新頁(yè)面數(shù)據(jù)顯示進(jìn)度時(shí),需使用?window.setTimeout(funname,0),在funname()中發(fā)送并更新頁(yè)度進(jìn)度值,否則將看不到數(shù)據(jù)變化。
在處理用戶的離線消息時(shí),應(yīng)使用線程安全的ConcurrentDictionary<string, ModeMsgServer> NeedSendMsgs 等類。
在收到或發(fā)送信息時(shí),讓聊天窗口自動(dòng)滾動(dòng)到最底部,使用?DivChatElement.scrollIntoView(false);代碼,需注意的是,信息中有圖片,則上述代碼應(yīng)在圖片的 load 事件中執(zhí)行。
采用客戶端收到服務(wù)器信息后立即回調(diào)signalr的Hub中指定方法的方式,以便使服務(wù)器得以確認(rèn)客戶端已收到信息,從而將已發(fā)送的信息刪除(否則將適時(shí)重新發(fā)送或在下次該用戶登陸時(shí)再發(fā)送,以確保用戶收到該信息)。
關(guān)于客戶端與服務(wù)器端“心跳”配置。(注意客戶端與服務(wù)器端的時(shí)間要相匹配)
服務(wù)器端(Startup.cs):? ?
services.AddSignalR(options =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? //客戶端發(fā)保持連接請(qǐng)求到服務(wù)端最長(zhǎng)間隔,默認(rèn)30秒
? ? ? ? ? ? ? ? options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
? ? ? ? ? ? ? ? //服務(wù)端發(fā)保持連接請(qǐng)求到客戶端間隔,默認(rèn)15秒
? ? ? ? ? ? ? ? options.KeepAliveInterval = TimeSpan.FromSeconds(15);
? ? ? ? ? ? ? ? options.EnableDetailedErrors = true;
? ? ? ? ? ? ? ? options.MaximumReceiveMessageSize = 1024 * 1024 * 1024;
? ? ? ? ? ? });
客戶端(js):
//設(shè)置連接對(duì)象的相關(guān)屬性
connection.serverTimeoutInMilliseconds = 30e3;? //等待服務(wù)器端發(fā)送過(guò)來(lái)的心跳包最長(zhǎng)等待時(shí)間30s(如該時(shí)間段時(shí)收到不服務(wù)器發(fā)送的“心跳”數(shù)據(jù),即認(rèn)為鏈接丟失)
connection.keepAliveIntervalInMilliseconds = 15e3; //客戶端向服務(wù)器發(fā)送心跳包頻率15s