J2EE WebSocket 实现方式
# J2EE WebSocket 实现方式
WebSocket 是一种在客户端和服务器之间进行全双工通信的协议,广泛用于实时聊天、通知、游戏等场景。在 J2EE 的环境下,我们可以利用 javax.websocket
包提供的注解和接口,结合 Tomcat 或 Spring Boot 内嵌容器,轻松地实现 WebSocket 服务端的功能。与传统的 HTTP 通信不同,WebSocket 是基于长连接的方式,可以在一个连接上进行持续的消息传输,而不需要每次都重新建立连接。
在本节中,我们将使用 Spring Boot 作为内嵌容器,并详细讲解如何通过 J2EE 的 WebSocket API 实现服务端的 WebSocket 功能。我们将逐步实现以下几个核心功能:
- 依赖配置:添加必要的 WebSocket 依赖,使项目能够支持 WebSocket 通信。
- WebSocket 配置类:通过配置类启用 WebSocket 支持,确保 Spring Boot 能够识别 WebSocket 端点。
- WebSocket 服务端实现:利用
@ServerEndpoint
注解定义 WebSocket 端点,处理连接建立、消息接收、连接关闭以及服务端主动推送消息的逻辑。 - 会话管理:通过
Session
对象管理客户端的连接和消息传输。 - 通信接口:介绍如何通过
Session
对象进行消息发送和状态管理。
# 1. 添加 WebSocket 依赖
在 Spring Boot 项目中,首先需要添加 WebSocket 相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2
3
4
# 2. WebSocket 配置类
配置类是 Spring Boot 项目中必不可少的一部分。我们需要通过配置类来启用 WebSocket 功能,并扫描 WebSocket 端点的注解 @ServerEndpoint
。
package com.example.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket 配置类,用于启用 WebSocket 支持。
*
* 主要通过 ServerEndpointExporter 使得 Spring Boot 能扫描并注册 @ServerEndpoint 注解。
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig {
/**
* 注册 ServerEndpointExporter 以扫描 @ServerEndpoint 注解。
* 注意:如果使用外部独立的 Web 容器(如 Tomcat),则不需要此配置。
*
* @return ServerEndpointExporter 对象
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
说明:
ServerEndpointExporter
:用于扫描项目中所有标注了@ServerEndpoint
注解的类,并将其注册为 WebSocket 端点。这个类是 Spring Boot 内嵌容器时必须的,但如果使用外部 Tomcat 等容器,则不需要此配置。@EnableWebSocket
:开启 WebSocket 的支持,使项目可以处理 WebSocket 连接。
# 3. WebSocket 服务端实现
通过 @ServerEndpoint
注解,我们可以定义 WebSocket 的端点路径,指定客户端可以连接的 WebSocket 服务地址。在这里,我们将实现一个简单的聊天服务端,能够处理客户端的连接、消息传输以及连接关闭等事件。
package com.example.websocket.ws;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket 服务端实现类。
*
* 使用 @ServerEndpoint 注解定义 WebSocket 访问路径,并提供连接、消息处理、断开等功能。
*/
@ServerEndpoint("/ws/chat")
@Component
public class WebSocketServer {
// 使用 ConcurrentHashMap 来存储所有客户端连接的会话,确保线程安全
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 当 WebSocket 连接建立成功时触发。
*
* @param session 当前会话的 Session 对象,代表一个客户端连接
*/
@OnOpen
public void onOpen(Session session) {
// 将新连接加入会话管理中
clients.put(session.getId(), session);
System.out.println("连接成功,Session ID: " + session.getId());
try {
// 发送欢迎消息给客户端
session.getBasicRemote().sendText("连接成功,欢迎使用 WebSocket 服务!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 当 WebSocket 连接关闭时触发。
*
* @param session 当前会话的 Session 对象
*/
@OnClose
public void onClose(Session session) {
// 从会话管理中移除关闭的连接
clients.remove(session.getId());
System.out.println("连接关闭,Session ID: " + session.getId());
}
/**
* 当服务器收到客户端消息时触发。
*
* @param message 客户端发送的消息
* @param session 当前会话的 Session 对象
* @return 服务器返回的响应消息(可选)
*/
@OnMessage
public String onMessage(String message, Session session) {
System.out.println("收到消息:" + message + ",来自 Session ID: " + session.getId());
// 返回消息给客户端
return "服务器回复: " + message;
}
/**
* 服务器主动推送消息给指定客户端。
*
* @param sessionId 客户端的 Session ID
* @param message 要发送的消息内容
*/
public void sendMessageToClient(String sessionId, String message) {
Session session = clients.get(sessionId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.println("消息发送失败,Session ID 无效或连接已关闭: " + sessionId);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 4. 关键注解与 API 解析
@ServerEndpoint("/ws/chat")
:
- 定义 WebSocket 端点的访问路径。此路径为
/ws/chat
,客户端可通过ws://localhost:8080/ws/chat
进行连接。
@OnOpen
:
- 当客户端成功建立 WebSocket 连接时触发,常用于初始化连接或发送欢迎消息。
Session
对象:代表与客户端的连接,包含连接的状态、通信方法等。每个客户端的连接对应一个唯一的Session
实例。
@OnClose
:
- 当 WebSocket 连接关闭时触发,用于清理资源、移除会话。
@OnMessage
:
- 当收到客户端消息时触发,支持同步消息处理并直接返回响应。
# 5. 用户状态存储(Session 管理)
在 WebSocket 中,每个客户端连接都有一个 Session
实例,代表与客户端的通信会话。为了管理多个客户端的连接,可以将这些 Session
存储在一个 ConcurrentHashMap
中,以确保在多线程环境下的安全性。
// 存储所有客户端连接的会话
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
2
- 线程安全的存储:使用
ConcurrentHashMap
来存储所有客户端的Session
,保证了在并发环境下的正确性。 - 会话管理:当新客户端连接时,将其
Session
存储;当连接断开时,移除其Session
。
# 6. 客户端通信接口(Session 通信)
在 WebSocket 中,Session
是服务器端与每个客户端连接的核心接口。Session
提供了多种方法来实现实时通信,并管理连接的生命周期。以下是核心功能解析与关键方法总结。
# 1. Session 的作用
- 客户端连接的标识:每个客户端连接都有一个唯一的
Session
实例,用于标识和管理该连接。 - 通信接口:提供了发送消息、接收消息和管理连接状态的接口。
- 连接管理:开发者可以通过
Session
管理连接的状态、主动关闭连接、获取连接信息等。
# 2. Session 提供的关键方法
在 WebSocket 通信中,以下方法是最常用且直接影响通信效果的:
发送消息:
session.getBasicRemote().sendText(String message)
- 这是最常用的操作,用于向客户端发送文本消息。
getBasicRemote()
返回RemoteEndpoint.Basic
对象,支持同步消息发送。 - 使用场景:同步向客户端推送即时反馈,例如系统通知、用户操作结果。
- 注意:同步操作会阻塞当前线程,适合轻量级消息推送。在高并发场景中,建议使用异步方式(
getAsyncRemote()
)。
- 这是最常用的操作,用于向客户端发送文本消息。
检查连接状态:
session.isOpen()
- 用于判断连接是否仍然有效,避免向已关闭的连接发送消息。
- 使用场景:在发送消息前进行状态校验,确保只向有效连接发送数据。
- 注意:状态检查能避免由于无效连接导致的异常抛出。
关闭连接:
session.close()
- 手动关闭 WebSocket 连接,常用于资源清理或用户退出。
- 使用场景:当用户主动断开连接、长时间无响应时释放资源。
- 注意:调用
close()
后,连接立即关闭,需在关闭前处理必要的资源清理。
# 3. 其他重要方法与属性
获取连接信息:
getRequestURI()
和getId()
session.getRequestURI()
:获取连接的请求 URI,用于区分不同的连接路径。session.getId()
:获取连接的唯一标识符,便于多连接管理。
发送二进制数据与异步消息
session.getBasicRemote().sendBinary(ByteBuffer data)
:同步发送二进制数据,如文件或图像。session.getAsyncRemote().sendText(String message)
:异步发送文本消息,适合高并发场景。
# 7. 服务器主动推送消息
在实际应用中,服务器可以在任何时候主动向客户端推送消息。以下代码展示了如何根据指定的 Session ID
向客户端发送消息:
public void sendMessageToClient(String sessionId, String message) {
Session session = clients.get(sessionId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.println("消息发送失败,Session ID 无效或连接已关闭: " + sessionId);
}
}
2
3
4
5
6
7
8
9
10
11
12
说明:
- 通过
session.getBasicRemote().sendText(message)
向客户端发送同步文本消息。 - 先检查连接是否仍然打开(
session.isOpen()
),避免发送失败。
# 8. 其他注意事项
- 会话超时处理:可以通过 WebSocket 配置或在代码中定期检查连接状态,确保清理失效的会话。
- 并发与线程安全:在高并发环境下,建议使用
ConcurrentHashMap
等线程安全的数据结构管理Session
。 - 心跳机制:为了维持长时间的连接稳定,可以在客户端和服务器之间定期发送心跳包(PING/PONG 消息)。
# 9. 测试与运行
在测试环境中,可以使用浏览器开发者工具或第三方 WebSocket 客户端工具进行连接测试。
WebSocket测试工具:http://devtest.run/websocket.html
客户端连接示例代码:
<template>
<el-container class="chat-container">
<el-header class="chat-header">
<el-avatar :size="50" icon="el-icon-chat-dot-round"></el-avatar>
<h2 style="display: inline-block; margin-left: 10px;">在线聊天</h2>
</el-header>
<el-main class="chat-content">
<div class="chat-messages" ref="chatMessages">
<div
v-for="(message, index) in messages"
:key="index"
:class="{'sent': message.type === 'sent', 'received': message.type === 'received'}"
class="message-wrapper"
>
<!-- 如果是接收到的消息,显示对方头像 -->
<el-avatar
v-if="message.type === 'received'"
:src="message.avatar"
class="message-avatar"
></el-avatar>
<!-- 消息气泡 -->
<el-card :body-style="{ padding: '10px' }" class="message-card" shadow="always">
<div class="message-content">{{ message.text }}</div>
</el-card>
<!-- 如果是发送的消息,显示用户的默认头像 -->
<el-avatar
v-if="message.type === 'sent'"
icon="el-icon-user-solid"
class="message-avatar"
></el-avatar>
</div>
</div>
</el-main>
<el-footer class="chat-input">
<el-input
v-model="inputMessage"
placeholder="输入消息..."
@keydown.enter.native="sendMessage"
clearable
size="large"
class="input-field"
/>
<el-button type="primary" @click="sendMessage" size="large">发送</el-button>
</el-footer>
</el-container>
</template>
<script>
export default {
data() {
return {
inputMessage: "", // 输入的消息内容
messages: [
// 示例消息
{ text: "您好,有什么我可以帮助的吗?", type: "received", avatar: "https://i.pravatar.cc/50" }
], // 消息列表
socket: null, // WebSocket 实例
};
},
mounted() {
this.connectWebSocket();
},
methods: {
connectWebSocket() {
const token = "yourToken"; // 替换为实际的 token
const url = `ws://localhost:3000/ws/chat?token=${token}`;
this.socket = new WebSocket(url);
this.socket.onopen = () => {
this.$message.success("WebSocket 连接成功");
};
this.socket.onmessage = (event) => {
this.messages.push({ text: event.data, type: "received", avatar: "https://i.pravatar.cc/50" });
this.scrollToBottom();
};
this.socket.onclose = () => {
this.$message.error("WebSocket 连接已关闭");
};
this.socket.onerror = (error) => {
this.$message.error("WebSocket 发生错误:" + error.message);
};
},
sendMessage() {
if (this.inputMessage && this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(this.inputMessage);
this.messages.push({ text: this.inputMessage, type: "sent" });
this.inputMessage = "";
this.scrollToBottom();
} else {
this.$message.warning("WebSocket 连接未打开或消息为空");
}
},
scrollToBottom() {
this.$nextTick(() => {
const chatMessages = this.$refs.chatMessages;
chatMessages.scrollTop = chatMessages.scrollHeight;
});
}
}
};
</script>
<style scoped>
.chat-container {
width: 600px;
margin: 20px auto;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
background-color: #fff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
display: flex;
align-items: center;
padding: 10px;
background-color: #409eff;
color: #fff;
}
.chat-content {
padding: 20px;
height: 400px;
overflow-y: auto;
background-color: #f5f5f5;
}
.chat-messages {
display: flex;
flex-direction: column;
}
.message-wrapper {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.sent {
justify-content: flex-end;
}
.received {
justify-content: flex-start;
}
.message-card {
padding: 5px 15px;
max-width: 75%;
word-wrap: break-word;
}
.sent .message-card {
background-color: #409eff;
color: white;
}
.received .message-card {
background-color: #d3dce6;
}
.message-avatar {
margin: 0 10px;
}
.chat-input {
display: flex;
padding: 10px 20px;
background-color: #f5f5f5;
border-top: 1px solid #ebeef5;
}
.input-field {
flex: 1;
margin-right: 10px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183