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.ByteBufferbyte[] 表示
  • Java 对象:允许在代码中使用原生 Java 对象,通过自定义转换器(编码器/解码器)将其转换为 WebSocket 协议兼容的格式(文本/二进制)
  • Ping-Pongjakarta.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 应用开发变得异常简单。


原始标题:A Guide to the Java API for WebSocket | Baeldung