杰瑞科技汇

Java WebSocket如何实现?关键步骤有哪些?

  1. Java EE (Jakarta EE) / Jakarta EE 方式:这是 Java 官方提供的标准 API,在 Jakarta EE 9+ (原 Java EE) 容器(如 Tomcat 10+, WildFly, Payara)中内置支持,这是最标准、最推荐的方式,尤其适合构建企业级应用。
  2. Spring Boot 方式:在 Spring Boot 框架中实现 WebSocket,通过 spring-boot-starter-websocket 提供了非常便捷和强大的集成,是目前在微服务和新项目中非常流行的选择。

使用 Java EE (Jakarta EE) API

这种方式的核心是创建一个 Endpoint 类来处理 WebSocket 连接的生命周期事件。

Java WebSocket如何实现?关键步骤有哪些?-图1
(图片来源网络,侵删)

环境准备

你需要一个支持 Jakarta EE 9+ 的 Web 服务器,

  • Apache Tomcat 10+
  • WildFly 26+
  • Payara 6+

创建 WebSocket 端点

创建一个 Java 类,继承 jakarta.websocket.Endpoint 并重写其生命周期方法。

// package com.example.websocket;
import jakarta.websocket.*;
import java.io.IOException;
@ServerEndpoint("/echo") // 定义 WebSocket 的访问路径
public class EchoEndpoint {
    // 当一个新的 WebSocket 连接建立时被调用
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("连接已建立,Session ID: " + session.getId());
        // 可以在这里将 session 存储起来,以便后续向特定客户端发送消息
    }
    // 当客户端发送消息时被调用
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        System.out.println("收到来自客户端 " + session.getId() + " 的消息: " + message);
        // 将收到的消息回显给客户端
        session.getBasicRemote().sendText("服务器回显: " + message);
    }
    // 当连接关闭时被调用
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        System.out.println("连接已关闭,Session ID: " + session.getId() + ", 原因: " + closeReason);
    }
    // 当连接过程中发生错误时被调用
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.println("连接发生错误,Session ID: " + session.getId());
        throwable.printStackTrace();
    }
}

注册 WebSocket 端点

在 Jakarta EE 中,你可以通过一个 ServerApplicationConfig 类来编程式地注册你的 Endpoint,或者在 web.xml 中声明式地注册。编程式注册更灵活,推荐使用

创建 ServerApplicationConfig 类:

Java WebSocket如何实现?关键步骤有哪些?-图2
(图片来源网络,侵删)
// package com.example.websocket;
import jakarta.websocket.Endpoint;
import jakarta.websocket.server.ServerApplicationConfig;
import jakarta.websocket.server.ServerEndpointConfig;
import java.util.Set;
public class MyWebSocketConfig implements ServerApplicationConfig {
    @Override
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
        // 返回我们自定义的 Endpoint 配置
        // 这里我们手动创建并配置我们的 EchoEndpoint
        ServerEndpointConfig config = ServerEndpointConfig.Builder.create(
            EchoEndpoint.class, // 我们的 Endpoint 类
            "/echo"             // 访问路径
        ).build();
        return Set.of(config);
    }
    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scannedClasses) {
        // 如果我们使用 @ServerEndpoint 注解,容器会自动扫描。
        // 这里我们返回空集,因为我们上面是手动配置的。
        return scannedClasses;
    }
}

创建前端页面 (index.html)

webapp 目录下创建一个 index.html 文件来测试。

<!DOCTYPE html>
<html>
<head>Java WebSocket 测试</title>
</head>
<body>
    <h2>Java WebSocket 聊天测试</h2>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="输入消息...">
    <button onclick="sendMessage()">发送</button>
    <script>
        // 1. 建立 WebSocket 连接
        // 注意:ws:// 对应 http, wss:// 对应 https
        // window.location.host 可以获取当前主机和端口
        const socket = new WebSocket("ws://" + window.location.host + "/echo");
        // 2. 监听打开事件
        socket.onopen = function(event) {
            console.log("WebSocket 连接已打开!");
            document.getElementById("messages").innerHTML += "<p>连接成功!</p>";
        };
        // 3. 监听消息事件
        socket.onmessage = function(event) {
            console.log("收到服务器消息: " + event.data);
            const messagesDiv = document.getElementById("messages");
            messagesDiv.innerHTML += "<p>服务器: " + event.data + "</p>";
        };
        // 4. 监听关闭事件
        socket.onclose = function(event) {
            if (event.wasClean) {
                console.log(`连接已正常关闭,代码=${event.code} 原因=${event.reason}`);
            } else {
                console.error('连接被异常中断');
            }
        };
        // 5. 监听错误事件
        socket.onerror = function(error) {
            console.error("WebSocket 错误: " + error);
        };
        // 发送消息的函数
        function sendMessage() {
            const input = document.getElementById("messageInput");
            const message = input.value;
            if (message) {
                socket.send(message);
                input.value = ""; // 清空输入框
            }
        }
    </script>
</body>
</html>

部署和测试

  1. 将你的 Java 项目打包成 WAR 文件。
  2. 部署到 Tomcat 10+ 服务器。
  3. 启动服务器,访问 http://localhost:8080/你的项目名/
  4. 打开浏览器的开发者工具(F12),在 Console 标签页可以看到连接日志。
  5. 在输入框中输入消息,点击发送,可以看到页面下方显示服务器回显的消息。

使用 Spring Boot

Spring Boot 对 WebSocket 的封装使得配置和使用都非常简单。

