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

最终效果
- 一个 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 文件。

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 步:部署和运行
-
打包项目: 在项目根目录下,打开终端或命令提示符,运行 Maven 命令打包项目:
(图片来源网络,侵删)mvn clean package
这将在
target目录下生成一个.war文件(java-websocket-chat-1.0-SNAPSHOT.war)。 -
部署到服务器:
- Tomcat: 将生成的
.war文件复制到 Tomcat 的webapps目录下。 - 其他服务器: 根据你使用的服务器(如 Jetty, WildFly)的部署方式进行部署。
- Tomcat: 将生成的
-
启动服务器: 启动你的 Tomcat(或其他 Web 服务器)。
-
测试:
- 打开浏览器,访问
http://localhost:8080/你的项目名/(http://localhost:8080/java-websocket-chat-1.0-SNAPSHOT/)。 - 打开另一个浏览器窗口或标签页,访问同一个地址。
- 在一个窗口中输入消息并发送,你应该能在另一个窗口的聊天记录中看到这条消息,两个窗口都会收到“用户加入”和“用户离开”的系统消息。
- 打开浏览器,访问
总结与扩展
恭喜!你已经成功创建了一个基础的 Java WebSocket 聊天室。
可以进一步扩展的功能:
- 用户名: 在
onOpen时让用户输入一个用户名,并在广播消息时显示用户名而不是session.getId()。 - 私聊: 实现一个
/私聊 用户名 消息的命令,将消息只发送给指定的用户。 - 在线用户列表: 在页面上显示一个当前在线用户列表。
- 消息持久化: 将聊天记录保存到数据库中。
- 表情和图片支持: 扩展协议以支持富文本消息。
这个例子为你提供了一个坚实的基础,你可以基于它构建更复杂的实时 Web 应用。
