【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>

浙公网安备 33010602011771号