针对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;
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;
40%;
height: 100px;
margin: 0 auto;
padding-top: 10px;
padding-bottom: 10px;
}
.messageArea {
80%;
height: 70px;
overflow-y: auto;
resize: none;
}
.btnArea {
60%;
margin-left: 40%;
}
.imageL {
30px;
height: 30px;
border: solid black 5px;
float: left;
}
img {
30px;
height: 30px;
}
.triangle-borderL {
display: block;
0;
height: 0;
margin-left: 20px;
margin-top: 10px;
margin-right: -1px;
border-style: solid;
border-color: transparent lawngreen transparent;
border- 10px 10px 10px 0;
float: left;
}
.message-divL {
text-align: center;
background-color: lawngreen;
padding: 15px 20px;
max- 180px;
margin-top: -5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
float: left;
}
.imageR {
30px;
height: 30px;
border: solid black 5px;
float: right;
}
.message-divR {
background-color: lawngreen;
text-align: center;
padding: 15px 20px;
max- 180px;
margin-top: -5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
float: right;
}
.triangle-borderR {
display: block;
0;
height: 0;
margin-left: -1px;
margin-right: 20px;
margin-top: 10px;
border-style: solid;
border-color: transparent lawngreen transparent;
border- 10px 0 10px 10px;
float: right;
}
需要说明的是,由于视图层只为测试后端代码使用,因此有些样式可能对不同的浏览器存在不兼容问题,本人使用Chrone和360浏览器
视图中引用的静态资源存放在/statics/images目录下
OK,现在让我们启动项目看看运行后结果:
再通过360浏览器创建一个用户:
发送消息: