SpringBoot+Vue整合WebSocket实现前后端消息推送
1. 了解
-
HTTP和WebSocket区别联系:
-
HTTP协议:
HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。
这种通信模型有一个弊端:HTTP协议无法实现服务器主动向客户端发起消息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 JavaScript 和 XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
-
WebSocket:
WebSocket 就是这样发明的。
WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。
WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
-
2. 后端SpringBoot集成
-
pom
文件加入依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
-
然后新建一个
WebSocket
的配置类,用来开启WebSocket的支持@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
-
然后新建一个客户端实体类【
WebSocketClient
】用来存储连接的Session和Uri@Data public class WebSocketClient { //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //连接的uri private String uri; }
-
然后新建
WebSocketService
,用来创建和处理连接import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; @Data @ServerEndpoint("/websocket/{username}") @Component @Slf4j public class WebSocketService { //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; private static ConcurrentHashMap<String, WebSocketClient> webSocketMap = new ConcurrentHashMap<>(); /** * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 接收userName */ private String userName = ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("userName") String userName) { if (!webSocketMap.containsKey(userName)) { // 在线数 +1 addOnlineCount(); } this.session = session; this.userName = userName; WebSocketClient client = new WebSocketClient(); client.setSession(session); client.setUri(session.getRequestURI().toString()); webSocketMap.put(userName, client); log.info("----------------------------------------------------------------------------"); log.info("用户连接:" + userName + ",当前在线人数为:" + getOnlineCount()); try { sendMessage("来自后台的反馈:连接成功"); } catch (IOException e) { log.error("用户:" + userName + ",网络异常!!!!!!"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { if (webSocketMap.containsKey(userName)) { webSocketMap.remove(userName); subOnlineCount(); } log.info("----------------------------------------------------------------------------"); log.info(userName + "用户退出,当前在线人数为:" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到用户消息:" + userName + ",报文:" + message); //可以群发消息 //消息保存到数据库、redis if (StringUtils.isNotBlank(message)) { //………… } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("用户错误:" + this.userName + ",原因:" + error.getMessage()); error.printStackTrace(); } /** * 连接服务器成功后主动推送 */ public void sendMessage(String message) throws IOException { synchronized (session) { this.session.getBasicRemote().sendText(message); } } /** * 向指定客户端发送消息 * * @param userName * @param message */ public static void sendMessage(String userName, String message) { try { WebSocketClient webSocketClient = webSocketMap.get(userName); if (webSocketClient != null) { webSocketClient.getSession().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } } /** * 群发自定义消息 */ public static void sendInfo(String message, @PathParam("userName") String userName) { for (String un : webSocketMap.keySet()) { //这里可以设定只推送给这个sid的,为null则全部推送 if (userName == null) { sendMessage(un, message); } else if (un.equals(userName)) { sendMessage(un, message); } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketService.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketService.onlineCount--; } public static void setOnlineCount(int onlineCount) { WebSocketService.onlineCount = onlineCount; } public static ConcurrentHashMap<String, WebSocketClient> getWebSocketMap() { return webSocketMap; } public static void setWebSocketMap(ConcurrentHashMap<String, WebSocketClient> webSocketMap) { WebSocketService.webSocketMap = webSocketMap; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } }
这里的WebSocketService服务可以有多个,用来代表一类场景下的的Socket服务。
比如 H5WebSocketService(web端)、WxWebSocketService(微信端)
- 解释:
- 这里引用的WebSocketClient是上面自己新建的用来存储连接相关信息的实体类。
- @ServerEndPoint("/websocket/{userName}") -> 指websocket连接的地址,后面的{username}是用来接受前端传递到参数->用作不同标识,进而做不同的处理。
- @OnOpen() -> 代表连接成功后调用的方法,这里做了统计在线用户(可以根据自己的业务需求要去处理)。
- @OnClose() -> 代表连接关闭后调用的方法。
- @OnMessage() -> 代表接收到客户端发来消息时的回调方法(里面根据自己需要去处理数据)
- sendMessage() 方法 -> 代表在客户端连接到服务器时使用当前会话来给客户端推送一个反馈消息
- sendMessage(String userName, String message) 方法 -> 向指定客户端userName发送message。
- 解释:
-
新建一个Controller接口用来测试消息的推送。
@RestController @RequestMapping("/websocket") public class WebSocketController { @GetMapping public void test(){ WebSocketService.sendMessage("h5","后端ljeabmreos"); } }
如果是普通的SpringBoot项目则后台到此就可以了,
如果是使用了安全框架(如:shiro、SpringSecurity)搭建的后台,需要将ws的url和接口的url放开权限认证。
3. Vue项目继承:
-
在components目录下新建一个WebSocket组件,
-
声明一些变量
data() { return { // ws是否启动 wsIsRun: false, // 定义ws对象 webSocket: null, // ws请求链接(类似于ws后台地址) ws: '', // ws定时器 wsTimer: null, } },
-
在mounted函数中执行初始化websocket连接的方法。
async mounted() { this.wsIsRun = true this.wsInit() },
wsInit():
进行websocket连接并传递h5作为后端接受的usernName参数,
并且设置回调处理方法。
最后设置了一个3秒的定时器去定时检查websocket的连接状态。/** * 初始化ws */ wsInit() { const wsuri = "ws://127.0.0.1:8000/websocket/h5"; this.ws = wsuri; if (!this.wsIsRun) return; // 销毁ws this.wsDestroy(); // 初始化ws this.webSocket = new WebSocket(this.ws); // ws连接建立时触发 this.webSocket.addEventListener("open", this.wsOpenHanler); // ws服务端给客户端推送消息 this.webSocket.addEventListener("message", this.wsMessageHanler); // ws通信发生错误时触发 this.webSocket.addEventListener("error", this.wsErrorHanler); // ws关闭时触发 this.webSocket.addEventListener("close", this.wsCloseHanler); // 检查ws连接状态,readyState值为0表示尚未连接,1表示建立连接,2正在关闭连接,3已经关闭或无法打开 clearInterval(this.wsTimer); this.wsTimer = setInterval(() => { if (this.webSocket.readyState === 1) { clearInterval(this.wsTimer); } else { console.log("ws建立连接失败"); this.wsInit(); } }, 3000); },
-
添加一个按钮并设置其点击事件用来给服务端推送消息
methods: { sendDataToServer() { if (this.webSocket.readyState === 1) { this.webSocket.send("来自前端的数据:生命不必每时每刻都要冲刺。低沉时,就当给自己放了一个悠长的假期"); } else { throw Error("服务未连接"); } },
-
完整的WebSocket组件代码:
<!-- websocket后期功能拓展使用预留。 --> <template> <el-button @click="sendDataToServer">测试:给后端发送消息。</el-button> </template> <script> export default { name: "WebSocket", data() { return { // ws是否启动 wsIsRun: false, // 定义ws对象 webSocket: null, // ws请求链接(类似于ws后台地址) ws: "", // ws定时器对象 wsTimer: null, }; }, async mounted() { this.wsIsRun = true; this.wsInit(); }, methods: { sendDataToServer() { if (this.webSocket.readyState === 1) { this.webSocket.send("来自前端的数据:生命不必每时每刻都要冲刺。低沉时,就当给自己放了一个悠长的假期"); } else { throw Error("服务未连接"); } }, /** * 初始化ws */ wsInit() { const wsuri = "ws://127.0.0.1:8000/websocket/h5"; this.ws = wsuri; if (!this.wsIsRun) return; // 销毁ws this.wsDestroy(); // 初始化ws this.webSocket = new WebSocket(this.ws); // ws连接建立时触发 this.webSocket.addEventListener("open", this.wsOpenHanler); // ws服务端给客户端推送消息 this.webSocket.addEventListener("message", this.wsMessageHanler); // ws通信发生错误时触发 this.webSocket.addEventListener("error", this.wsErrorHanler); // ws关闭时触发 this.webSocket.addEventListener("close", this.wsCloseHanler); // 检查ws连接状态,readyState值为0表示尚未连接,1表示建立连接,2正在关闭连接,3已经关闭或无法打开 clearInterval(this.wsTimer); this.wsTimer = setInterval(() => { if (this.webSocket.readyState === 1) { clearInterval(this.wsTimer); } else { console.log("ws建立连接失败"); this.wsInit(); } }, 3000); }, wsOpenHanler(event) { console.log("ws建立连接成功"); }, wsMessageHanler(e) { console.log("wsMessageHanler"); console.log(e); //const redata = JSON.parse(e.data) //console.log(redata) } /** * ws通信发生错误 */, wsErrorHanler(event) { console.log(event, "通信发生错误"); this.wsInit(); } /** * ws关闭 */, wsCloseHanler(event) { console.log(event, "ws关闭"); this.wsInit(); } /** * 销毁ws */, wsDestroy() { if (this.webSocket !== null) { this.webSocket.removeEventListener("open", this.wsOpenHanler); this.webSocket.removeEventListener("message", this.wsMessageHanler); this.webSocket.removeEventListener("error", this.wsErrorHanler); this.webSocket.removeEventListener("close", this.wsCloseHanler); this.webSocket.close(); this.webSocket = null; clearInterval(this.wsTimer); } }, }, }; </script> <style scoped> </style>
-
4. 测试:
后端:
前端: