- 核心概念简介:什么是 WebSocket,为什么用它?
- 项目环境搭建:使用 Maven 和一个简单的 Servlet 容器(如 Tomcat)。
- 后端实现:创建 WebSocket 端点、处理消息和广播。
- 前端实现:创建一个简单的 HTML 页面来连接和收发消息。
- 运行与测试:启动应用并体验聊天功能。
- 进阶与扩展:讨论如何让应用更完善。
核心概念简介
HTTP vs. WebSocket

- HTTP (请求/响应模式):客户端主动发起请求,服务器响应,服务器无法主动向客户端推送信息,想象一下你每隔几秒就问一次服务器“有新消息吗?”,这很浪费资源。
- WebSocket (全双工通信):客户端和服务器之间建立一个长连接,一旦连接建立,双方都可以主动向对方发送消息,这就像一个电话,连接后双方可以随时说话,非常适合实时聊天、在线游戏、股票行情等场景。
WebSocket 端点 在 Java 中,我们使用一个特殊的类来处理 WebSocket 连接,这个类被称为“端点”(Endpoint),它使用注解来声明其功能,
@ServerEndpoint:将一个类标记为 WebSocket 端点,并指定连接的 URL。@OnOpen:当一个新的 WebSocket 连接建立时,此方法被调用。@OnClose:当 WebSocket 连接关闭时,此方法被调用。@OnMessage:当从客户端接收到消息时,此方法被调用。@OnError:当连接过程中发生错误时,此方法被调用。
项目环境搭建
我们将使用 Maven 来管理项目依赖。
创建 Maven 项目
在你的 IDE(如 IntelliJ IDEA 或 Eclipse)中,创建一个新的 Maven 项目。
pom.xml 配置
我们需要添加 Jakarta WebSocket API 和一个 Servlet 容器(如 Tomcat)的依赖,对于开发,我们可以使用 maven-war-plugin 来打包项目。

<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 -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope> <!-- 由 Servlet 容器(如 Tomcat)提供 -->
</dependency>
</dependencies>
<build>
<finalName>java-websocket-chat</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
<!-- 用于在 IDE 中方便运行 Tomcat 的插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
注意:如果你使用的是 Java 8 和旧版的 Java EE (javax),依赖会是 javax.websocket:javax.websocket-api,这里我们使用较新的 Jakarta EE 9 规范。
后端实现
后端的核心是 WebSocket 端点,它负责管理所有连接和消息的广播。
创建 ChatEndpoint.java
在 src/main/java 下创建你的包,com.example.websocket,然后在该包下创建 ChatEndpoint.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 端点。
* value 属性定义了 WebSocket 连接的 URL。
* ws://your-domain.com:8080/java-websocket-chat/chat
*/
@ServerEndpoint("/chat")
public class ChatEndpoint {
// 使用一个静态的 Set 来存储所有活跃的会话。
// 必须是线程安全的,因为多个线程(来自不同连接)可能会同时访问它。
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() + "] 离开了聊天室。");
}
/**
* 当通信过程中发生错误时调用。
* @param session 出错的会话
* @param throwable 异常对象
*/
@OnError
public void onError(Session session, Throwable throwable) {
System.err.println("连接出错: " + session.getId());
throwable.printStackTrace();
}
/**
* 广播消息给所有连接的客户端。
* @param message 要广播的消息
*/
private void broadcast(String message) {
// 遍历所有会话并发送消息
chatroomUsers.forEach(session -> {
try {
// getBasicRemote() 用于同步发送消息
session.getBasicRemote().sendText(message);
} catch (IOException e) {
// 如果发送失败,很可能是因为客户端已经断开连接,我们将其从集合中移除
System.err.println("向会话 " + session.getId() + " 发送消息失败: " + e.getMessage());
chatroomUsers.remove(session);
}
});
}
}
前端实现
前端只需要一个 HTML 文件和一些 JavaScript 代码来连接 WebSocket 并处理用户交互。

在 src/main/webapp 目录下创建 index.html。
<!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-box {
border: 1px solid #ccc;
padding: 10px;
height: 400px;
overflow-y: scroll;
margin-bottom: 10px;
}
#chat-log {
white-space: pre-wrap; /* 保留换行符 */
word-wrap: break-word; /* 长单词自动换行 */
}
#input-box {
display: flex;
}
#message-input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
#send-button {
padding: 8px 15px;
margin-left: 10px;
border: none;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
}
#send-button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1>Java WebSocket 聊天室</h1>
<div id="chat-box">
<div id="chat-log"></div>
</div>
<div id="input-box">
<input type="text" id="message-input" placeholder="输入消息..." onkeypress="if(event.key==='Enter') sendMessage()">
<button id="send-button" onclick="sendMessage()">发送</button>
</div>
<script>
// 获取 DOM 元素
const chatLog = document.getElementById('chat-log');
const messageInput = document.getElementById('message-input');
// 获取当前页面的协议和主机名,构建 WebSocket URL
// 注意:这里的路径需要与后端 @ServerEndpoint 的 value 属性匹配
const wsUrl = `ws://${window.location.host}/java-websocket-chat/chat`;
// 创建 WebSocket 对象
const socket = new WebSocket(wsUrl);
// 连接建立时的回调
socket.onopen = function(event) {
console.log("WebSocket 连接已建立!");
appendLog("系统: 你已成功连接到聊天室。");
};
// 接收到服务器消息时的回调
socket.onmessage = function(event) {
console.log("收到服务器消息: " + event.data);
appendLog(event.data);
};
// 连接关闭时的回调
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`连接已正常关闭,代码=${event.code},原因=${event.reason}`);
} else {
console.error('连接被意外中断');
}
appendLog("系统: 与聊天室的连接已断开。");
};
// 发生错误时的回调
socket.onerror = function(error) {
console.error("WebSocket 错误: " + error);
appendLog("系统: 发生错误,请检查连接。");
};
// 发送消息的函数
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
socket.send(message); // 通过 WebSocket 发送消息
messageInput.value = ''; // 清空输入框
}
}
// 将消息追加到聊天日志的函数
function appendLog(message) {
chatLog.textContent += message + '\n';
// 滚动到底部
chatLog.scrollTop = chatLog.scrollHeight;
}
</script>
</body>
</html>
运行与测试
- 打包项目:在项目根目录下运行 Maven 命令
mvn clean package,这会在target目录下生成一个java-websocket-chat.war文件。 - 部署到 Tomcat:
- 方式一(推荐开发):如果你配置了
tomcat7-maven-plugin,可以直接运行mvn tomcat7:run,它会自动启动一个嵌入式的 Tomcat 服务器并部署你的应用。 - 方式二:将生成的
java-websocket-chat.war文件复制到你安装的 Tomcat 的webapps目录下,然后启动 Tomcat。
- 方式一(推荐开发):如果你配置了
- 打开浏览器:
- 打开两个或多个浏览器窗口(或标签页)。
- 在地址栏输入
http://localhost:8080/(如果你使用了tomcat7:run且path配置为 ) 或者http://localhost:8080/java-websocket-chat/(如果你部署了 .war 文件)。
- 体验聊天:
- 在第一个窗口中输入消息并点击“发送”,你会看到消息出现在该窗口的聊天记录中。
- 立刻在第二个窗口中输入消息并发送,你会看到它出现在所有窗口的聊天记录中。
- 当你关闭其中一个窗口时,其他窗口会立即收到“用户 [xxx] 离开了聊天室”的通知。
进阶与扩展
这个简单的聊天室已经具备了基本功能,但一个生产级的应用还需要更多:
-
用户身份认证:
- 问题:目前用户只是一个匿名的
session.getId()。 - 方案:在
@OnOpen时,可以从 HTTP Session 或 Token 中获取用户信息(如用户名),并将其与 WebSocket Session 关联起来,你需要一个ConcurrentHashMap<Session, User>来存储这种映射关系,广播消息时,使用用户名而不是session.getId()。
- 问题:目前用户只是一个匿名的
-
聊天室/频道:
- 问题:所有用户都在一个公共聊天室。
- 方案:修改 WebSocket 端点的 URL,使其包含房间 ID,
@ServerEndpoint("/chat/{roomId}"),在onOpen方法中,可以从路径参数中获取roomId,并将用户添加到对应房间的Set中,广播时,只向特定房间的用户发送消息。
-
消息持久化:
- 问题:用户刷新页面后,之前的聊天记录就丢失了。
- 方案:将聊天消息保存到数据库(如 MySQL, PostgreSQL)中,当用户连接时,可以从数据库中加载历史消息并显示。
-
安全性:
- 问题:WebSocket 连接容易被未授权的用户滥用。
- 方案:
- HTTPS/WSS:在生产环境中,必须使用
wss://(WebSocket Secure) 协议,它建立在 TLS/SSL 之上。 - 鉴权:如第1点所述,在建立连接前验证用户身份。
- HTTPS/WSS:在生产环境中,必须使用
-
心跳机制:
- 问题:如果客户端或服务器突然崩溃,对方可能无法及时感知连接已断开。
- 方案:实现一个心跳机制,客户端定期向服务器发送一个“ping”消息,服务器收到后立即回复一个“pong”消息,如果客户端在一定时间内没有收到“pong”,就认为连接已断开。
@OnMessage方法需要能够区分心跳消息和普通聊天消息。
这个教程为你提供了一个坚实的基础,你可以基于此进行扩展,构建出功能更强大、更健壮的实时应用。
