介紹: 前面寫過一篇簡單的websocke實現服務端。這一篇就不在說什麼基礎的東西主要是來用實例說話,主要是講一下實現單聊和群組聊天和所有群發的思路設計。 直接不懂的可以看一下上一篇簡單版本再來看也行:實現服務端WebSocket傳送門 實現效果: 本示例主要實現了個什麼東西哪,我們都使用qq或者其 ...
介紹:
前面寫過一篇簡單的websocke實現服務端。這一篇就不在說什麼基礎的東西主要是來用實例說話,主要是講一下實現單聊和群組聊天和所有群發的思路設計。
直接不懂的可以看一下上一篇簡單版本再來看也行:實現服務端WebSocket傳送門
實現效果:
本示例主要實現了個什麼東西哪,我們都使用qq或者其他的聊天工具,所有下麵我說的大家也都懂。就不啰嗦廢話了。
首先說實現6個主要的功能:
- 單聊:可以指定人進行聊天。
- 群發:這個的意思就是當前伺服器內的所有人包含自己,這個就跟一個推送效果一樣。
- 開啟連接(客戶端):通知除自己以外的所有用戶
- 關閉連接(客戶端):通知除自己以外的所有用戶
- 群組A:實現一個群組名字為A
- 群組B:實現一個群組名字為B
好了基本就是這個大致功能。下麵看下最終效果吧:
以上是第一個圖先進入了A群組,後面兩個在B群組。然後A有進入了B群組,所有第一張圖可以收到所有聊天,但是後面兩張只能收到B群組的聊天。
開始擼代碼
因為是在上面說道的文章改造的,所有基本的三連擊(開啟服務,開啟監聽,接受事件)我就不介紹了。
思路分析
我們既然實現的是聊天,那麼跟誰聊天當然是其他人,所以我們應該有其他人,可是問題又來了我們登錄瞭如何確認記錄狀態哪,我登錄之後我可以跟伺服器通訊,怎麼找到其他人進行通訊哪?我就是想到的是使用字典Dictionary來進行存儲,為什麼用字典而不用list是因為,字典中是鍵值儲存,我們把鍵當作人,然後值存儲這個人的通訊連接,這樣我只要知道這個人就在裡面找到這個人,然後就取到這個人的連接就可以通訊了。
//建立登錄用戶記錄信息 public static Dictionary<string, Socket> ListUser = new Dictionary<string, Socket>();
註:寫完這個之後我們老大看了下我的代碼說你這個存在一個問題:線程安全,確實的Dictionary不是線程安全,當時寫的時候沒多想,他說完我就想起來了,以前用Paralle時候用到的線程安全類ConcurrentBag和ConcurrentDictionary,在這了當然可以改成:
//建立登錄用戶記錄信息 public static ConcurrentDictionary<string, Socket> ListUser = new ConcurrentDictionary<string, Socket>();
好了我們可以進行通訊了,可以找到指定的人進行通訊了,那當然所有人的通訊也可以解決了。所有我就直接說下開啟連接和關閉連接的通知。我在消息接受和消息發送的時候定義了自己的規則:
開啟連接:我在發送的時候最前面帶:login字元串告訴消息接受我現在是登錄,你告訴別人吧。
關閉連接:退出的時候沒有發送字元串所以為空
ws.send("login,我已經連接上了!!!");
ws.close();
alert("關閉了通訊")
然後我在消息處理增加了判斷處理:
if (string.IsNullOrEmpty(resultList[0])) { //退出 SignOut(myClientSocket.RemoteEndPoint.ToString()); ListUser.Remove(myClientSocket.RemoteEndPoint.ToString()); myClientSocket.Shutdown(SocketShutdown.Both); myClientSocket.Close(); Debug.WriteLine("當前退出用戶:" + myClientSocket.RemoteEndPoint.ToString()); } else if (resultList[0] == "login") { //登錄 Login(myClientSocket.RemoteEndPoint.ToString()); ListUser.Add(myClientSocket.RemoteEndPoint.ToString(), myClientSocket); Debug.WriteLine("當前登錄用戶:" + myClientSocket.RemoteEndPoint.ToString()); }
大致其他的思路也是這個樣子:單聊,群發,群組都是定義相應的規則來進行判斷然後進行單獨的業務。
全部判斷邏輯代碼
這裡是寫在了服務端的消息接受ReceiveMessage方法內,這個方法是一個統一的發送接受方法。想看原方法的請看上一篇:實現服務端WebSocket傳送門
我這裡只是寫了我要做的效果,當然可以自己隨便修改的。
var resultStr = AnalyzeClientData(result, receiveNumber); string[] resultList = resultStr.Split(','); //string sendMsg = $"你({myClientSocket.RemoteEndPoint.ToString()}):" + resultList[1] + "【服務端回覆】"; //myClientSocket.Send(SendMsg(sendMsg));//取消對自己提示發送給別人 if (string.IsNullOrEmpty(resultList[0])) { //退出 SignOut(myClientSocket.RemoteEndPoint.ToString()); ListUser.Remove(myClientSocket.RemoteEndPoint.ToString()); myClientSocket.Shutdown(SocketShutdown.Both); myClientSocket.Close(); Debug.WriteLine("當前退出用戶:" + myClientSocket.RemoteEndPoint.ToString()); } else if (resultList[0] == "login") { //登錄 Login(myClientSocket.RemoteEndPoint.ToString()); ListUser.Add(myClientSocket.RemoteEndPoint.ToString(), myClientSocket); Debug.WriteLine("當前登錄用戶:" + myClientSocket.RemoteEndPoint.ToString()); } else if (resultList[0] == "all") { //群發所有用戶 GroupChat(myClientSocket.RemoteEndPoint.ToString(), resultList[1]); } else if (resultList[0] == "groupA") { //群組發送 GroupChatA("groupA", myClientSocket.RemoteEndPoint.ToString(), resultList[1]); } else if (resultList[0] == "groupB") { //群組發送 GroupChatA("groupB", myClientSocket.RemoteEndPoint.ToString(), resultList[1]); } else { //單聊 SingleChat(myClientSocket.RemoteEndPoint.ToString(), resultList[0], resultList[1]); }View Code
邏輯判斷完成就進入相應的業務方法了,下麵我把每一個業務方法放上來。
開啟連接
#region 登錄提示別人 public void Login(string userId) { if (ListUser.Count() > 0) { foreach (var item in ListUser) { if (item.Key != userId) { Socket socket = item.Value; try { socket.Send(SendMsg($"用戶({userId})登錄了")); } catch (Exception e) { Debug.WriteLine("該用戶已掉線:" + item.Key); //用戶已掉線就刪除掉 ListUser.Remove(item.Key); } } } } } #endregionView Code
關閉連接
#region 退出提示別人 public void SignOut(string userId) { if (ListUser.Count() > 0) { foreach (var item in ListUser) { if (item.Key != userId) { Socket socket = item.Value; try { socket.Send(SendMsg($"用戶({userId})退出了")); } catch (Exception e) { Debug.WriteLine("該用戶已掉線:" + item.Key); //用戶已掉線就刪除掉 ListUser.Remove(item.Key); } } } } } #endregionView Code
單聊
#region 單聊 public void SingleChat(string userIdA, string userIdB, string msg) { Socket socket = ListUser[userIdB]; if (socket != null) { try { socket.Send(SendMsg($"用戶({userIdA}=>{userIdB}):{msg}")); } catch (Exception e) { Debug.WriteLine("該用戶已掉線:" + userIdB); //用戶已掉線就刪除掉 ListUser.Remove(userIdB); } } } #endregionView Code
群發所有人
#region 群發 public void GroupChat(string userId, string msg) { if (ListUser.Count() > 0) { foreach (var item in ListUser) { if (item.Key != userId) { Socket socket = item.Value; try { socket.Send(SendMsg($"用戶({userId}=>{item.Key}):{msg}")); } catch (Exception e) { Debug.WriteLine("該用戶已掉線:" + item.Key); //用戶已掉線就刪除掉 ListUser.Remove(item.Key); } } } } } #endregionView Code
群組實現
#region 實現群組 //群組記錄分類 List<GroupHelp> groupList = new List<GroupHelp>(); public void GroupChatA(string groupName, string userId, string msg) { if (string.IsNullOrEmpty(groupName)) { return; } //判斷自己是否在群組 GroupHelp isEisx = groupList.Where(b => b.userId == userId && b.Name == groupName).FirstOrDefault(); if (isEisx == null) { groupList.Add(new GroupHelp() { Name = groupName, userId = userId }); } //根據群組名稱判斷是否存在群組 var nowGroupList = groupList.Where(b => b.Name == groupName).ToList(); foreach (var itemG in nowGroupList) { Socket socket = ListUser[itemG.userId]; try { socket.Send(SendMsg($"用戶({userId}=>{itemG.userId}):{msg}")); } catch (Exception e) { Debug.WriteLine("該用戶已掉線:" + itemG.userId); //用戶已掉線就刪除掉 ListUser.Remove(itemG.userId); } } } #endregionView Code
數據處理方法
#region 打包請求連接數據 /// <summary> /// 打包請求連接數據 /// </summary> /// <param name="handShakeBytes"></param> /// <param name="length"></param> /// <returns></returns> private byte[] PackageHandShakeData(byte[] handShakeBytes, int length) { string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, length); string key = string.Empty; Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n"); Match m = reg.Match(handShakeText); if (m.Value != "") { key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim(); } byte[] secKeyBytes = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); string secKey = Convert.ToBase64String(secKeyBytes); var responseBuilder = new StringBuilder(); responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n"); responseBuilder.Append("Upgrade: websocket" + "\r\n"); responseBuilder.Append("Connection: Upgrade" + "\r\n"); responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n"); return Encoding.UTF8.GetBytes(responseBuilder.ToString()); } #endregion #region 處理接收的數據 /// <summary> /// 處理接收的數據 /// 參考 http://www.cnblogs.com/smark/archive/2012/11/26/2789812.html /// </summary> /// <param name="recBytes"></param> /// <param name="length"></param> /// <returns></returns> private string AnalyzeClientData(byte[] recBytes, int length) { int start = 0; // 如果有數據則至少包括3位 if (length < 2) return ""; // 判斷是否為結束針 bool IsEof = (recBytes[start] >> 7) > 0; // 暫不處理超過一幀的數據 if (!IsEof) return ""; start++; // 是否包含掩碼 bool hasMask = (recBytes[start] >> 7) > 0; // 不包含掩碼的暫不處理 if (!hasMask) return ""; // 獲取數據長度 UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F; start++; // 存儲4位掩碼值 byte[] Masking_key = new byte[4]; // 存儲數據 byte[] mDataPackage; if (mPackageLength == 126) { // 等於126 隨後的兩個位元組16位表示數據長度 mPackageLength = (UInt64)(recBytes[start] << 8 | recBytes[start + 1]); start += 2; } if (mPackageLength == 127) { // 等於127 隨後的八個位元組64位表示數據長度 mPackageLength = (UInt64)(recBytes[start] << (8 * 7) | recBytes[start] << (8 * 6) | recBytes[start] << (8 * 5) | recBytes[start] << (8 * 4) | recBytes[start] << (8 * 3) | recBytes[start] << (8 * 2) | recBytes[start] << 8 | recBytes[start + 1]); start += 8; } mDataPackage = new byte[mPackageLength]; for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = recBytes[i + (UInt64)start + 4]; } Buffer.BlockCopy(recBytes, start, Masking_key, 0, 4); for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % 4]); } return Encoding.UTF8.GetString(mDataPackage); } #endregion #region 發送數據 /// <summary> /// 把發送給客戶端消息打包處理(拼接上誰什麼時候發的什麼消息) /// </summary> /// <returns>The data.</returns> /// <param name="message">Message.</param> private byte[] SendMsg(string msg) { byte[] content = null; byte[] temp = Encoding.UTF8.GetBytes(msg); if (temp.Length < 126) { content = new byte[temp.Length + 2]; content[0] = 0x81; content[1] = (byte)temp.Length; Buffer.BlockCopy(temp, 0, content, 2, temp.Length); } else if (temp.Length < 0xFFFF) { content = new byte[temp.Length + 4]; content[0] = 0x81; content[1] = 126; content[2] = (byte)(temp.Length & 0xFF); content[3] = (byte)(temp.Length >> 8 & 0xFF); Buffer.BlockCopy(temp, 0, content, 4, temp.Length); } return content; } #endregionView Code
javascript代碼
function webSocketClose() { ws.close(); alert("關閉了通訊") } //單聊 function send() { var msg = document.getElementById("message").value; var data = ""+document.getElementById("userId").value +","+ msg if (msg == "" || msg == undefined) { alert("請填寫發送內容!") return; } ws.send(data); } //群發(所有用戶) function sendGroup() { var msg = document.getElementById("message").value; var data = "all," + msg if (msg == "" || msg == undefined) { alert("請填寫發送內容!") return; } ws.send(data); } //群組發送A function sendGroupA() { var msg = document.getElementById("message").value; var data = "groupA," + msg if (msg == "" || msg == undefined) { alert("請填寫發送內容!") return; } ws.send(data); } //群組發送A function sendGroupB() { var msg = document.getElementById("message").value; var data = "groupB," + msg if (msg == "" || msg == undefined) { alert("請填寫發送內容!") return; } ws.send(data); }View Code
寫在最後
這個就是我不是根據seesion來進行判斷用戶的,所有每當刷新了頁面也就相當於退出了當前用戶,還是需要重新開啟連接的,這就是一個基本思路實現。還有待完善和不足。還請見諒。代碼基本就差不多了。
源碼放在了gitHub:https://github.com/Yanbigfeng/WebSocketToSocket