杰瑞科技汇

Java WebSocket聊天室如何实现实时消息交互?

我们将创建一个简单的、基于服务器的广播聊天室,所有连接到该聊天室的客户端都会收到彼此发送的消息。

Java WebSocket聊天室如何实现实时消息交互?-图1
(图片来源网络,侵删)

最终效果

  • 一个 HTML/JavaScript 客户端页面。
  • 一个 Java WebSocket 端点,处理所有连接和消息。
  • 当一个用户发送消息时,服务器将消息广播给所有在线用户。

第 1 步:项目环境准备

我们将使用 Maven 来管理项目,创建一个标准的 Maven Web 项目。

1. pom.xml 文件

确保你的 pom.xml 文件包含了必要的依赖,对于 Java WebSocket API,你只需要一个 provided 范围的依赖,因为这个 API 通常由 Web 容器(如 Tomcat, Jetty)提供。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>java-websocket-chat</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- Jakarta WebSocket API (provided by the server) -->
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>9.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- For older Java EE versions, use this:
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
        -->
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
        </plugins>
    </build>
</project>

第 2 步:创建 WebSocket 端点

这是聊天室的核心,我们将创建一个 Java 类来处理 WebSocket 连接、关闭、错误和消息接收。

src/main/java 下创建你的包,com.example.websocket,然后创建 ChatServerEndpoint.java 文件。

Java WebSocket聊天室如何实现实时消息交互?-图2
(图片来源网络,侵删)
package com.example.websocket;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
 * WebSocket 聊天室端点
 * @ServerEndpoint 注解将该类标记为 WebSocket 端点。
 * 值 "/chat" 是 WebSocket 连接的 URI。
 */
@ServerEndpoint("/chat")
public class ChatServerEndpoint {
    // 使用一个线程安全的 Set 来存储所有活跃的 WebSocket 会话
    // static 保证所有实例共享这个集合
    private static final Set<Session> chatroomUsers = Collections.synchronizedSet(new HashSet<>());
    /**
     * 当新的 WebSocket 连接建立时调用此方法。
     * @param session 与客户端关联的会话
     */
    @OnOpen
    public void onOpen(Session session) {
        // 将新用户的会话添加到集合中
        chatroomUsers.add(session);
        System.out.println("新用户连接: " + session.getId());
        // 广播新用户加入的消息
        broadcast("用户 [" + session.getId() + "] 加入了聊天室。");
    }
    /**
     * 当客户端发送消息时调用此方法。
     * @param message 客户端发送的消息
     * @param session 发送消息的客户端会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自用户 " + session.getId() + " 的消息: " + message);
        // 将收到的消息广播给所有用户
        broadcast("用户 [" + session.getId() + "]: " + message);
    }
    /**
     * 当 WebSocket 连接关闭时调用此方法。
     * @param session 关闭的会话
     * @param closeReason 关闭原因
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        // 从集合中移除已关闭的会话
        chatroomUsers.remove(session);
        System.out.println("用户断开连接: " + session.getId() + ", 原因: " + closeReason.getReasonPhrase());
        // 广播用户离开的消息
        broadcast("用户 [" + session.getId() + "] 离开了聊天室。");
    }
    /**
     * 当 WebSocket 连接过程中发生错误时调用此方法。
     * @param session 出错的会话
     * @param throwable 捕获到的异常
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.println("WebSocket 错误 - 会话 ID: " + session.getId());
        throwable.printStackTrace();
    }
    /**
     * 辅助方法:向所有连接的客户端广播消息。
     * @param message 要广播的消息
     */
    private void broadcast(String message) {
        // 同步遍历集合,避免并发修改问题
        chatroomUsers.forEach(session -> {
            try {
                // 检查会话是否仍然打开
                if (session.isOpen()) {
                    // getBasicRemote() 是同步的,对于大量用户可能成为性能瓶颈
                    // 对于高性能场景,应考虑 getAsyncRemote()
                    session.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                System.err.println("向用户 " + session.getId() + " 发送消息失败: " + e.getMessage());
                // 如果发送失败,很可能连接已经出问题,可以安全地移除它
                chatroomUsers.remove(session);
            }
        });
    }
}

代码解释:

  • @ServerEndpoint("/chat"): 告诉服务器这是一个 WebSocket 端点,客户端可以通过 ws://your-server/chat 来连接它。
  • @OnOpen, @OnMessage, @OnClose, @OnError: 这些是生命周期方法注解,分别在连接建立、收到消息、连接关闭和发生错误时自动调用。
  • Set<Session> chatroomUsers: 我们使用一个静态的 Set 来存储所有连接到 /chat 端点的用户会话。Collections.synchronizedSet 确保 Set 的线程安全。
  • broadcast() 方法:遍历所有用户会话,并向每个活跃的会话发送相同的消息。

第 3 步:创建客户端页面

src/main/webapp 目录下创建一个 index.html 文件,这个页面将包含聊天室的界面和与 WebSocket 服务器通信的 JavaScript 代码。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">Java WebSocket 聊天室</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #chat-log {
            border: 1px solid #ccc;
            padding: 10px;
            height: 400px;
            overflow-y: scroll;
            margin-bottom: 10px;
        }
        #chat-log div {
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .system-message { color: #888; font-style: italic; }
        #message-form { display: flex; }
        #message-input {
            flex-grow: 1;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px 0 0 4px;
        }
        #send-button {
            padding: 8px 15px;
            border: 1px solid #007bff;
            background-color: #007bff;
            color: white;
            border-radius: 0 4px 4px 0;
            cursor: pointer;
        }
        #send-button:hover { background-color: #0056b3; }
        #status { margin-bottom: 10px; font-weight: bold; }
    </style>
