一、什么是 WebSocket?
定义:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
作用:实现客户端(浏览器)和服务器之间的实时、双向通信。
优势:
连接保持,通信实时性强(不像 HTTP 需要每次请求都建立连接)。
节省带宽,减少延迟。
适合聊天室、实时推送、游戏、股票行情、物联网等场景。
二、WebSocket 工作原理简述
客户端发起 HTTP 请求(带有 Upgrade: websocket 头)请求升级协议。
服务器响应协议升级,建立 WebSocket 连接。
建立连接后,客户端和服务器可以随时互相发送消息,连接保持,直到关闭。
连接关闭后,双方不能再通信。
三、Java 原生 WebSocket 使用示例
Java 标准库中
javax.websocket
提供了 WebSocket 支持。下面示例是一个简单的服务端:1. 添加依赖(以 Maven 为例)
<!-- 只在Java EE容器或者支持Java WebSocket API的容器中需要 --> <dependency><groupId>javax.websocket</groupId><artifactId>javax.websocket-api</artifactId><version>1.1</version> </dependency>
2. 编写WebSocketConfig配置类
Spring Boot 默认不支持直接扫描 @ServerEndpoint,需要做个配置类注册它:
@Configuration public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();} }
这个 ServerEndpointExporter 负责注册所有带 @ServerEndpoint 注解的 WebSocket 类。
(1)
@ServerEndpoint
注解的类和 Spring 容器
使用标准 Java EE
javax.websocket
API 的@ServerEndpoint
注解时,WebSocket 服务端点是由 Java EE 容器(如 Tomcat、Jetty)扫描并管理的,不依赖 Spring 容器。但是,Spring Boot 本身不会自动扫描和管理 用
@ServerEndpoint
注解的类。
(2) 为什么需要
ServerEndpointExporter
ServerEndpointExporter
是 Spring Boot WebSocket 模块提供的一个 bean,作用是:
让 Spring Boot 容器扫描并注册所有用
@ServerEndpoint
注解的类。把这些端点交给底层的 WebSocket 容器(Tomcat 等)管理。
(3) 什么时候需要
ServerEndpointExporter
?
如果你用的是 Spring Boot 嵌入式 Tomcat 或其他容器,且你的 WebSocket 端点是用标准的
@ServerEndpoint
注解实现的,通常需要配置这个 bean3. 编写 WebSocket 服务器端
@ServerEndpoint("/myWs") @Component @Slf4j public class WsServerEndpont {static Map<String,Session> sessionMap = new ConcurrentHashMap<>();//连接建立时执行的操作@OnOpenpublic void onOpen(Session session){sessionMap.put(session.getId(),session);log.info("websocket is open");}//收到了客户端消息执行的操作@OnMessagepublic String onMessage(String text){log.info("收到了一条消息:"+text);return "已收到你的消息";}//连接关闭的时候执行的操作@OnClosepublic void onClose(Session session){sessionMap.remove(session.getId());log.info("websocket is close");}//每2s发送给客户端心跳消息@Scheduled(fixedRate = 2000)public void sendMsg() throws IOException {for(String key:sessionMap.keySet()){sessionMap.get(key).getBasicRemote().sendText("心跳");}} }
3. 部署运行
需要在支持 Java EE WebSocket 的容器中(如 Tomcat 8+、Jetty、Glassfish)部署。
客户端可用浏览器或工具连接:
ws://localhost:8080/your-app/websocket
四、Spring Boot 中使用 WebSocket(实现多人聊天)
Spring Boot 提供了非常方便的 WebSocket 支持,通常结合 STOMP 协议和 SockJS 来实现消息的订阅和广播。
1. 添加依赖(Maven)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Spring Boot 的依赖管理机制(依赖版本继承)
Spring Boot 使用了 “依赖管理(Dependency Management)”,这是它的核心特性之一。
你只需在你的项目中声明
spring-boot-starter-parent
或通过spring-boot-dependencies
管理依赖版本,Spring Boot 会自动帮你管理和统一版本号。2. 配置 MyWsConfig
// WebSocket配置类,注册WebSocket处理器和拦截器 @Configuration @EnableWebSocket // 启用WebSocket支持 public class MyWsConfig implements WebSocketConfigurer {@ResourceMyWsHandler myWsHandler; // 自定义的WebSocket处理器@ResourceMyWsInterceptor myWsInterceptor; // 自定义的握手拦截器@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 注册WebSocket处理器,路径为/myWs1// 添加握手拦截器,允许所有源跨域请求(*)registry.addHandler(myWsHandler, "/myWs1") // 注册WebSocket处理器及路径.addInterceptors(myWsInterceptor) // 添加握手阶段的拦截器,做一些额外处理.setAllowedOrigins("*"); // 允许所有域发起跨域连接请求} }
2.1 代码讲解:
(1)
addHandler(myWsHandler, "/myWs1")
作用:给 WebSocket 服务器注册一个处理器(Handler),并指定客户端连接时使用的 URL 路径。
myWsHandler 是你自定义的 WebSocket 业务处理类(继承自
WebSocketHandler
或AbstractWebSocketHandler
)。"/myWs1" 是 WebSocket 连接的访问路径,客户端通过
ws://服务器地址/myWs1
来建立 WebSocket 连接。总结:这句相当于告诉服务器,“当客户端请求
/myWs1
路径时,交给myWsHandler
来处理这次 WebSocket 连接和消息。”
(2)
addInterceptors(myWsInterceptor)
作用:给这个 WebSocket 处理器绑定一个或多个拦截器(
HandshakeInterceptor
),用来拦截 WebSocket 握手阶段的请求。握手阶段是 WebSocket 建立连接的第一步,类似 HTTP 请求升级。
你可以在拦截器里做一些额外操作,比如:
记录日志
权限校验
传递用户信息到 WebSocket Session
修改握手请求和响应
你的
myWsInterceptor
继承自HttpSessionHandshakeInterceptor
,默认支持把 HTTP Session 关联到 WebSocket Session。总结:这句是告诉服务器,“在建立 WebSocket 连接握手时,执行
myWsInterceptor
中的逻辑。”
(3)
setAllowedOrigins("*")
作用:设置允许连接的客户端来源(Origin),即支持跨域连接的源。
这里
"*"
表示允许所有源都能连接你的 WebSocket 服务。Origin 是浏览器在 WebSocket 握手请求头里自动带上的,服务器根据它判断是否允许该请求。
注意:生产环境一般不要用
"*"
,建议指定可信的域名,如setAllowedOrigins("https://example.com")
。总结:这句是告诉服务器,“允许哪些来源的网页可以发起 WebSocket 连接请求”。
3. 配置 MyWsHandler
@Slf4j @Component public class MyWsHandler extends AbstractWebSocketHandler {// 存放所有连接的客户端会话,线程安全的Mapprivate static Map<String, SessionBean> sessionBeanMap;// 用于生成客户端唯一ID的原子计数器private static AtomicInteger clientIdMaker;// 用于存放群聊消息的缓冲区private static StringBuffer stringBuffer;static {sessionBeanMap = new ConcurrentHashMap<>();clientIdMaker = new AtomicInteger(0);stringBuffer = new StringBuffer();}/*** 连接建立成功时调用//相当于上面原生的onOpe******************************/@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);// 新建SessionBean,给该连接分配唯一clientIdSessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());// 放入sessionMap管理sessionBeanMap.put(session.getId(), sessionBean);log.info(sessionBean.getClientId() + "建立了连接");// 群聊消息中记录进入群聊通知stringBuffer.append(sessionBean.getClientId() + "进入了群聊<br/>");// 给所有客户端发送当前群聊消息sendMessage(sessionBeanMap);}/*** 收到客户端消息时调用//相当于上面原生的onMessage*********************************/@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);// 记录日志,打印客户端发送的消息log.info(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload());// 将消息追加到群聊缓冲区stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload() + "<br/>");// 广播给所有客户端sendMessage(sessionBeanMap);}/*** 传输发生异常时调用*/@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);// 如果会话还开着,先关闭if (session.isOpen()) {session.close();}// 移除该会话sessionBeanMap.remove(session.getId());}/*** 连接关闭时调用 //相当于上面原生的onClose*****************************/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);// 获取即将关闭连接的clientIdint clientId = sessionBeanMap.get(session.getId()).getClientId();// 移除关闭的会话sessionBeanMap.remove(session.getId());log.info(clientId + "关闭了连接");// 群聊消息中记录退出通知stringBuffer.append(clientId + "退出了群聊<br/>");// 广播给所有客户端sendMessage(sessionBeanMap);}/*** 给所有客户端发送消息*/public void sendMessage(Map<String, SessionBean> sessionBeanMap) {for (String key : sessionBeanMap.keySet()) {try {// 发送群聊缓冲区里的消息给每个客户端sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage(stringBuffer.toString()));} catch (IOException e) {// 日志记录异常log.error(e.getMessage());}}} }
4. 配置 MyWsInterceptor
@Slf4j @Component public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {/*** 握手之前调用,可用于记录日志或做权限校验*/@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {log.info(request.getRemoteAddress().toString() + "开始握手");return super.beforeHandshake(request, response, wsHandler, attributes);}/*** 握手完成调用*/@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Exception ex) {log.info(request.getRemoteAddress().toString() + "完成握手");super.afterHandshake(request, response, wsHandler, ex);} }
5. 配置 SessionBean
// 简单的会话封装类,保存WebSocketSession和客户端唯一ID @AllArgsConstructor @Data public class SessionBean {private WebSocketSession webSocketSession;private Integer clientId; }
6. 启动类WsDemoApplication
// 启动类,开启定时任务支持 @EnableScheduling @SpringBootApplication public class WsDemoApplication {public static void main(String[] args) {SpringApplication.run(WsDemoApplication.class, args);} }
7. 前端示例(HTML + JavaScript)
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>ws client</title> </head> <body> <p style="border:1px solid black;width: 600px;height: 500px" id="talkMsg"></p> <input id="message" /><button id="sendBtn" onclick="sendMsg()">发送</button> </body> <script>let ws = new WebSocket("ws://localhost:8080/myWs1")// ws.onopen=function () {// }ws.onmessage=function (message) {document.getElementById("talkMsg").innerHTML = message.data}function sendMsg() {ws.send(document.getElementById("message").value)document.getElementById("message").value=""} </script> </html>
五、小结
特点 Java 原生 WebSocket Spring Boot WebSocket + STOMP 适用场景 简单直接的 WebSocket 应用 需要消息订阅/广播、复杂消息路由的应用 配置复杂度 低(容器支持即可) 需要配置消息代理,依赖更多 功能支持 基础双向通信 支持订阅、广播、分组、消息转换 前端开发支持 需自行实现协议 使用 STOMP.js 等库简化开发