环境准备

  • 安装 JDK 8+。
  • 使用 Spring Initializr (https://start.spring.io/) 创建一个新项目,添加以下依赖:
    • Spring Web:用于提供 Web 服务。
    • Spring WebSocket:核心 WebSocket 支持。

启用 WebSocket

在你的 Spring Boot 主应用类上添加 @EnableWebSocket 注解。

// package com.example.websocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
@SpringBootApplication
@EnableWebSocket // 启用 WebSocket 支持
public class WebSocketApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketApplication.class, args);
    }
}

配置 WebSocket 并创建处理器

创建一个配置类来注册 WebSocket 端点,并创建一个处理器类来处理消息逻辑。

Java WebSocket如何实现?关键步骤有哪些?-图3
(图片来源网络,侵删)

WebSocket 配置类 (WebSocketConfig.java)

// package com.example.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.beans.factory.annotation.Autowired;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MyWebSocketHandler webSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册我们的处理器
        // .addHandler(处理器实例, "访问路径")
        // .setAllowedOrigins("*") 允许所有来源的连接,生产环境请设置具体域名
        registry.addHandler(webSocketHandler, "/chat")
                  .setAllowedOrigins("*");
    }
}

WebSocket 处理器类 (MyWebSocketHandler.java)

Spring Boot 推荐使用 WebSocketHandler 接口,它提供了更丰富的回调方法。

// package com.example.websocket;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
    // 使用一个线程安全的 Map 来存储所有在线的会话
    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    // 连接建立后
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("新连接建立: " + session.getId());
        sessions.put(session.getId(), session);
        // 可以在这里广播新用户加入的消息
        broadcastMessage("用户 " + session.getId() + " 加入了聊天室。");
    }
    // 收到文本消息后
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("收到来自 " + session.getId() + " 的消息: " + message.getPayload());
        // 广播消息给所有连接的客户端
        broadcastMessage("用户 " + session.getId() + " 说: " + message.getPayload());
    }
    // 连接关闭后
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("连接关闭: " + session.getId() + ", 原因: " + status);
        sessions.remove(session.getId());
        // 广播用户离开的消息
        broadcastMessage("用户 " + session.getId() + " 离开了聊天室。");
    }
    // 广播消息的辅助方法
    private void broadcastMessage(String message) {
        TextMessage textMessage = new TextMessage(message);
        sessions.forEach((id, session) -> {
            try {
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}

创建前端页面 (index.html)

前端代码与方案一中的 index.html 几乎完全相同,只需要确保 WebSocket 的连接地址是 /chat

<!DOCTYPE html>
<html>
<head>Spring WebSocket 测试</title>
    <style>
        #messages { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; }
        #messageInput { width: 80%; }
    </style>
</head>
<body>
    <h2>Spring WebSocket 聊天室</h2>
    <div id="messages"></div>
    <br>
    <input type="text" id="messageInput" placeholder="输入消息...">
    <button onclick="sendMessage()">发送</button>
    <script>
        const socket = new WebSocket("ws://" + window.location.host + "/chat");
        socket.onopen = function(event) {
            console.log("WebSocket 连接已打开!");
            appendMessage("系统:连接成功!");
        };
        socket.onmessage = function(event) {
            console.log("收到服务器消息: " + event.data);
            appendMessage(event.data);
        };
        socket.onclose = function(event) {
            console.log(`连接已关闭: ${event.code} ${event.reason}`);
            appendMessage("系统:连接已关闭。");
        };
        socket.onerror = function(error) {
            console.error("WebSocket 错误: " + error);
            appendMessage("系统:发生连接错误。");
        };
        function sendMessage() {
            const input = document.getElementById("messageInput");
            const message = input.value;
            if (message) {
                socket.send(message);
                input.value = "";
            }
        }
        function appendMessage(message) {
            const messagesDiv = document.getElementById("messages");
            const p = document.createElement("p");
            p.textContent = message;
            messagesDiv.appendChild(p);
            messagesDiv.scrollTop = messagesDiv.scrollHeight; // 自动滚动到底部
        }
        // 监听回车键发送
        document.getElementById("messageInput").addEventListener("keypress", function(event) {
            if (event.key === "Enter") {
                sendMessage();
            }
        });
    </script>
</body>
</html>

运行和测试

  1. 运行 WebSocketApplication.javamain 方法。
  2. 打开浏览器访问 http://localhost:8080
  3. 打开两个或更多浏览器窗口,都访问同一个地址。
  4. 在一个窗口输入消息并发送,你会看到所有窗口都收到了这条广播消息。

总结与对比

特性 Java EE (Jakarta EE) API Spring Boot
核心思想 标准 API,基于 Endpoint 和生命周期注解 (@OnOpen, @OnMessage 等) 基于 WebSocketHandler 接口,更面向对象和 Spring 风格
配置方式 编程式 (ServerApplicationConfig) 或声明式 (web.xml) 通过 @Configuration 类和 WebSocketConfigurer 接口
状态管理 需要自己用 ConcurrentHashMap 等工具管理 Session 同样需要自己管理,但与 Spring 的其他部分(如 Security)集成更方便
优点 标准、规范,与 Java EE 容器深度集成,无额外依赖 配置简单、集成度高,与 Spring 生态(Security, MVC等)无缝协作,开发效率高
缺点 配置略显繁琐,功能相对基础 引入了 Spring 框架,对于简单的 WebSocket 应用可能显得“重”
适用场景 传统 Java EE 项目,需要遵循标准规范的场景 新项目,特别是基于 Spring Boot 或 Spring Cloud 的微服务架构

如何选择?

  • 如果你的项目已经是基于 Jakarta EE / Java EE 的,或者你希望遵循最标准、最官方的实现方式,选择方案一
  • 如果你正在构建一个新的项目,特别是使用 Spring Boot 框架,或者希望获得更快的开发速度和更好的集成体验,强烈推荐方案二,它是目前社区的主流选择。
分享:
扫描分享到社交APP
上一篇
下一篇