Java 服务端实现
Java 提供了标准的 WebSocket API(JSR 356 / Jakarta WebSocket),支持创建企业级 WebSocket 应用。本章介绍如何使用 Jakarta WebSocket 和 Spring Boot 实现 WebSocket 服务端。
Jakarta WebSocket API
Jakarta WebSocket 是 Java 平台的标准 WebSocket API,定义在 JSR 356 规范中。从 Jakarta EE 9 开始,包名从 javax.websocket 改为 jakarta.websocket。
依赖配置
Maven 依赖(Jakarta EE 9+):
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.1.0</version>
<scope>provided</scope>
</dependency>
Maven 依赖(Java EE 7/8):
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
核心包结构
Jakarta WebSocket 包含两个主要包:
jakarta.websocket:客户端和服务端通用的注解、类和接口jakarta.websocket.server:创建和配置服务端端点的注解、类和接口
注解式端点
注解式端点是创建 WebSocket 服务最简单的方式,通过在类和方法上添加注解来定义端点行为。
基本端点
import jakarta.websocket.OnMessage;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.Session;
import java.io.IOException;
@ServerEndpoint("/echo")
public class EchoEndpoint {
@OnMessage
public void onMessage(Session session, String message) throws IOException {
// 接收消息并回显
session.getBasicRemote().sendText("Echo: " + message);
}
}
@ServerEndpoint 注解指定端点的 URL 路径,@OnMessage 注解标记处理消息的方法。
生命周期注解
WebSocket 端点有四个生命周期事件,对应四个注解:
| 注解 | 事件 | 说明 |
|---|---|---|
@OnOpen | 连接打开 | 客户端连接建立时调用 |
@OnMessage | 消息接收 | 收到客户端消息时调用 |
@OnClose | 连接关闭 | 连接关闭时调用 |
@OnError | 错误发生 | 发生错误时调用 |
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint("/chat")
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
System.out.println("客户端连接: " + session.getId());
// 获取握手请求信息
String origin = (String) config.getUserProperties()
.get("jakarta.websocket.endpoint.http.request.origin");
System.out.println("Origin: " + origin);
}
@OnMessage
public void onMessage(Session session, String message) throws IOException {
System.out.println("收到消息: " + message);
session.getBasicRemote().sendText("收到: " + message);
}
@OnClose
public void onClose(Session session, CloseReason reason) {
System.out.println("连接关闭: " + session.getId());
System.out.println("关闭码: " + reason.getCloseCode().getCode());
System.out.println("关闭原因: " + reason.getReasonPhrase());
}
@OnError
public void onError(Session session, Throwable throwable) {
System.err.println("发生错误: " + throwable.getMessage());
}
}
发送消息
通过 Session 对象的 getBasicRemote() 或 getAsyncRemote() 方法获取远程端点,然后发送消息:
同步发送:
@OnMessage
public void onMessage(Session session, String message) throws IOException {
// 获取基本远程端点(同步)
RemoteEndpoint.Basic remote = session.getBasicRemote();
// 发送文本消息
remote.sendText("文本消息");
// 发送二进制消息
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{1, 2, 3, 4});
remote.sendBinary(buffer);
// 发送 Ping
remote.sendPing(ByteBuffer.wrap(new byte[]{1, 2, 3}));
// 发送 Pong
remote.sendPong(ByteBuffer.wrap(new byte[]{1, 2, 3}));
}
异步发送:
@OnMessage
public void onMessage(Session session, String message) {
// 获取异步远程端点
RemoteEndpoint.Async asyncRemote = session.getAsyncRemote();
// 异步发送文本
asyncRemote.sendText("异步消息");
// 异步发送二进制
asyncRemote.sendBinary(ByteBuffer.wrap(new byte[]{1, 2, 3}));
// 带回调的异步发送
asyncRemote.sendText("消息", new SendHandler() {
@Override
public void onResult(SendResult result) {
if (result.isOK()) {
System.out.println("发送成功");
} else {
System.err.println("发送失败: " + result.getException());
}
}
});
}
接收不同类型消息
一个端点可以定义多个 @OnMessage 方法处理不同类型的消息:
@ServerEndpoint("/messages")
public class MessageTypeEndpoint {
// 处理文本消息
@OnMessage
public void onTextMessage(Session session, String message) {
System.out.println("文本消息: " + message);
}
// 处理二进制消息
@OnMessage
public void onBinaryMessage(Session session, ByteBuffer buffer) {
System.out.println("二进制消息: " + buffer.remaining() + " 字节");
}
// 处理 Pong 消息
@OnMessage
public void onPongMessage(Session session, PongMessage pong) {
System.out.println("Pong 消息");
}
}
每个端点每种消息类型最多只能有一个处理方法。
编程式端点
编程式端点通过继承 Endpoint 类实现,提供更细粒度的控制。
基本实现
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpointConfig;
import java.io.IOException;
import java.nio.ByteBuffer;
public class ProgrammaticEndpoint extends Endpoint {
@Override
public void onOpen(Session session, EndpointConfig config) {
System.out.println("连接打开: " + session.getId());
// 添加消息处理器
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
System.out.println("收到文本消息: " + message);
try {
session.getBasicRemote().sendText("Echo: " + message);
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 添加二进制消息处理器
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
System.out.println("收到二进制消息: " + message.remaining() + " 字节");
}
});
}
@Override
public void onClose(Session session, CloseReason closeReason) {
System.out.println("连接关闭: " + closeReason);
}
@Override
public void onError(Session session, Throwable thr) {
System.err.println("发生错误: " + thr.getMessage());
}
}
部署编程式端点
编程式端点需要通过 ServerEndpointConfig 配置部署:
import jakarta.websocket.server.ServerEndpointConfig;
import jakarta.websocket.Endpoint;
// 创建配置
ServerEndpointConfig config = ServerEndpointConfig.Builder
.create(ProgrammaticEndpoint.class, "/programmatic")
.build();
在 Servlet 容器中,通常会通过 ServerApplicationConfig 接口自动部署:
import jakarta.websocket.server.ServerApplicationConfig;
import jakarta.websocket.server.ServerEndpointConfig;
import java.util.HashSet;
import java.util.Set;
public class WebSocketConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
Set<ServerEndpointConfig> configs = new HashSet<>();
for (Class<? extends Endpoint> endpointClass : endpointClasses) {
if (endpointClass == ProgrammaticEndpoint.class) {
configs.add(ServerEndpointConfig.Builder
.create(endpointClass, "/programmatic")
.build());
}
}
return configs;
}
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
// 返回需要部署的注解端点类
Set<Class<?>> endpoints = new HashSet<>();
for (Class<?> clazz : scanned) {
if (clazz.getPackage().getName().equals("com.example.endpoints")) {
endpoints.add(clazz);
}
}
return endpoints;
}
}
Session 管理
Session 对象
Session 对象代表客户端与服务端之间的 WebSocket 会话:
@ServerEndpoint("/session")
public class SessionEndpoint {
@OnOpen
public void onOpen(Session session) {
// 获取会话 ID
String id = session.getId();
// 检查是否开放
boolean isOpen = session.isOpen();
// 获取最大空闲超时(毫秒)
long timeout = session.getMaxIdleTimeout();
// 设置超时
session.setMaxIdleTimeout(60000L);
// 获取最大消息缓冲区大小
int maxBufferSize = session.getMaxTextMessageBufferSize();
// 设置缓冲区大小
session.setMaxTextMessageBufferSize(65536);
}
}
存储用户数据
Session 提供了用户属性映射,用于存储会话相关数据:
@ServerEndpoint("/user")
public class UserEndpoint {
@OnOpen
public void onOpen(Session session) {
// 存储用户数据
session.getUserProperties().put("username", "匿名用户");
session.getUserProperties().put("joinTime", System.currentTimeMillis());
}
@OnMessage
public void onMessage(Session session, String message) {
// 获取用户数据
String username = (String) session.getUserProperties().get("username");
Long joinTime = (Long) session.getUserProperties().get("joinTime");
System.out.println(username + " 说: " + message);
System.out.println("已在线: " + (System.currentTimeMillis() - joinTime) / 1000 + " 秒");
// 更新用户名
if (message.startsWith("/name ")) {
String newName = message.substring(6);
session.getUserProperties().put("username", newName);
sendText(session, "用户名已更新为: " + newName);
}
}
private void sendText(Session session, String text) {
try {
session.getBasicRemote().sendText(text);
} catch (IOException e) {
e.printStackTrace();
}
}
}
广播消息
获取所有打开的会话并广播消息:
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Set;
@ServerEndpoint("/broadcast")
public class BroadcastEndpoint {
// 存储所有连接(线程安全)
private static final Set<Session> sessions =
java.util.Collections.newSetFromMap(
new java.util.concurrent.ConcurrentHashMap<>()
);
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
broadcast("用户加入,当前在线: " + sessions.size());
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
broadcast("用户离开,当前在线: " + sessions.size());
}
@OnMessage
public void onMessage(Session session, String message) {
broadcast(session.getUserProperties().get("username") + ": " + message);
}
private void broadcast(String message) {
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
也可以通过 Session.getOpenSessions() 获取当前端点的所有打开会话:
@OnMessage
public void onMessage(Session session, String message) {
// 获取当前端点的所有打开会话
Set<Session> openSessions = session.getOpenSessions();
for (Session s : openSessions) {
if (s.isOpen() && s != session) {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
路径参数
WebSocket 端点支持 URI 模板参数,用于从 URL 中提取动态值。
基本用法
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.*;
@ServerEndpoint("/chat/{room}")
public class RoomEndpoint {
@OnOpen
public void onOpen(Session session, @PathParam("room") String room) {
System.out.println("加入房间: " + room);
session.getUserProperties().put("room", room);
}
@OnMessage
public void onMessage(Session session, String message,
@PathParam("room") String room) {
System.out.println("房间 " + room + " 收到消息: " + message);
// 只广播给同一房间的用户
for (Session s : session.getOpenSessions()) {
String sRoom = (String) s.getUserProperties().get("room");
if (room.equals(sRoom) && s.isOpen()) {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
多个路径参数
@ServerEndpoint("/chat/{room}/{username}")
public class MultiParamEndpoint {
@OnOpen
public void onOpen(Session session,
@PathParam("room") String room,
@PathParam("username") String username) {
System.out.println(username + " 加入房间 " + room);
}
@OnMessage
public void onMessage(Session session, String message,
@PathParam("room") String room,
@PathParam("username") String username) {
String formatted = String.format("[%s] %s: %s", room, username, message);
for (Session s : session.getOpenSessions()) {
// 广播给同房间用户
try {
s.getBasicRemote().sendText(formatted);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
编码器和解码器
Jakarta WebSocket 提供了编码器和解码器机制,用于在 Java 对象和 WebSocket 消息之间转换。
编码器(Encoder)
编码器将 Java 对象转换为 WebSocket 消息格式:
import jakarta.websocket.Encoder;
import jakarta.websocket.EndpointConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
// 文本编码器
public class MessageEncoder implements Encoder.Text<Message> {
private ObjectMapper mapper;
@Override
public void init(EndpointConfig config) {
mapper = new ObjectMapper();
}
@Override
public void destroy() {
// 清理资源
}
@Override
public String encode(Message message) throws EncodeException {
try {
return mapper.writeValueAsString(message);
} catch (Exception e) {
throw new EncodeException(message, "编码失败", e);
}
}
}
// 二进制编码器
public class MessageBinaryEncoder implements Encoder.Binary<Message> {
@Override
public ByteBuffer encode(Message message) throws EncodeException {
// 将对象编码为 ByteBuffer
String json = new ObjectMapper().writeValueAsString(message);
return ByteBuffer.wrap(json.getBytes(StandardCharsets.UTF_8));
}
@Override
public void init(EndpointConfig config) {}
@Override
public void destroy() {}
}
解码器(Decoder)
解码器将 WebSocket 消息转换为 Java 对象:
import jakarta.websocket.Decoder;
import jakarta.websocket.EndpointConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
public class MessageDecoder implements Decoder.Text<Message> {
private ObjectMapper mapper;
@Override
public void init(EndpointConfig config) {
mapper = new ObjectMapper();
}
@Override
public void destroy() {
// 清理资源
}
@Override
public Message decode(String s) throws DecodeException {
try {
return mapper.readValue(s, Message.class);
} catch (Exception e) {
throw new DecodeException(s, "解码失败", e);
}
}
@Override
public boolean willDecode(String s) {
// 检查是否可以解码
try {
mapper.readTree(s);
return true;
} catch (Exception e) {
return false;
}
}
}
使用编解码器
在 @ServerEndpoint 注解中指定编解码器:
@ServerEndpoint(
value = "/message",
encoders = {MessageEncoder.class},
decoders = {MessageDecoder.class}
)
public class MessageEndpoint {
@OnMessage
public void onMessage(Session session, Message message) {
// message 已经是解码后的对象
System.out.println("收到消息: " + message.getContent());
try {
// 发送对象(自动编码)
Message response = new Message("server", "收到: " + message.getContent());
session.getBasicRemote().sendObject(response);
} catch (IOException | EncodeException e) {
e.printStackTrace();
}
}
}
消息类定义
public class Message {
private String from;
private String content;
private long timestamp;
public Message() {
this.timestamp = System.currentTimeMillis();
}
public Message(String from, String content) {
this();
this.from = from;
this.content = content;
}
// Getter 和 Setter
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
自定义配置器
通过继承 ServerEndpointConfig.Configurator 类可以自定义端点配置。
访问 HTTP 请求信息
import jakarta.websocket.server.ServerEndpointConfig;
import jakarta.websocket.server.HandshakeRequest;
import jakarta.websocket.HandshakeResponse;
import java.util.List;
import java.util.Map;
public class CustomConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request,
HandshakeResponse response) {
// 获取 HTTP 请求头
Map<String, List<String>> headers = request.getHeaders();
// 获取查询参数
String queryString = request.getRequestURI().getQuery();
// 获取 Cookie
Map<String, List<String>> cookies = request.getHeaders().get("Cookie");
// 将请求信息存储到用户属性中
sec.getUserProperties().put("httpRequest", request);
sec.getUserProperties().put("headers", headers);
// 修改响应头
response.getHeaders().put("X-Custom-Header",
List.of("Custom Value"));
}
}
Origin 验证
public class OriginConfigurator extends ServerEndpointConfig.Configurator {
private static final List<String> ALLOWED_ORIGINS = List.of(
"http://localhost:3000",
"https://example.com"
);
@Override
public boolean checkOrigin(String originHeaderValue) {
if (originHeaderValue == null) {
return false;
}
return ALLOWED_ORIGINS.contains(originHeaderValue);
}
}
使用配置器
@ServerEndpoint(
value = "/secure",
configurator = CustomConfigurator.class
)
public class SecureEndpoint {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 获取存储的请求信息
HandshakeRequest request = (HandshakeRequest)
config.getUserProperties().get("httpRequest");
Map<String, List<String>> headers = request.getHeaders();
System.out.println("Origin: " + headers.get("Origin"));
}
}
Spring Boot 集成
Spring Boot 提供了更简洁的 WebSocket 支持,可以与 Spring 生态系统无缝集成。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket 配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler(), "/ws/chat")
.setAllowedOrigins("*");
}
}
处理器实现
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatHandler extends TextWebSocketHandler {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
System.out.println("连接建立: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
System.out.println("收到消息: " + payload);
// 广播给所有连接
for (WebSocketSession s : sessions) {
if (s.isOpen()) {
s.sendMessage(new TextMessage(payload));
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
System.out.println("连接关闭: " + session.getId());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.err.println("传输错误: " + exception.getMessage());
if (session.isOpen()) {
session.close();
}
}
}
使用 STOMP 协议
Spring 支持 STOMP 子协议,提供更丰富的消息模式:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单的内存消息代理
config.enableSimpleBroker("/topic");
// 客户端发送消息的前缀
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
消息处理控制器
import org.springframework.messaging.handler.annotation.*;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/messages")
public ChatMessage send(ChatMessage message) {
return new ChatMessage(message.getFrom(), message.getContent());
}
@MessageMapping("/chat/{room}")
@SendTo("/topic/room/{room}")
public ChatMessage sendToRoom(@DestinationVariable String room,
ChatMessage message) {
return new ChatMessage(message.getFrom(),
"[" + room + "] " + message.getContent());
}
}
消息类
public class ChatMessage {
private String from;
private String content;
public ChatMessage() {}
public ChatMessage(String from, String content) {
this.from = from;
this.content = content;
}
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
前端连接 STOMP
import SockJS from 'sockjs-client';
import { Stomp } from '@stomp/stompjs';
// 连接到 STOMP 端点
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
console.log('已连接:', frame);
// 订阅主题
stompClient.subscribe('/topic/messages', (message) => {
const body = JSON.parse(message.body);
console.log(`${body.from}: ${body.content}`);
});
// 发送消息
stompClient.send('/app/chat', {}, JSON.stringify({
from: '张三',
content: '大家好'
}));
});
完整聊天室示例
服务端代码
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/chatroom/{username}")
public class ChatRoomEndpoint {
// 存储所有房间和会话
private static final Map<String, Set<Session>> rooms = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
// 从查询参数获取房间名
String room = getQueryParam(session, "room", "general");
// 存储用户信息
session.getUserProperties().put("username", username);
session.getUserProperties().put("room", room);
// 添加到房间
rooms.computeIfAbsent(room, k -> ConcurrentHashMap.newKeySet())
.add(session);
// 广播加入消息
broadcast(room, String.format("系统: %s 加入了聊天室", username));
// 发送欢迎消息
sendTo(session, "系统: 欢迎 " + username + ",当前在线 " +
rooms.get(room).size() + " 人");
}
@OnMessage
public void onMessage(Session session, String message) {
String username = (String) session.getUserProperties().get("username");
String room = (String) session.getUserProperties().get("room");
// 广播消息
broadcast(room, String.format("%s: %s", username, message));
}
@OnClose
public void onClose(Session session) {
String username = (String) session.getUserProperties().get("username");
String room = (String) session.getUserProperties().get("room");
// 从房间移除
Set<Session> roomSessions = rooms.get(room);
if (roomSessions != null) {
roomSessions.remove(session);
if (roomSessions.isEmpty()) {
rooms.remove(room);
}
}
// 广播离开消息
broadcast(room, String.format("系统: %s 离开了聊天室", username));
}
@OnError
public void onError(Session session, Throwable error) {
System.err.println("WebSocket 错误: " + error.getMessage());
}
private void broadcast(String room, String message) {
Set<Session> roomSessions = rooms.get(room);
if (roomSessions == null) return;
for (Session session : roomSessions) {
sendTo(session, message);
}
}
private void sendTo(Session session, String message) {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String getQueryParam(Session session, String name, String defaultValue) {
String query = session.getQueryString();
if (query == null) return defaultValue;
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2 && pair[0].equals(name)) {
return pair[1];
}
}
return defaultValue;
}
}
前端代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#messages {
height: 400px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin: 5px 0;
padding: 5px;
}
.system {
color: #666;
font-style: italic;
}
.mine {
background-color: #e8f5e9;
margin-left: 20%;
}
.others {
background-color: #f5f5f5;
margin-right: 20%;
}
#input {
width: 70%;
padding: 10px;
}
button {
padding: 10px 20px;
}
</style>
</head>
<body>
<h1>聊天室</h1>
<div id="messages"></div>
<input type="text" id="input" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
<script>
const username = prompt('请输入用户名:') || '匿名';
const room = new URLSearchParams(window.location.search).get('room') || 'general';
const ws = new WebSocket(`ws://${location.host}/chatroom/${username}?room=${room}`);
const messagesDiv = document.getElementById('messages');
const input = document.getElementById('input');
ws.onopen = () => {
console.log('已连接');
};
ws.onmessage = (event) => {
const message = event.data;
const div = document.createElement('div');
div.className = 'message';
if (message.startsWith('系统:')) {
div.classList.add('system');
div.textContent = message;
} else {
const colonIndex = message.indexOf(':');
const sender = message.substring(0, colonIndex);
const content = message.substring(colonIndex + 2);
div.classList.add(sender === username ? 'mine' : 'others');
div.innerHTML = `<strong>${sender}</strong>: ${content}`;
}
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};
ws.onclose = () => {
console.log('连接关闭');
};
ws.onerror = (error) => {
console.error('连接错误:', error);
};
function sendMessage() {
const content = input.value.trim();
if (content && ws.readyState === WebSocket.OPEN) {
ws.send(content);
input.value = '';
}
}
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
小结
本章介绍了 Java WebSocket 服务端的多种实现方式:
-
Jakarta WebSocket API:Java 标准 WebSocket API
- 注解式端点:使用
@ServerEndpoint等注解,简单易用 - 编程式端点:继承
Endpoint类,提供更多控制
- 注解式端点:使用
-
核心概念:
- Session 管理:连接状态、用户数据存储
- 路径参数:从 URL 中提取动态值
- 编码器和解码器:对象与消息的转换
-
Spring Boot 集成:
- 基本 WebSocket 处理器
- STOMP 协议支持
选择建议:
- 标准 Java EE/Jakarta EE 项目:使用 Jakarta WebSocket API
- Spring Boot 项目:使用 Spring WebSocket 支持
- 需要复杂消息模式:使用 STOMP 子协议