在实时性要求较高的应用场景,如在线聊天、实时数据监控、股票行情推送等,传统的HTTP协议由于其请求-响应的模式,无法高效实现服务器与客户端之间的双向实时通信。而WebSocket协议的出现解决了这一难题,它允许在单个TCP连接上进行全双工通信,使得服务器和客户端可以随时主动发送消息。Spring Boot对WebSocket提供了良好的支持,极大地简化了开发流程。本文将从入门到精通,详细介绍Spring Boot中WebSocket的常用使用方法。
WebSocket是一种网络通信协议,于2011年被IETF定为标准RFC 6455,并被HTML5所支持 。与HTTP协议不同,WebSocket在建立连接后,通信双方可以随时主动发送和接收数据,无需像HTTP那样每次通信都要建立新的连接,从而减少了开销,提高了实时性。
特性 | HTTP | WebSocket |
---|---|---|
通信模式 | 客户端发起请求,服务器响应(单向) | 全双工通信(双向) |
连接方式 | 每次请求都需建立新连接 | 一次握手建立持久连接 |
数据格式 | 通常为文本(JSON、XML等) | 支持文本和二进制数据 |
应用场景 | 适用于一般的Web页面请求 | 适用于实时性要求高的场景 |
在Spring Boot项目的pom.xml
文件中添加WebSocket依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
如果使用Gradle,在build.gradle
中添加:
implementation 'org.springframework.boot:spring-boot-starter-websocket'
创建一个配置类,用于注册WebSocket处理程序和配置消息代理:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(StompBrokerRelayRegistration config) {
config.setApplicationDestinationPrefixes("/app");
config.setDestinationPrefixes("/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket-endpoint").withSockJS();
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(65536);
container.setMaxBinaryMessageBufferSize(65536);
return container;
}
}
在上述代码中:
@EnableWebSocketMessageBroker
注解启用WebSocket消息代理。configureMessageBroker
方法配置消息代理的前缀,/app
用于应用程序发送消息的目的地前缀,/topic
用于服务器发送消息的目的地前缀。registerStompEndpoints
方法注册WebSocket端点,addEndpoint
方法指定端点的路径,withSockJS
表示启用SockJS支持,以提供对不支持WebSocket浏览器的兼容。createWebSocketContainer
方法配置WebSocket容器的参数,如消息缓冲区大小。public class ChatMessage {
private String sender;
private String content;
private MessageType type;
// 省略构造函数、Getter和Setter方法
public enum MessageType {
CHAT, JOIN, LEAVE
}
}
ChatMessage
类用于封装聊天消息,包含发送者、消息内容和消息类型(聊天、加入、离开)。
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(ChatMessage chatMessage) {
return chatMessage;
}
@MessageMapping("/chat.join")
@SendTo("/topic/public")
public ChatMessage joinChat(ChatMessage chatMessage) {
chatMessage.setType(ChatMessage.MessageType.JOIN);
return chatMessage;
}
}
在上述代码中:
@MessageMapping
注解用于映射客户端发送的消息路径,如"/chat.send"
和"/chat.join"
。@SendTo
注解指定消息发送的目的地,这里将消息发送到"/topic/public"
,所有订阅该主题的客户端都能接收到消息。sendMessage
方法处理聊天消息的发送,joinChat
方法处理用户加入聊天的消息。DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebSocket Chattitle>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js">script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stompjs/2.3.3/stomp.min.js">script>
head>
<body>
<input type="text" id="username" placeholder="用户名">
<button onclick="connect()">连接button>
<div id="chat-window">div>
<input type="text" id="message" placeholder="输入消息">
<button onclick="sendMessage()">发送button>
<script>
let socket = new SockJS('/websocket-endpoint');
let stompClient = Stomp.over(socket);
function connect() {
let username = document.getElementById('username').value;
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/public', function (message) {
let chatWindow = document.getElementById('chat-window');
let msg = JSON.parse(message.body);
if (msg.type === 'JOIN') {
chatWindow.innerHTML += msg.sender + " 加入了聊天
";
} else {
chatWindow.innerHTML += msg.sender + ": " + msg.content + "
";
}
});
let joinMessage = {
sender: username,
content: '',
type: 'JOIN'
};
stompClient.send("/app/chat.join", {}, JSON.stringify(joinMessage));
});
}
function sendMessage() {
let message = document.getElementById('message').value;
let username = document.getElementById('username').value;
let chatMessage = {
sender: username,
content: message,
type: 'CHAT'
};
stompClient.send("/app/chat.send", {}, JSON.stringify(chatMessage));
}
script>
body>
html>
前端页面通过SockJS和StompJS库与后端建立WebSocket连接,实现消息的发送和接收。
有时候需要实现一对一的消息发送,而不是广播给所有客户端。可以通过在@SendTo
中指定具体的用户目的地来实现。
在WebSocketConfig
类中添加用户目的地前缀配置:
@Override
public void configureMessageBroker(StompBrokerRelayRegistration config) {
config.setApplicationDestinationPrefixes("/app");
config.setDestinationPrefixes("/topic", "/user");
config.setUserDestinationPrefix("/user");
}
这里添加了/user
作为用户目的地前缀。
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
@Controller
public class PrivateChatController {
@MessageMapping("/chat.private")
public void sendPrivateMessage(SimpMessageHeaderAccessor headerAccessor, ChatMessage chatMessage) {
String recipient = chatMessage.getRecipient();
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
this.stompMessagingTemplate.convertAndSendToUser(recipient, "/private", chatMessage);
}
}
在上述代码中:
@MessageMapping("/chat.private")
映射处理点对点消息的路径。SimpMessageHeaderAccessor
用于获取和设置消息头信息。stompMessagingTemplate.convertAndSendToUser
方法将消息发送到指定用户的私有目的地。function sendPrivateMessage() {
let message = document.getElementById('message').value;
let username = document.getElementById('username').value;
let recipient = document.getElementById('recipient').value;
let chatMessage = {
sender: username,
recipient: recipient,
content: message,
type: 'CHAT'
};
stompClient.send("/app/chat.private", {}, JSON.stringify(chatMessage));
}
前端添加输入接收者的文本框,并在发送消息时指定接收者,实现点对点消息发送。
在实际应用中,可能需要对WebSocket消息进行拦截和认证,确保只有合法用户才能进行通信。
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
@Component
public class WebSocketInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 在这里进行认证逻辑,如检查Token等
String token = accessor.getFirstNativeHeader("Authorization");
if (token == null ||!isValidToken(token)) {
throw new RuntimeException("认证失败");
}
}
return message;
}
private boolean isValidToken(String token) {
// 实现具体的Token验证逻辑
return true;
}
}
上述代码创建了一个WebSocketInterceptor
拦截器,在preSend
方法中对连接请求进行认证,检查请求头中的Authorization
Token是否有效。
在WebSocketConfig
类中注册拦截器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new WebSocketInterceptor());
}
// 其他配置方法...
}
通过configureClientInboundChannel
方法将拦截器注册到客户端入站通道,对所有进入的消息进行拦截处理。
在Spring Boot中,除了通过实现接口的方式处理WebSocket消息,还可以利用注解来简化开发过程。通过@ServerEndpoint
注解定义WebSocket端点,结合@OnOpen
、@OnMessage
、@OnClose
、@OnError
等注解,能够轻松实现对WebSocket连接生命周期的监听,以及接收和处理客户端发送的数据。
首先,创建一个WebSocket处理类:
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws/{userId}")
public class MyWebSocket {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收userId
private String userId;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
webSocketSet.add(this);
addOnlineCount();
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
subOnlineCount();
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端" + userId + "的消息:" + message);
// 群发消息
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount--;
}
}
在上述代码中:
@ServerEndpoint("/ws/{userId}")
注解定义了WebSocket的访问端点,{userId}
为路径参数,用于标识不同的客户端。@OnOpen
注解的方法在连接建立时被调用,用于初始化连接相关信息,并将当前连接对象添加到在线连接集合中。@OnMessage
注解的方法在接收到客户端发送的消息时被调用,实现了消息的接收和群发功能。@OnClose
注解的方法在连接关闭时被调用,从在线连接集合中移除当前连接对象。@OnError
注解的方法在发生错误时被调用,用于处理异常情况。前端页面同样需要进行相应的修改,以连接基于注解实现的WebSocket端点:
DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>基于注解的WebSocket Chattitle>
head>
<body>
<input type="text" id="userId" placeholder="用户ID">
<button onclick="connect()">连接button>
<div id="chat-window">div>
<input type="text" id="message" placeholder="输入消息">
<button onclick="sendMessage()">发送button>
<script>
let socket;
function connect() {
let userId = document.getElementById('userId').value;
socket = new WebSocket("ws://localhost:8080/ws/" + userId);
socket.onopen = function (event) {
console.log("连接成功");
};
socket.onmessage = function (event) {
let chatWindow = document.getElementById('chat-window');
chatWindow.innerHTML += "收到消息: " + event.data + "
";
};
socket.onclose = function (event) {
console.log("连接关闭");
};
socket.onerror = function (event) {
console.log("连接错误");
};
}
function sendMessage() {
let message = document.getElementById('message').value;
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(message);
document.getElementById('chat-window').innerHTML += "发送消息: " + message + "
";
document.getElementById('message').value = "";
} else {
alert("WebSocket连接未建立或已关闭");
}
}
script>
body>
html>
还需要在Spring Boot中配置WebSocket支持,确保端点被正确注册:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
public class WebSocketConfig {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
这种基于注解的实现方式相比传统接口方式更加简洁直观,通过注解即可完成WebSocket连接的生命周期管理和消息处理。
在股票交易、天气监测等场景中,服务器需要实时向客户端推送数据。可以结合定时任务实现:
@Service
public class RealTimeDataService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Scheduled(fixedRate = 5000) // 每5秒执行一次
public void pushRealTimeData() {
// 获取实时数据
StockData stockData = getStockData();
// 推送给订阅了实时股票信息的客户端
messagingTemplate.convertAndSend("/topic/stock", stockData);
}
private StockData getStockData() {
// 模拟获取股票数据
return new StockData("000001", "平安银行", 15.68, 0.23);
}
}
多个用户可以同时编辑同一个文档,实时看到彼此的操作:
@MessageMapping("/edit")
@SendTo("/topic/document/{docId}")
public EditOperation handleEdit(@DestinationVariable String docId, EditOperation operation) {
// 处理编辑操作,更新文档
documentService.applyEdit(docId, operation);
return operation;
}
在在线游戏中,玩家的操作需要实时同步到其他玩家:
@MessageMapping("/game/{roomId}/move")
public void handleGameMove(@DestinationVariable String roomId, MoveAction action) {
// 更新游戏状态
gameService.updateGameState(roomId, action);
// 广播给房间内的所有玩家
messagingTemplate.convertAndSend("/topic/game/" + roomId, action);
}
registry.addEndpoint("/websocket-endpoint")
.setAllowedOrigins("*")
.withSockJS();
本文从WebSocket的基础概念出发,详细介绍了Spring Boot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消息收发、点对点消息发送、消息拦截与认证,以及不使用接口而是基于注解的WebSocket实现方式。同时,还拓展了WebSocket在不同场景下的应用,提供了性能优化建议和常见问题解决方案。
通过这些方法,开发者可以根据实际需求,灵活运用WebSocket在Spring Boot应用中实现高效的实时通信功能。在实际项目中,还可以结合更多的Spring Boot特性和业务逻辑,进一步扩展和优化WebSocket的应用,打造出更强大、更实用的实时应用程序。
以上补充内容完善了基于注解的WebSocket实现方案,并新增了应用场景拓展、性能优化等实用内容。如需进一步深入探讨某个主题,或需要其他补充,请随时告知。