【WebSocket】一个简单的前后端交互Demo
WebSocket资料参考:
https://www.jianshu.com/p/d79bf8174196
使用SpringBoot整合参考:
https://blog.csdn.net/KeepStruggling/article/details/105543449
一、通信实现
后端部分:
直接使用Springboot,依赖只有内嵌tomcat和对应的websocket封装启动包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <groupId>cn.cloud9</groupId> <artifactId>WebSocket</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- 内嵌Tomcat库来提供WebSocketAPI --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring对WebSocket的扩展Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies> </project>
定义WebSocket接口
package cn.cloud9.endpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author OnCloud9 * @description * @project WebSocket * @date 2022年06月15日 20:13 * * ws://localhost:8080/ws/test */ @Component @ServerEndpoint("/ws/test") public class TestEndpoint { private static final Logger LOGGER = LoggerFactory.getLogger(TestEndpoint.class); private static final Map<String, Session> CLIENT_SESSION_MAP = new ConcurrentHashMap<>(); /** * 侦测客户端向此服务建立连接,此终端实例会新创建出来 * @param session */ @OnOpen public void onOpen(Session session) { LOGGER.info("客户端 {} 开启了连接", session.getId()); // 默认按照ID保管存放这个客户端的会话信息 CLIENT_SESSION_MAP.put(session.getId(), session); } /** * 侦测客户端异常 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { LOGGER.info("客户端 {} 连接异常... 异常信息:{}", session.getId(), error.getMessage()); LOGGER.error(error.getMessage()); } /** * 侦测客户端向此服务端发送消息 * @param session * @param message */ @OnMessage public void onMessage(Session session, String message) { // 可根据会话ID或者message中自定义唯一标识从容器中取出会话对象来进行操作 LOGGER.info("收到消息, 来自客户端 {}, 消息内容 -> {}", session.getId(), message); } /** * 侦测客户端关闭事件 * @param session */ @OnClose public void onClose(Session session) { LOGGER.info("客户端 {} 关闭了连接...", session.getId()); // 客户端关闭时,从保管容器中踢出会话 CLIENT_SESSION_MAP.remove(session.getId()); } /** * 给所有客户端发送消息 * @param message */ public static void sendMessageForAllClient(String message) { CLIENT_SESSION_MAP.values().forEach(session -> { try { LOGGER.info("给客户端 {} 发送消息 消息内容: {}", session.getId(), message); session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); LOGGER.info("给客户端 {} 发送消息失败, 异常信息:{}", session.getId(), e.getMessage()); } }); } }
配置WebSocket接口暴露器
package cn.cloud9.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * @author OnCloud9 * @description * @project WebSocket * @date 2022年06月15日 20:42 */ @Configuration public class WebSocketConfig { /** * 如果使用Springboot默认内置的tomcat容器,则必须注入ServerEndpoint的bean; * 如果使用外置的web容器,则不需要提供ServerEndpointExporter,下面的注入可以注解掉 */ @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
写一个Controller,通过一般Http接口向WebSocket客户端推送消息
package cn.cloud9.controller; import cn.cloud9.endpoint.TestEndpoint; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author OnCloud9 * @description * @project WebSocket * @date 2022年06月15日 20:46 */ @RestController @RequestMapping("/api/ws") public class WebSocketController { /** * http://localhost:8080/api/ws/send * @param message * @return */ @GetMapping("/send") public boolean send(@RequestParam String message) { TestEndpoint.sendMessageForAllClient(message); return true; } }
前端页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket 页面客户端</title> </head> <body> <div> <button onclick="initConnection()">开启WebSocket连接</button> </div> <div> <input type="text" id="message" ><button onclick="sendMessageFromClient()">发送消息</button> </div> <div> <button onclick="closeConnection()">关闭连接</button> </div> <script> const connectionUrl = 'ws://localhost:8080/ws/test' var webSocket = null function initConnection() { webSocket = new WebSocket(connectionUrl) webSocket.onopen = (event) => { console.log('建立新的WebSocket连接') } webSocket.onmessage = (event) => { const message = JSON.stringify(event.data) console.log(`收到服务端发送的消息,消息内容 -> ${message}`) } webSocket.onerror = (error) => { console.log(`WebSocket连接异常 -> ${JSON.stringify(error)}`) } webSocket.onclose = (event) => { console.log(`WebSocket连接关闭 -> ${JSON.stringify(event)}`) } } function sendMessageFromClient() { const message = document.querySelector('#message').value webSocket.send(JSON.stringify({ message: message })) } function closeConnection() { webSocket.close() } </script> </body> </html>
二、解决客户端标识区分问题:
WebSocket提供了一个@PathParam注解
在开启和关闭时通过该注解的参数传递URL路径值
通过这个在这个路径值放置唯一标识即可区分客户端连接
菜坑点:
1、路径值不能随意放置,必须是在最后面
2、可以放置JSON,但是接收的参数将会移除JSON对象的大括号符号,因为路径值的占位符原因...,解决办法就是追加大括号即可
完整代码:
添加了身份区分的终端案例:
package cn.cloud9.server.struct.websocket; import com.alibaba.fastjson.JSONObject; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static cn.cloud9.server.struct.websocket.WsMessageEndPoint.PATH; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月09日 下午 09:56 */ @Slf4j @Service @ServerEndpoint(PATH) public class WsMessageEndPoint { /** * localhost:8080/websocket/message * 路径参数用于客户端身份标识,区分每个客户端的连接 */ public static final String PATH = "/websocket/message/{token}"; /** * 客户端会话管理容器 */ private static final Map<String, Session> CLIENT_SESSION_MAP = new ConcurrentHashMap<>(); /** * 侦测客户端开启连接?,通过客户端身份标识 重复创建连接则覆盖原有的连接 * @param session */ @OnOpen public void onConnectionOpen(Session session, @PathParam("token") String clientToken) { log.info("客户端 {} 开启了连接", clientToken); final Map<String, String> map = JSONObject.parseObject(clientToken, Map.class); // 默认按照ID保管存放这个客户端的会话信息 CLIENT_SESSION_MAP.put(map.get("userId"), session); } /** * 侦测客户端关闭事件 * @param session */ @OnClose public void onClose(Session session, @PathParam("token") String clientToken) { final Map<String, String> map = JSONObject.parseObject(clientToken, Map.class); log.info("客户端 {} 关闭了连接...", clientToken); // 客户端关闭时,从保管容器中踢出会话 final String userId = map.get("userId"); CLIENT_SESSION_MAP.remove(userId); } /** * 侦测客户端异常 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("客户端 {} 连接异常... 异常信息:{}", session.getId(), error.getMessage()); } /** * 收到客户端消息 * @param session */ @OnMessage public void receiveClientMessage(String message, Session session) throws IOException { log.info("来自客户端 {} 的消息: {}", session.getId(), message); session.getBasicRemote().sendText("服务器已收到"); } /** * 给所有客户端发送消息 * @param message */ public static void sendMessageForAllClient(String message) { CLIENT_SESSION_MAP.values().forEach(session -> { try { log.info("给客户端 {} 发送消息 消息内容: {}", session.getId(), message); session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); log.info("给客户端 {} 发送消息失败, 异常信息:{}", session.getId(), e.getMessage()); } }); } @SneakyThrows public static void sendMessageForClient(String clientId, String message) { final Session session = CLIENT_SESSION_MAP.get(clientId); session.getBasicRemote().sendText(message); } }
服务端发送消息接口:
给PostMan调用,然后看对应的客户端是否更新消息
package cn.cloud9.server.test.controller; import cn.cloud9.server.struct.websocket.WsMessageEndPoint; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月09日 下午 10:03 */ @RestController @RequestMapping("/websocket/sender") public class WsMessageController { @PostMapping("/all") public void sendMessageToAllClient(@RequestBody Map<String, String> map) { final String text = map.get("text"); WsMessageEndPoint.sendMessageForAllClient(text); } @PostMapping("/one") public void sendMessageToClient(@RequestBody Map<String, String> map) { final String text = map.get("text"); final String client = map.get("client"); WsMessageEndPoint.sendMessageForClient(client, text); } }
浏览器客户端:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket 页面客户端</title> </head> <body> <div> <button onclick="initConnection()">开启WebSocket连接</button> </div> <div> <input type="text" id="message" ><button onclick="sendMessageFromClient()">发送消息</button> </div> <div> server message list <ul id="msgList"></ul> </div> <div> <button onclick="closeConnection()">关闭连接</button> </div> <script> let clientInfo = { userId: 1001, username: '张三' } clientInfo = JSON.stringify(clientInfo) let connectionUrl = `ws://localhost:8080/websocket/message/{${clientInfo}}` console.log(connectionUrl) var webSocket = null function initConnection() { webSocket = new WebSocket(connectionUrl) webSocket.onopen = (event) => { console.log('建立新的WebSocket连接') } webSocket.onmessage = (event) => { const message = JSON.stringify(event.data) console.log(`收到服务端发送的消息,消息内容 -> ${message}`) const msgList = document.querySelector('#msgList') msgList.innerHTML += `<li>${message}</li>` } webSocket.onerror = (error) => { console.log(`WebSocket连接异常 -> ${JSON.stringify(error)}`) } webSocket.onclose = (event) => { console.log(`WebSocket连接关闭 -> ${JSON.stringify(event)}`) } } function sendMessageFromClient() { const message = document.querySelector('#message').value webSocket.send(JSON.stringify({ message: message })) } function closeConnection() { webSocket.close() } </script> </body> </html>