1. 概述
WebSocket 提供了一种解决服务器与浏览器间高效通信限制的方案,实现了双向、全双工的实时客户端/服务器通信。服务器可以随时向客户端推送数据。由于基于 TCP 协议,它还提供了低延迟、低开销的通信能力,并降低了每条消息的处理成本。
本教程将通过构建一个类聊天应用来探索 Java WebSocket API 的使用。
2. JSR 356
JSR 356 即 Java WebSocket API,定义了一套供 Java 开发者使用的 API,用于在应用中集成 WebSocket 功能,涵盖服务器端和 Java 客户端。
该 API 包含以下组件:
- 服务器端:
jakarta.websocket.server
包中的所有内容 - 客户端:
jakarta.websocket
包中的内容,包含客户端 API 和服务器/客户端共享的通用库
3. 使用 WebSocket 构建聊天应用
我们将构建一个极简的类聊天应用。任何用户都能通过浏览器打开聊天界面,输入用户名登录后,即可与所有在线用户实时交流。
首先在 pom.xml
中添加最新依赖:
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.2.0</version>
</dependency>
最新版本可在 这里 查找。
为处理 Java 对象与 JSON 的转换,我们使用 Gson:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.0</version>
</dependency>
最新版本可在 Maven Central 仓库获取。
3.1. 接口配置
配置 WebSocket 接口有两种方式:基于注解和基于扩展。我们可以继承 jakarta.websocket.Endpoint
类,或使用方法级注解。由于注解模型比编程模型代码更简洁,已成为主流选择。我们将使用以下注解处理 WebSocket 生命周期事件:
@ServerEndpoint
:标记类作为 WebSocket 服务器,监听特定 URI@ClientEndpoint
:标记类作为 WebSocket 客户端@OnOpen
:当新 WebSocket 连接建立时调用@OnMessage
:当接口收到消息时调用@OnError
:通信出现问题时调用@OnClose
:当 WebSocket 连接关闭时调用
3.2. 编写服务器接口
使用 @ServerEndpoint
注解声明 Java 类作为 WebSocket 服务器接口,并指定部署 URI。URI 需相对于服务器容器根路径,且必须以斜杠开头:
@ServerEndpoint(value = "/chat/{username}")
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session) throws IOException {
// 获取会话和 WebSocket 连接
}
@OnMessage
public void onMessage(Session session, Message message) throws IOException {
// 处理新消息
}
@OnClose
public void onClose(Session session) throws IOException {
// WebSocket 连接关闭
}
@OnError
public void onError(Session session, Throwable throwable) {
// 错误处理逻辑
}
}
上述代码是聊天应用的服务器接口骨架。下面是具体实现:
@ServerEndpoint(value="/chat/{username}")
public class ChatEndpoint {
private Session session;
private static Set<ChatEndpoint> chatEndpoints
= new CopyOnWriteArraySet<>();
private static HashMap<String, String> users = new HashMap<>();
@OnOpen
public void onOpen(
Session session,
@PathParam("username") String username) throws IOException {
this.session = session;
chatEndpoints.add(this);
users.put(session.getId(), username);
Message message = new Message();
message.setFrom(username);
message.setContent("Connected!");
broadcast(message);
}
@OnMessage
public void onMessage(Session session, Message message)
throws IOException {
message.setFrom(users.get(session.getId()));
broadcast(message);
}
@OnClose
public void onClose(Session session) throws IOException {
chatEndpoints.remove(this);
Message message = new Message();
message.setFrom(users.get(session.getId()));
message.setContent("Disconnected!");
broadcast(message);
}
@OnError
public void onError(Session session, Throwable throwable) {
// 错误处理逻辑
}
private static void broadcast(Message message)
throws IOException, EncodeException {
chatEndpoints.forEach(endpoint -> {
synchronized (endpoint) {
try {
endpoint.session.getBasicRemote().
sendObject(message);
} catch (IOException | EncodeException e) {
e.printStackTrace();
}
}
});
}
}
当新用户登录时(@OnOpen
),立即将其加入活跃用户数据结构,然后创建消息并通过 broadcast
方法广播给所有接口。
该方法也用于处理任何用户发送的新消息(@OnMessage
),这是聊天的核心功能。
若发生错误(@OnError
),则记录错误信息并清理接口。
最后当用户断开连接时(@OnClose
),清理接口并广播用户离线消息。
4. 消息类型
WebSocket 规范支持两种线上数据格式:文本和二进制。该 API 同时支持这两种格式,并增加了处理 Java 对象和健康检查消息(ping-pong)的能力:
- 文本:任何文本数据(如
java.lang.String
、基本类型及其包装类) - 二进制:二进制数据(如音频、图像等),通过
java.nio.ByteBuffer
或byte[]
表示 - Java 对象:允许在代码中使用原生 Java 对象,通过自定义转换器(编码器/解码器)将其转换为 WebSocket 协议兼容的格式(文本/二进制)
- Ping-Pong:
jakarta.websocket.PongMessage
是对等端响应健康检查(ping)请求的确认消息
本应用中我们将使用 Java 对象。需要创建消息的编码和解码类。
4.1. 编码器
编码器将 Java 对象转换为适合传输的表示形式(如 JSON、XML 或二进制)。通过实现 Encoder.Text<T>
或 Encoder.Binary<T>
接口创建编码器。
下面定义 Message
类及编码器,使用 Gson 将 Java 对象编码为 JSON:
public class Message {
private String from;
private String to;
private String content;
// 标准构造函数、getter/setter
}
public class MessageEncoder implements Encoder.Text<Message> {
private static Gson gson = new Gson();
@Override
public String encode(Message message) throws EncodeException {
return gson.toJson(message);
}
@Override
public void init(EndpointConfig endpointConfig) {
// 自定义初始化逻辑
}
@Override
public void destroy() {
// 释放资源
}
}
4.2. 解码器
解码器执行相反操作,将数据转换回 Java 对象。通过实现 Decoder.Text<T>
或 Decoder.Binary<T>
接口创建。
在 decode
方法中,使用 Gson 将消息中的 JSON 转换为 Message
对象:
public class MessageDecoder implements Decoder.Text<Message> {
private static Gson gson = new Gson();
@Override
public Message decode(String s) throws DecodeException {
return gson.fromJson(s, Message.class);
}
@Override
public boolean willDecode(String s) {
return (s != null);
}
@Override
public void init(EndpointConfig endpointConfig) {
// 自定义初始化逻辑
}
@Override
public void destroy() {
// 释放资源
}
}
4.3. 在服务器接口中配置编码器和解码器
在 @ServerEndpoint
注解中添加编码器和解码器类:
@ServerEndpoint(
value="/chat/{username}",
decoders = MessageDecoder.class,
encoders = MessageEncoder.class )
此后,所有发送到接口的消息将自动在 JSON 和 Java 对象间转换。
5. 总结
本文分析了 Java WebSocket API,并展示了如何构建实时聊天应用等场景。
我们讨论了创建接口的两种编程模型(注解式和编程式),然后使用注解模型为应用定义了接口及其生命周期方法。
此外,为实现服务器与客户端的双向通信,我们演示了如何通过编码器和解码器在 Java 对象与 JSON 间转换。
JSR 356 API 设计简洁,基于注解的编程模型使 WebSocket 应用开发变得异常简单。