</head>
<body>
    <h1>Java WebSocket 聊天室</h1>
    <div id="status">连接中...</div>
    <div id="chat-log"></div>
    <form id="message-form">
        <input type="text" id="message-input" placeholder="输入消息..." autocomplete="off" disabled>
        <button id="send-button" type="submit" disabled>发送</button>
    </form>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const chatLog = document.getElementById('chat-log');
            const messageForm = document.getElementById('message-form');
            const messageInput = document.getElementById('message-input');
            const sendButton = document.getElementById('send-button');
            const statusDiv = document.getElementById('status');
            // 获取当前页面的协议 (http -> ws, https -> wss)
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsUrl = `${protocol}//${window.location.host}${window.location.pathname}chat`;
            let socket;
            function connect() {
                socket = new WebSocket(wsUrl);
                socket.onopen = () => {
                    statusDiv.textContent = '已连接';
                    statusDiv.style.color = 'green';
                    messageInput.disabled = false;
                    sendButton.disabled = false;
                    logMessage('你已加入聊天室。', 'system-message');
                };
                socket.onmessage = (event) => {
                    // 服务器广播的消息直接显示
                    logMessage(event.data);
                };
                socket.onclose = () => {
                    statusDiv.textContent = '连接已断开,尝试重新连接...';
                    statusDiv.style.color = 'red';
                    messageInput.disabled = true;
                    sendButton.disabled = true;
                    // 可以在这里添加自动重连逻辑
                    setTimeout(connect, 3000); // 3秒后重连
                };
                socket.onerror = (error) => {
                    logMessage('发生错误: ' + error.message, 'system-message');
                    statusDiv.textContent = '连接错误';
                    statusDiv.style.color = 'red';
                };
            }
            function logMessage(message, className = '') {
                const messageElement = document.createElement('div');
                messageElement.textContent = message;
                messageElement.className = className;
                chatLog.appendChild(messageElement);
                // 自动滚动到底部
                chatLog.scrollTop = chatLog.scrollHeight;
            }
            messageForm.addEventListener('submit', (event) => {
                event.preventDefault(); // 阻止表单默认提交行为
                const message = messageInput.value.trim();
                if (message) {
                    // 发送消息到服务器
                    socket.send(message);
                    messageInput.value = ''; // 清空输入框
                }
            });
            // 初始连接
            connect();
        });
    </script>
</body>
</html>

JavaScript 代码解释:

  • new WebSocket(wsUrl): 创建一个 WebSocket 连接,URL 的协议和主机名从当前页面自动获取,路径是 /chat(与我们 @ServerEndpoint 中定义的匹配)。
  • socket.onopen, socket.onmessage, socket.onclose, socket.onerror: 这些事件处理器对应服务器端的 @OnOpen, @OnMessage, @OnClose, @OnError 方法。
  • socket.send(message): 将输入框中的文本发送到服务器。
  • logMessage(): 一个辅助函数,用于将消息添加到聊天记录区域,并自动滚动到底部。

第 4 步:部署和运行

  1. 打包项目: 在项目根目录下,打开终端或命令提示符,运行 Maven 命令打包项目:

    Java WebSocket聊天室如何实现实时消息交互?-图3
    (图片来源网络,侵删)
    mvn clean package

    这将在 target 目录下生成一个 .war 文件(java-websocket-chat-1.0-SNAPSHOT.war)。

  2. 部署到服务器:

    • Tomcat: 将生成的 .war 文件复制到 Tomcat 的 webapps 目录下。
    • 其他服务器: 根据你使用的服务器(如 Jetty, WildFly)的部署方式进行部署。
  3. 启动服务器: 启动你的 Tomcat(或其他 Web 服务器)。

  4. 测试:

    • 打开浏览器,访问 http://localhost:8080/你的项目名/ (http://localhost:8080/java-websocket-chat-1.0-SNAPSHOT/)。
    • 打开另一个浏览器窗口或标签页,访问同一个地址。
    • 在一个窗口中输入消息并发送,你应该能在另一个窗口的聊天记录中看到这条消息,两个窗口都会收到“用户加入”和“用户离开”的系统消息。

总结与扩展

恭喜!你已经成功创建了一个基础的 Java WebSocket 聊天室。

可以进一步扩展的功能:

  1. 用户名: 在 onOpen 时让用户输入一个用户名,并在广播消息时显示用户名而不是 session.getId()
  2. 私聊: 实现一个 /私聊 用户名 消息 的命令,将消息只发送给指定的用户。
  3. 在线用户列表: 在页面上显示一个当前在线用户列表。
  4. 消息持久化: 将聊天记录保存到数据库中。
  5. 表情和图片支持: 扩展协议以支持富文本消息。

这个例子为你提供了一个坚实的基础,你可以基于它构建更复杂的实时 Web 应用。

分享:
扫描分享到社交APP
上一篇
下一篇