目录
- WebSocket 简介
- 与 HTTP 的区别
- 为什么需要 WebSocket?
- Java WebSocket API (JSR-356)
- 核心概念:
@ServerEndpoint,@OnOpen,@OnClose,@OnMessage,@OnError
- 核心概念:
- 原生 API 实现 (Tomcat)
- 步骤详解
- 完整代码示例
- Spring Boot 集成实现 (推荐)
- 为什么推荐 Spring Boot?
- 步骤详解
- 完整代码示例
- 客户端测试
HTML + JavaScript 客户端
(图片来源网络,侵删) - 生产环境考虑
WebSocket 简介
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间可以实时、高效地交换数据。
与 HTTP 的区别
| 特性 | HTTP | WebSocket |
|---|---|---|
| 连接模式 | 客户端请求,服务器响应 (单向) | 客户端和服务器均可主动发送消息 (全双工) |
| 连接状态 | 无状态,每次请求都建立新连接 | 有状态,建立一次长连接,后续通信复用此连接 |
| 协议 | HTTP/1.1, HTTP/2 | WebSocket (ws://, wss://) |
| 性能 | 每次通信都有较大的头部开销 | 头部非常小,通信开销低,延迟低 |
| 用途 | 请求/响应模型,如网页加载、API调用 | 实时通信,如聊天室、在线游戏、股票行情 |
为什么需要 WebSocket?
对于需要服务器主动向客户端推送数据的场景,HTTP 的轮询或长轮询效率极低,WebSocket 解决了这个问题,一旦连接建立,服务器可以随时向客户端推送信息,无需客户端反复请求,非常适合:
- 即时通讯 (聊天室、私信)
- 实时数据更新 (股票价格、体育赛事比分)
- 在线协作 (多人文档编辑、白板)
- 游戏 (实时对战状态同步)
Java WebSocket API (JSR-356)
Java EE 7 引入了标准的 WebSocket API (JSR-356),这使得在 Java 应用中实现 WebSocket 变得非常简单,核心是 javax.websocket 包。
核心概念
-
@ServerEndpoint: 这是一个注解,用于将一个 Java 类声明为 WebSocket 端点,你需要指定端点的 URL 路径。
(图片来源网络,侵删)@ServerEndpoint("/chat") public class ChatEndpoint { // ... } -
Session: 代表一个 WebSocket 连接,服务器可以通过Session向客户端发送消息 (getBasicRemote().sendText(...)),并获取连接信息。 -
生命周期方法: 在类中使用特定注解的方法来处理连接的不同事件。
@OnOpen: 当一个新的 WebSocket 连接建立时,此方法被调用,通常用于初始化工作,如将新连接加入某个集合。@OnClose: 当 WebSocket 连接关闭时,此方法被调用,通常用于清理工作,如从集合中移除断开的连接。@OnMessage: 当从客户端接收到消息时,此方法被调用,可以处理文本、二进制等不同类型的数据。@OnError: 当连接过程中发生错误时,此方法被调用。
原生 API 实现 (以 Tomcat 为例)
这是最基础的实现方式,可以帮助你理解 WebSocket 的工作原理,我们使用一个支持 WebSocket 的 Servlet 容器,如 Apache Tomcat。
步骤详解
-
环境准备:
(图片来源网络,侵删)- 安装 JDK 8+。
- 下载并安装 Tomcat 9+ (Tomcat 8+ 即可)。
- 一个 IDE (如 IntelliJ IDEA 或 Eclipse)。
-
创建项目:
- 在 IDE 中创建一个新的 "Dynamic Web Project"。
- 确保项目的 Target runtime 设置为你的 Tomcat 服务器。
- 项目会自动包含
servlet-api.jar,你还需要手动添加javax.websocket-api.jar(通常在 Tomcat 的lib目录下可以找到,或者从 Maven 仓库下载)。
-
编写 WebSocket 端点类:
- 创建一个 Java 类,
ChatEndpoint.java。 - 添加
@ServerEndpoint注解和生命周期方法。
- 创建一个 Java 类,
-
部署和测试:
- 将项目部署到 Tomcat。
- 编写一个 HTML 客户端来连接和测试。
完整代码示例
ChatEndpoint.java
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ServerEndpoint("/chat")
public class ChatEndpoint {
// 使用一个静态的 Set 来存储所有活跃的会话,实现广播功能
// 注意:这个集合不是线程安全的,在并发环境下需要使用 ConcurrentHashMap 等结构
private static final Set<Session> chatroomUsers = Collections.synchronizedSet(new HashSet<>());
@OnOpen
public void onOpen(Session session) {
// 将新会话添加到集合中
chatroomUsers.add(session);
System.out.println("新连接已建立: " + session.getId());
// 通知所有用户有人加入
broadcast("用户 " + session.getId() + " 加入了聊天室。");
}
@OnClose
public void onClose(Session session) {
// 从集合中移除断开的会话
chatroomUsers.remove(session);
System.out.println("连接已关闭: " + session.getId());
// 通知所有用户有人离开
broadcast("用户 " + session.getId() + " 离开了聊天室。");
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自 " + session.getId() + " 的消息: " + message);
// 将接收到的消息广播给所有用户
broadcast("用户 " + session.getId() + ": " + message);
}
@OnError
public void onError(Throwable error, Session session) {
System.err.println("发生错误: " + session.getId());
error.printStackTrace();
}
/**
* 广播消息给所有连接的客户端
* @param message 要广播的消息
*/
private void broadcast(String message) {
// 遍历所有会话并发送消息
chatroomUsers.forEach(session -> {
try {
// 检查会话是否仍然打开
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
Spring Boot 集成实现 (推荐)
在实际生产环境中,我们几乎不会使用原生 API,而是会使用 Spring Boot,因为它提供了更强大的集成、配置和依赖管理。
为什么推荐 Spring Boot?
- 快速启动: 内嵌 Tomcat/Jetty 无需额外安装。
- 自动配置: Spring Boot 自动配置 WebSocket,减少样板代码。
- 集成度高: 方便与 Spring Security、Spring MVC 等框架集成。
- 编程模型: 除了支持
@ServerEndpoint,还支持更符合 Spring 思想的@Controller模式。
步骤详解
-
创建 Spring Boot 项目:
- 使用 Spring Initializr 创建项目。
- 添加依赖:
Spring Web,Spring WebSocket,Spring Boot DevTools(可选,用于热部署)。 - 生成项目并导入 IDE。
-
启用 WebSocket 支持:
- 创建一个配置类,继承
WebSocketMessageBrokerConfigurer。 - 重写
configureMessageBroker和registerStompEndpoints方法。
- 创建一个配置类,继承
-
创建 WebSocket 端点:
- 使用
@Controller注解创建一个控制器。 - 使用
@MessageMapping注解来处理特定目的地的消息。 - 使用
@SendTo注解将方法的返回值发送到指定的目的地,实现广播。
- 使用
完整代码示例
pom.xml (确保有这些依赖)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 其他依赖... -->
</dependencies>
WebSocketConfig.java
