針對Spring Websocket的實現,我參照了其他博主的文章https://www.cnblogs.com/leechenxiang/p/5306372.html
下面直接給出實現:
一、引入相關依賴
在之前的文章中,我們說到要使用websocket,我們首先要在maven中引入相關的依賴包,具體如下:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2.1-b03</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>5.1.3.RELEASE</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.8</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency>
這里沒有逐個添加注釋,因為之前有寫。
二、分析聊天功能的實現方式
在拓展了AbstractWebSocketHandler的websocket處理器類中,我們實現了處理連接建立后行為的方法、處理服務器接收來自客戶端信息的行為方法以及處理關閉連接行為的方法,
客戶端建立連接后發送信息給服務端,服務端通過TextMessage或WebSocketMessage類獲取信息,在通過調用與指定客戶端開啟的Session將信息發送給指定客戶端。因此我們需要
一個標識使得我們的服務端能夠在接收的客戶端發送的信息后,正確地將收到的信息發送給指定的客戶端,從而完成聊天業務。
三、實現
在對每一個Session進行獨立標識處理時,我使用了所參照博主的方法,通過設置攔截器來處理每一個建立連接后開啟的Session,代碼如下:
package com.example.websocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import javax.servlet.http.HttpSession; import java.util.Map; public class WsInterceptor implements HandshakeInterceptor { private static final Logger log = LoggerFactory.getLogger(WsInterceptor.class); @Override public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception { if (serverHttpRequest instanceof ServletServerHttpRequest) { // 取用戶連接姓名作為參數傳入 ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest; String userName = request.getServletRequest().getParameter("name"); log.info("攔截請求 獲取用戶信息: " + userName); // 拿到對應的Session HttpSession session = request.getServletRequest().getSession(true); if (session != null) { // 唯一表示Session map.put("username", userName); log.info("獲取Session: " + session.getId()); } } return true; } @Override public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { } }
在配置文件中加入對攔截器的配置:
package com.example.config; import com.example.websocket.MyHandler; import com.example.websocket.WsInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry.addHandler(myHandler(), "/myHandler") .addInterceptors(wsInterceptor()).withSockJS(); } @Bean public MyHandler myHandler() { return new MyHandler(); } @Bean public WsInterceptor wsInterceptor() { return new WsInterceptor(); } }
由於本人瀏覽器不支持websocket,所以我使用SockJS代替websocket
現在我們就可以開始編寫我們的信息收發業務了,代碼如下:
package com.example.websocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.AbstractWebSocketHandler; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class MyHandler extends AbstractWebSocketHandler { private static final Logger log = LoggerFactory.getLogger(MyHandler.class); private static final List<WebSocketSession> sessions = new ArrayList<>(); private static int count = 0; @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { super.afterConnectionClosed(session, status); log.debug("session" + session.getId() + "已關閉"); String userName = (String) session.getAttributes().get("username"); sendMessageToOthers(new TextMessage(userName + "已經下線了"), userName); count = count - 1; sessions.remove(session); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); if (session != null) { log.debug("連接成功成功 Session " + session.getId() + "已打開"); count = count + 1; sessions.add(session); String userName = (String)session.getAttributes().get("username"); if (userName != null) { log.debug("用戶" + userName + "已連接"); String message = "SystemInfo: " + userName + "上線了, 當前在線人數: " + count; session.sendMessage(new TextMessage(message)); } } } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); String userName = (String) session.getAttributes().get("username"); log.debug(userName + "發送信息: " + message.getPayload()); sendMessageToOthers(new TextMessage(message.getPayload()), userName); } public void sendMessageToOthers(TextMessage message, String userName) throws IOException { for (WebSocketSession session: sessions) { String theUser = (String) session.getAttributes().get("username"); if ((!userName.equals(theUser)) && session.isOpen()) { session.sendMessage(message); } } } }
websocket服務端的部分完成了,現在我們來構建視圖層,使用之前的Controller類:
package com.example.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/") public class wsController { @RequestMapping(method = RequestMethod.GET) public ModelAndView websocket() { return new ModelAndView("websocket"); } }
我們來手寫一個jsp文件作為我們的視圖層顯示:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%-- Created by IntelliJ IDEA. User: asus1 Date: 2019/1/26 Time: 13:01 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>websocket在線聊天</title> <c:set value="${pageContext.request.contextPath}" var="path" scope="page"/> <link type="text/css" rel="stylesheet" href="/statics/css/communication.css"> </head> <body> <div class="communicationInfo"> </div> <div class="message"> <label> <textarea class="messageArea"></textarea> </label> <br/> <div class="btnArea"> username: <label><input id="username" type="text"/></label> <input type="button" class="btn" id="connect" value="連接"/> <input type="button" class="btn" id="send" value="發送"/> </div> </div> <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script type="text/javascript" src="/statics/js/sockjs-1.0.0.min.js"></script> <script> $(function () { var sock; // 創建新連接 function getConnect() { var username = $("#username").val(); var url = "http://localhost:8080/myHandler?name=" + username; sock = new SockJS(url); } // 綁定連接按鈕事件 $("#connect").bind("click", function () { getConnect(); sock.onopen = function () { alert("連接成功"); }; sock.onerror = function () { }; sock.onmessage = function (ev) { var element = document.createElement("div"); element.style.textAlign = "left"; element.style.paddingLeft = "20px"; // element.style.border = "green solid 3px"; element.style.overflow = "hidden"; element.style.paddingTop = "5px"; console.log(ev.data); var receivedFromOthers = ""; var infoHead = ev.data.substr(0, 11); if (infoHead === "SystemInfo:") { element.style.textAlign = "center"; receivedFromOthers = ev.data; } else { receivedFromOthers = '<div class="imageL">' + '<img src="/statics/images/QQ圖片20171104161032.jpg" alt=""/>' + '</div>' + '<span class="triangle-borderL">' + '</span>' + '<div class="message-divL">' + ev.data + '</div>'; } element.innerHTML += receivedFromOthers; $(".communicationInfo").append(element); }; sock.onclose = function () { alert("與服務器斷開連接"); }; }); // 綁定發送按鈕事件 $("#send").bind("click", function () { if (sock != null) { var msg = $(".messageArea").val(); console.log(msg); sock.send(msg); var element = document.createElement("div"); element.style.textAlign = "right"; element.style.paddingRight = "20px"; // element.style.border = "gold solid 3px"; element.style.overflow = "hidden"; element.style.paddingTop = "5px"; var sendToOthers = '<div class="imageR">' + '<img src="/statics/images/photo.jpg" alt=""/>' + '</div>' + '<div class="triangle-borderR">' + '</div>' + '<span class="message-divR">' + msg + '</span>'; element.innerHTML += sendToOthers; $(".communicationInfo").append(element); } else { alert("未與服務器連接") } clearContent(); }); function clearContent() { $(".messageArea").val(""); } }); </script> </body> </html>
jsp對應的css代碼如下:
/*
聊天樣式
*/
body {
text-align: center;
}
.communicationInfo {
border: black solid 3px;
width: 40%;
height: 250px;
margin-top: 10%;
margin-left: auto;
margin-right: auto;
padding-top: 30px;
padding-bottom: 30px;
overflow-y: auto;
}
.message {
border: black solid 3px;
width: 40%;
height: 100px;
margin: 0 auto;
padding-top: 10px;
padding-bottom: 10px;
}
.messageArea {
width: 80%;
height: 70px;
overflow-y: auto;
resize: none;
}
.btnArea {
width: 60%;
margin-left: 40%;
}
.imageL {
width: 30px;
height: 30px;
border: solid black 5px;
float: left;
}
img {
width: 30px;
height: 30px;
}
.triangle-borderL {
display: block;
width: 0;
height: 0;
margin-left: 20px;
margin-top: 10px;
margin-right: -1px;
border-style: solid;
border-color: transparent lawngreen transparent;
border-width: 10px 10px 10px 0;
float: left;
}
.message-divL {
text-align: center;
background-color: lawngreen;
padding: 15px 20px;
max-width: 180px;
margin-top: -5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
float: left;
}
.imageR {
width: 30px;
height: 30px;
border: solid black 5px;
float: right;
}
.message-divR {
background-color: lawngreen;
text-align: center;
padding: 15px 20px;
max-width: 180px;
margin-top: -5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
float: right;
}
.triangle-borderR {
display: block;
width: 0;
height: 0;
margin-left: -1px;
margin-right: 20px;
margin-top: 10px;
border-style: solid;
border-color: transparent lawngreen transparent;
border-width: 10px 0 10px 10px;
float: right;
}
需要說明的是,由於視圖層只為測試后端代碼使用,因此有些樣式可能對不同的瀏覽器存在不兼容問題,本人使用Chrone和360瀏覽器
視圖中引用的靜態資源存放在/statics/images目錄下
OK,現在讓我們啟動項目看看運行后結果:
再通過360瀏覽器創建一個用戶:
發送消息: