引言
在现代Web应用中,实时双向通信已成为提升用户体验的关键。无论是直播弹幕、在线客服、协同编辑还是实时数据大屏,都要求后端能主动、即时地将数据推送给前端。传统的HTTP请求-响应模式(如轮询)难以高效地满足这一需求。
本文将详细讲解如何整合 Spring Boot (后端)、Vue (前端)并通过 Spring WebSocket + STOMP + SockJS 这一强大组合,构建一个高效、可靠的双向通信机制。最后,我们还会介绍如何用 Nginx 来部署前后端分离的项目。
一、技术选型:为什么是它们?
- WebSocket : HTML5提供的一种在单个TCP连接上进行全双工通信的协议,真正实现了低延迟的双向数据交换。
- STOMP (Simple Text Oriented Messaging Protocol) : 一种简单的基于帧的协议,定义了消息的格式和语义。它位于WebSocket之上,为我们提供了一个类似于消息队列(如RabbitMQ)的发布-订阅模式,使得我们可以像使用
@MessageMapping
注解那样处理消息,极大地简化了开发。 - SockJS: 一个JavaScript库,提供了WebSocket的模拟实现。它会在运行时优先尝试使用原生WebSocket,如果浏览器不支持或网络环境受限(如某些代理阻止WS连接),则会自动降级为其他技术(如长轮询),从而保证应用的兼容性和健壮性。
- Spring Boot : 提供了极其便捷的WebSocket支持,通过
@EnableWebSocketMessageBroker
等注解即可快速配置一个功能强大的WebSocket服务器。 - Vue : 轻量级、易上手的前端框架,配合
stompjs
和sockjs-client
库可以轻松连接WebSocket服务。 - Nginx: 高性能的HTTP和反向代理服务器,我们将用它来代理前端静态资源(Vue打包后的文件)和后端API/WebSocket请求。
二、实战
我们将通过一个经典的消息收发场景来串联所有技术点。
第一部分:Spring Boot 后端实现
1. 创建项目并引入依赖
引入 Spring Web
和 WebSocket
依赖。
xml
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2. 配置 WebSocket 和 STOMP 代理
创建一个配置类 WebSocketConfig
:
arduino
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker // 1. 启用WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 2. 配置消息代理
// 启用一个简单的基于内存的消息代理,将消息定向到以 `/topic` 为前缀的目的地
config.enableSimpleBroker("/topic");
// 设置应用程序目的地的前缀,所有以 `/app` 开头的消息都会路由到 @MessageMapping 注解的方法
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 3. 注册一个STOMP端点,客户端将使用它来连接
registry.addEndpoint("/ws-chat") // 端点URL
.setAllowedOriginPatterns("*") // 允许跨域。生产环境应严格限制为前端域名!
.withSockJS(); // 启用SockJS回退选项
}
}
/topic
: 用于发布-订阅模式(一对多)。服务端向所有订阅了/topic/xxx
的客户端广播消息。/app
: 用于点对点模式。客户端发送消息到/app/xxx
,由服务端的@MessageMapping("xxx")
方法处理。/ws-chat
: 这是WebSocket握手的HTTP端点URL。
3. 创建消息处理控制器
创建一个控制器来处理消息。
kotlin
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
// 处理所有发送到 `/app/chat` 目的地的消息
@MessageMapping("/chat") // 等价于 @RequestMapping
@SendTo("/topic/messages") // 将方法的返回值广播给所有订阅了 `/topic/messages` 的客户端
public ChatMessage sendMessage(ChatMessage message) {
// 这里可以处理消息,比如保存到数据库等
System.out.println("Received message: " + message.getContent());
return message; // 直接将消息广播出去
}
}
消息实体
typescript
public class ChatMessage {
private String from;
private String content;
private String timestamp;
// 务必提供默认构造函数和getter/setter方法
public ChatMessage() {}
public ChatMessage(String from, String content, String timestamp) {
this.from = from;
this.content = content;
this.timestamp = timestamp;
}
// ... getters and setters ...
}
至此,后端服务就完成了!它提供了一个WebSocket端点,能够接收/app/chat
的消息,并将其广播到/topic/messages
。
第二部分:Vue 前端实现
1. 创建Vue项目并安装依赖
perl
npm create vue@latest my-websocket-chat
cd my-websocket-chat
npm install sockjs-client stompjs
2. 创建WebSocket工具类 (src/utils/websocket.js
)
javascript
import SockJS from 'sockjs-client';
import { Stomp } from '@stomp/stompjs';
// 导出连接、断开、发送消息的方法
export const webSocketService = {
stompClient: null,
isConnected: false,
// 连接WebSocket
connect(config) {
const { url, onConnect, onError } = config;
const socket = new SockJS(url);
this.stompClient = Stomp.over(socket);
// 禁用调试信息(生产环境)
this.stompClient.debug = () => {};
this.stompClient.connect(
{},
(frame) => {
console.log('Connected: ' + frame);
this.isConnected = true;
if (onConnect) onConnect(this.stompClient);
},
(error) => {
console.error('Connection error: ', error);
this.isConnected = false;
if (onError) onError(error);
}
);
},
// 断开连接
disconnect() {
if (this.stompClient !== null) {
this.stompClient.disconnect();
this.isConnected = false;
}
console.log("Disconnected");
},
// 订阅主题
subscribe(destination, callback) {
if (this.stompClient && this.isConnected) {
return this.stompClient.subscribe(destination, (message) => {
if (callback) callback(JSON.parse(message.body));
});
}
return null;
},
// 发送消息
sendMessage(destination, message) {
if (this.stompClient && this.isConnected) {
this.stompClient.send(destination, {}, JSON.stringify(message));
} else {
console.error('WebSocket is not connected. Cannot send message.');
}
}
};
3. 在Vue组件中使用 (src/App.vue
)
xml
<template>
<div id="app">
<h1>Spring Boot + Vue Chat Room</h1>
<div class="chat-box">
<div v-for="(msg, index) in messages" :key="index" class="message">
<strong>{{ msg.from }}:</strong> {{ msg.content }} <em>({{ msg.timestamp }})</em>
</div>
</div>
<div class="input-area">
<input v-model="currentMessage" @keyup.enter="sendMessage" placeholder="Type a message..." />
<button @click="sendMessage" :disabled="!isConnected">Send</button>
<p>Status: {{ isConnected ? 'Connected' : 'Disconnected' }}</p>
</div>
</div>
</template>
<script>
import { webSocketService } from './utils/websocket.js';
export default {
name: 'App',
data() {
return {
isConnected: false,
currentMessage: '',
messages: [] // 存储收到的消息
};
},
mounted() {
this.connectWebSocket();
},
beforeUnmount() {
// 组件销毁前断开连接
webSocketService.disconnect();
},
methods: {
connectWebSocket() {
// 后端WebSocket端点,注意是 http,SockJS会自己处理
const serverUrl = 'http://localhost:8080/ws-chat';
webSocketService.connect({
url: serverUrl,
onConnect: (stompClient) => {
this.isConnected = true;
console.log('WebSocket connected successfully!');
// 订阅 `/topic/messages`,接收广播消息
webSocketService.subscribe('/topic/messages', (message) => {
this.messages.push(message); // 将收到的消息添加到列表
});
},
onError: (error) => {
this.isConnected = false;
console.error('Could not connect to WebSocket server.', error);
}
});
},
sendMessage() {
if (this.currentMessage.trim() && this.isConnected) {
const chatMessage = {
from: 'VueUser', // 这里可以改成用户输入的名字
content: this.currentMessage,
timestamp: new Date().toLocaleTimeString()
};
// 发送消息到 `/app/chat`
webSocketService.sendMessage('/app/chat', chatMessage);
this.currentMessage = ''; // 清空输入框
}
}
}
};
</script>
<style>
/* 添加一些简单的样式 */
.chat-box {
border: 1px solid #ccc;
height: 300px;
overflow-y: scroll;
margin-bottom: 10px;
padding: 10px;
}
.message {
margin-bottom: 5px;
}
.input-area {
display: flex;
gap: 10px;
}
input {
flex-grow: 1;
padding: 5px;
}
</style>
第三部分:Nginx 部署配置
现在,我们有了独立的前端(Vue,通常在8081端口)和后端(Spring Boot,在8080端口)。我们需要Nginx作为反向代理,让用户通过一个统一的域名和端口(通常是80/443)来访问整个应用。
1. 打包前端项目
arduino
npm run build
这会生成一个 dist
目录,里面是静态资源文件(HTML, JS, CSS)。
2. 编写Nginx配置文件 (nginx.conf
或 sites-available/your-site
)
bash
http {
# ... 其他全局配置 ...
server {
listen 80;
server_name your-domain.com; # 你的域名,本地测试可用 localhost
# 1. 代理所有静态资源请求到Vue的dist目录
location / {
root /path/to/your/vue-project/dist; # 替换为你的dist目录绝对路径
index index.html;
try_files $uri $uri/ /index.html; # 支持Vue Router的history模式
}
# 2. 代理后端API请求到Spring Boot应用
location /api/ {
proxy_pass http://localhost:8080/; # 代理到后端服务器
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 3. 代理WebSocket连接请求!
# 关键:因为WebSocket使用HTTP Upgrade机制,需要特殊配置
location /ws-chat/ {
proxy_pass http://localhost:8080; # 注意这里没有尾随的 `/`
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # 升级协议头
proxy_set_header Connection "Upgrade"; # 升级连接
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400; # WebSocket连接保持时间可以设长一些
}
# 也可以代理其他WebSocket端点,比如 /ws-notification/
}
}
3. 重启Nginx使配置生效
sudo nginx -s reload
4. 修改前端连接配置
在生产环境中,前端不再直接连接 localhost:8080
,而是连接相同的域名(或相对路径)。
ini
// 在 websocket.js 或 App.vue 中修改
// const serverUrl = 'http://localhost:8080/ws-chat'; // 开发环境
const serverUrl = '/ws-chat'; // 生产环境:使用相对路径,Nginx会自动代理
现在,访问 http://your-domain.com
,Nginx会:
- 将
/
请求指向Vue静态页面。 - 将
/api/xxx
请求转发给后端的Spring Boot应用。 - 将
/ws-chat
的WebSocket升级请求也转发给后端,从而建立起全双工通信。
第四部分:SimpMessagingTemplate
支持
使用 Spring Boot 的 SimpMessagingTemplate
给客户端发送消息非常方便。使用 convertAndSend(String destination, Object payload)
方法,所有订阅了该目的地(destination)的客户端都会收到消息。
arduino
private final SimpMessagingTemplate messagingTemplate;
@Autowired // 通过构造器注入
public MessageSendingService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
public void broadcastMessage(String messageContent) {
// 构建你的消息对象,这里用一个简单的字符串示例
String greeting = "Hello, everyone! " + messageContent;
// 发送给所有订阅了 "/topic/greetings" 的客户端
messagingTemplate.convertAndSend("/topic/greetings", greeting);
}
前端订阅示例(使用 Stomp.js):
javascript
stompClient.subscribe('/topic/greetings', function(message) {
console.log('Received broadcast: ' + message.body);
// 更新UI...
});