WebSocket+Java 私聊、群聊例項
前言
之前寫畢業設計的時候就想加上聊天系統,當時已經用ajax長輪詢實現了一個(還不懂什麼是輪詢機制的,猛戳這裡:https://www.cnblogs.com/hoojo/p/longPolling_comet_jquery_iframe_ajax.html),但由於種種原因沒有加到畢設裡面。後來回校答辯後研究了一下websocket,並參照網上資料寫了一個簡單的聊天,現在又重新整理並記錄下來。 以下介紹來自維基百科: WebSocket是一種在單個TCP連線上進行全雙工通訊的協議。WebSocket通訊協議於2011年被IETF定為標準RFC 6455,並由RFC7936補充規範。WebSocket API也被W3C定為標準。
效果
上下線有提示
如果這時候傳送訊息給離線的人,則會收到系統提示訊息
群聊
huanzi一發送群聊,laowang跟xiaofang都不是在當前群聊視窗,出現小圓點+1
huanzi一發送群聊,xiaofang在當前群聊視窗,直接追加訊息,老王不在對應的聊天視窗,出現小圓點+1
xiaofang回覆,huanzi直接追加訊息,laowang依舊小圓點+1
laowang點選群聊視窗,小圓點消失,追加群聊訊息
laowang參與群聊
xiaofang切出群聊視窗,laowang在群聊傳送訊息,xiaofang出現小圓點+1
切回來,小圓點消失,聊天資料正常接收追加
三方正常參與聊天
私聊
huanzis私聊xiaofang,xiaofang聊天視窗在群聊,小圓點+1,而laowang不受影響
xiaofang切到私聊視窗,小圓點消失,資料正常追加;huanzi剛好處於私聊視窗,資料直接追加
效果演示到此結束,下面貼出程式碼
程式碼編寫
首先先介紹一下專案背景:springboot + thymeleaf ,websocket用的是spring的
maven
<!-- springboot websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!--fast json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.1.41</version> </dependency>
socketChart.css樣式
body{ background-color: #efebdc; } #hz-main{ width: 700px; height: 500px; background-color: red; margin: 0 auto; } #hz-message{ width: 500px; height: 500px; float: left; background-color: #B5B5B5; } #hz-message-body{ width: 460px; height: 340px; background-color: #E0C4DA; padding: 10px 20px; overflow:auto; } #hz-message-input{ width: 500px; height: 99px; background-color: white; overflow:auto; } #hz-group{ width: 200px; height: 500px; background-color: rosybrown; float: right; } .hz-message-list{ min-height: 30px; margin: 10px 0; } .hz-message-list-text{ padding: 7px 13px; border-radius: 15px; width: auto; max-width: 85%; display: inline-block; } .hz-message-list-username{ margin: 0; } .hz-group-body{ overflow:auto; } .hz-group-list{ padding: 10px; } .left{ float: left; color: #595a5a; background-color: #ebebeb; } .right{ float: right; color: #f7f8f8; background-color: #919292; } .hz-badge{ width: 20px; height: 20px; background-color: #FF5722; border-radius: 50%; float: right; color: white; text-align: center; line-height: 20px; font-weight: bold; opacity: 0; }
socketChart.html頁面
<!DOCTYPE> <!--解決idea thymeleaf 表示式模板報紅波浪線--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>聊天頁面</title> <!-- jquery線上版本 --> <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script> <!--引入樣式--> <link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/> </head> <body> <div id="hz-main"> <div id="hz-message"> <!-- 頭部 --> 正在與<span id="toUserName"></span>聊天 <hr style="margin: 0px;"/> <!-- 主體 --> <div id="hz-message-body"> </div> <!-- 功能條 --> <div id=""> <button>表情</button> <button>圖片</button> <button id="videoBut">視訊</button> <button onclick="send()" style="float: right;">傳送</button> </div> <!-- 輸入框 --> <div contenteditable="true" id="hz-message-input"> </div> </div> <div id="hz-group"> 登入使用者:<span id="talks" th:text="${username}">請登入</span> <br/> 線上人數:<span id="onlineCount">0</span> <!-- 主體 --> <div id="hz-group-body"> </div> </div> </div> </body> <script type="text/javascript" th:inline="javascript"> //專案根路徑 var ctx = /*[[@{/}]]*/''; //登入名 var username = /*[[${username}]]*/''; </script> <script th:src="@{/js/socketChart.js}"></script> </html>
socketChart.js 邏輯程式碼
//訊息物件陣列 var msgObjArr = new Array(); var websocket = null; //判斷當前瀏覽器是否支援WebSocket, springboot是專案名 if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:10086/springboot/websocket/"+username); } else { console.error("不支援WebSocket"); } //連線發生錯誤的回撥方法 websocket.onerror = function (e) { console.error("WebSocket連線發生錯誤"); }; //連線成功建立的回撥方法 websocket.onopen = function () { //獲取所有線上使用者 $.ajax({ type: 'post', url: ctx + "/websocket/getOnlineList", contentType: 'application/json;charset=utf-8', dataType: 'json', data: {username:username}, success: function (data) { if (data.length) { //列表 for (var i = 0; i < data.length; i++) { var userName = data[i]; $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[線上]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>"); } //線上人數 $("#onlineCount").text(data.length); } }, error: function (xhr, status, error) { console.log("ajax錯誤!"); } }); } //接收到訊息的回撥方法 websocket.onmessage = function (event) { var messageJson = eval("(" + event.data + ")"); //普通訊息(私聊) if (messageJson.type == "1") { //來源使用者 var srcUser = messageJson.srcUser; //目標使用者 var tarUser = messageJson.tarUser; //訊息 var message = messageJson.message; //最加聊天資料 setMessageInnerHTML(srcUser.username,srcUser.username, message); } //普通訊息(群聊) if (messageJson.type == "2"){ //來源使用者 var srcUser = messageJson.srcUser; //目標使用者 var tarUser = messageJson.tarUser; //訊息 var message = messageJson.message; //最加聊天資料 setMessageInnerHTML(username,tarUser.username, message); } //對方不線上 if (messageJson.type == "0"){ //訊息 var message = messageJson.message; $("#hz-message-body").append( "<div class=\"hz-message-list\" style='text-align: center;'>" + "<div class=\"hz-message-list-text\">" + "<span>" + message + "</span>" + "</div>" + "</div>"); } //線上人數 if (messageJson.type == "onlineCount") { //取出username var onlineCount = messageJson.onlineCount; var userName = messageJson.username; var oldOnlineCount = $("#onlineCount").text(); //新舊線上人數對比 if (oldOnlineCount < onlineCount) { if($("#" + userName + "-status").length > 0){ $("#" + userName + "-status").text("[線上]"); }else{ $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[線上]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>"); } } else { //有人下線 $("#" + userName + "-status").text("[離線]"); } $("#onlineCount").text(onlineCount); } } //連線關閉的回撥方法 websocket.onclose = function () { //alert("WebSocket連線關閉"); } //將訊息顯示在對應聊天視窗 對於接收訊息來說這裡的toUserName就是來源使用者,對於傳送來說則相反 function setMessageInnerHTML(srcUserName,msgUserName, message) { //判斷 var childrens = $("#hz-group-body").children(".hz-group-list"); var isExist = false; for (var i = 0; i < childrens.length; i++) { var text = $(childrens[i]).find(".hz-group-list-username").text(); if (text == srcUserName) { isExist = true; break; } } if (!isExist) { //追加聊天物件 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: NowTime()}]//封裝資料 }); $("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[線上]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>"); } else { //取出物件 var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == srcUserName) { //儲存最新資料 obj.message.push({username: msgUserName, message: message, date: NowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天物件 msgObjArr.push({ toUserName: srcUserName, message: [{username: msgUserName, message: message, date: NowTime()}]//封裝資料 }); } } // 對於接收訊息來說這裡的toUserName就是來源使用者,對於傳送來說則相反 var username = $("#toUserName").text(); //剛好開啟的是對應的聊天頁面 if (srcUserName == username) { $("#hz-message-body").append( "<div class=\"hz-message-list\">" + "<p class='hz-message-list-username'>"+msgUserName+":</p>" + "<div class=\"hz-message-list-text left\">" + "<span>" + message + "</span>" + "</div>" + "<div style=\" clear: both; \"></div>" + "</div>"); } else { //小圓點++ var conut = $("#hz-badge-" + srcUserName).text(); $("#hz-badge-" + srcUserName).text(parseInt(conut) + 1); $("#hz-badge-" + srcUserName).css("opacity", "1"); } } //傳送訊息 function send() { //訊息 var message = $("#hz-message-input").html(); //目標使用者名稱 var tarUserName = $("#toUserName").text(); //登入使用者名稱 var srcUserName = $("#talks").text(); websocket.send(JSON.stringify({ "type": "1", "tarUser": {"username": tarUserName}, "srcUser": {"username": srcUserName}, "message": message })); $("#hz-message-body").append( "<div class=\"hz-message-list\">" + "<div class=\"hz-message-list-text right\">" + "<span>" + message + "</span>" + "</div>" + "</div>"); $("#hz-message-input").html(""); //取出物件 if (msgObjArr.length > 0) { var isExist = false; for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == tarUserName) { //儲存最新資料 obj.message.push({username: srcUserName, message: message, date: NowTime()}); isExist = true; break; } } if (!isExist) { //追加聊天物件 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: NowTime()}]//封裝資料[{username:huanzi,message:"你好,我是歡子!",date:2018-04-29 22:48:00}] }); } } else { //追加聊天物件 msgObjArr.push({ toUserName: tarUserName, message: [{username: srcUserName, message: message, date: NowTime()}]//封裝資料[{username:huanzi,message:"你好,我是歡子!",date:2018-04-29 22:48:00}] }); } } //監聽點選使用者 $("body").on("click", ".hz-group-list", function () { $(".hz-group-list").css("background-color", ""); $(this).css("background-color", "whitesmoke"); $("#toUserName").text($(this).find(".hz-group-list-username").text()); //清空舊資料,從物件中取出並追加 $("#hz-message-body").empty(); $("#hz-badge-" + $("#toUserName").text()).text("0"); $("#hz-badge-" + $("#toUserName").text()).css("opacity", "0"); if (msgObjArr.length > 0) { for (var i = 0; i < msgObjArr.length; i++) { var obj = msgObjArr[i]; if (obj.toUserName == $("#toUserName").text()) { //追加資料 var messageArr = obj.message; if (messageArr.length > 0) { for (var j = 0; j < messageArr.length; j++) { var msgObj = messageArr[j]; var leftOrRight = "right"; var message = msgObj.message; var msgUserName = msgObj.username; var toUserName = $("#toUserName").text(); //當聊天視窗與msgUserName的人相同,文字在左邊(對方/其他人),否則在右邊(自己) if (msgUserName == toUserName) { leftOrRight = "left"; } //但是如果點選的是自己,群聊的邏輯就不太一樣了 if (username == toUserName && msgUserName != toUserName) { leftOrRight = "left"; } if (username == toUserName && msgUserName == toUserName) { leftOrRight = "right"; } var magUserName = leftOrRight == "left" ? "<p class='hz-message-list-username'>"+msgUserName+":</p>" : ""; $("#hz-message-body").append( "<div class=\"hz-message-list\">" + magUserName+ "<div class=\"hz-message-list-text " + leftOrRight + "\">" + "<span>" + message + "</span>" + "</div>" + "<div style=\" clear: both; \"></div>" + "</div>"); } } break; } } } }); //獲取當前時間 function NowTime() { var time = new Date(); var year = time.getFullYear();//獲取年 var month = time.getMonth() + 1;//或者月 var day = time.getDate();//或者天 var hour = time.getHours();//獲取小時 var minu = time.getMinutes();//獲取分鐘 var second = time.getSeconds();//或者秒 var data = year + "-"; if (month < 10) { data += "0"; } data += month + "-"; if (day < 10) { data += "0" } data += day + " "; if (hour < 10) { data += "0" } data += hour + ":"; if (minu < 10) { data += "0" } data += minu + ":"; if (second < 10) { data += "0" } data += second; return data; }
java程式碼有三個類,MyEndpointConfigure,WebSocketConfig,WebSocketServer;
MyEndpointConfigure
/** * 解決注入其他類的問題,詳情參考這篇帖子:webSocket無法注入其他類:https://blog.csdn.net/tornadojava/article/details/78781474 */ public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware { private static volatile BeanFactory context; @Override public <T> T getEndpointInstance(Class<T> clazz){ return context.getBean(clazz); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { MyEndpointConfigure.context = applicationContext; } }
WebSocketConfig
/** * WebSocket配置 */ @Configuration public class WebSocketConfig{ /** * 用途:掃描並註冊所有攜帶@ServerEndpoint註解的例項。 @ServerEndpoint("/websocket") * PS:如果使用外部容器 則無需提供ServerEndpointExporter。 */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } /** * 支援注入其他類 */ @Bean public MyEndpointConfigure newMyEndpointConfigure (){ return new MyEndpointConfigure (); } }
WebSocketServer
/** * WebSocket服務 */ @RestController @RequestMapping("/websocket") @ServerEndpoint(value = "/websocket/{username}", configurator = MyEndpointConfigure.class) public class WebSocketServer { /** * 線上人數 */ private static int onlineCount = 0; /** * 線上使用者的Map集合,key:使用者名稱,value:Session物件 */ private static Map<String, Session> sessionMap = new HashMap<String, Session>(); /** * 注入其他類(換成自己想注入的物件) */ @Autowired private UserService userService; /** * 連線建立成功呼叫的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username) { //在webSocketMap新增上線使用者 sessionMap.put(username, session); //線上人數加加 WebSocketServer.onlineCount++; //通知除了自己之外的所有人 sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); } /** * 連線關閉呼叫的方法 */ @OnClose public void onClose(Session session) { //下線使用者名稱 String logoutUserName = ""; //從webSocketMap刪除下線使用者 for (Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); logoutUserName = entry.getKey(); break; } } //線上人數減減 WebSocketServer.onlineCount--; //通知除了自己之外的所有人 sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + logoutUserName + "'}"); } /** * 伺服器接收到客戶端訊息時呼叫的方法 */ @OnMessage public void onMessage(String message, Session session) { try { //JSON字串轉 HashMap HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class); //訊息型別 String type = (String) hashMap.get("type"); //來源使用者 Map srcUser = (Map) hashMap.get("srcUser"); //目標使用者 Map tarUser = (Map) hashMap.get("tarUser"); //如果點選的是自己,那就是群聊 if (srcUser.get("username").equals(tarUser.get("username"))) { //群聊 groupChat(session,hashMap); } else { //私聊 privateChat(session, tarUser, hashMap); } //後期要做訊息持久化 } catch (IOException e) { e.printStackTrace(); } } /** * 發生錯誤時呼叫 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 通知除了自己之外的所有人 */ private void sendOnlineCount(Session session, String message) { for (Entry<String, Session> entry : sessionMap.entrySet()) { try { if (entry.getValue() != session) { entry.getValue().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); } } } /** * 私聊 */ private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException { //獲取目標使用者的session Session tarUserSession = sessionMap.get(tarUser.get("username")); //如果不線上則傳送“對方不線上”回來源使用者 if (tarUserSession == null) { session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"對方不線上\"}"); } else { hashMap.put("type", "1"); tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } /** * 群聊 */ private void groupChat(Session session,HashMap hashMap) throws IOException { for (Entry<String, Session> entry : sessionMap.entrySet()) { //自己就不用再發送訊息了 if (entry.getValue() != session) { hashMap.put("type", "2"); entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap)); } } } /** * 登入 */ @RequestMapping("/login/{username}") public ModelAndView login(HttpServletRequest request, @PathVariable String username) { return new ModelAndView("socketChart.html", "username", username); } /** * 登出 */