构建实时消息应用:Spring Boot + Vue 与 WebSocket 的有机融合

引言

在现代Web应用中,实时双向通信已成为提升用户体验的关键。无论是直播弹幕、在线客服、协同编辑还是实时数据大屏,都要求后端能主动、即时地将数据推送给前端。传统的HTTP请求-响应模式(如轮询)难以高效地满足这一需求。

本文将详细讲解如何整合 Spring Boot (后端)、Vue (前端)并通过 Spring WebSocket + STOMP + SockJS 这一强大组合,构建一个高效、可靠的双向通信机制。最后,我们还会介绍如何用 Nginx 来部署前后端分离的项目。

一、技术选型:为什么是它们?

  1. WebSocket : HTML5提供的一种在单个TCP连接上进行全双工通信的协议,真正实现了低延迟的双向数据交换。
  2. STOMP (Simple Text Oriented Messaging Protocol) : 一种简单的基于帧的协议,定义了消息的格式和语义。它位于WebSocket之上,为我们提供了一个类似于消息队列(如RabbitMQ)的发布-订阅模式,使得我们可以像使用@MessageMapping注解那样处理消息,极大地简化了开发。
  3. SockJS: 一个JavaScript库,提供了WebSocket的模拟实现。它会在运行时优先尝试使用原生WebSocket,如果浏览器不支持或网络环境受限(如某些代理阻止WS连接),则会自动降级为其他技术(如长轮询),从而保证应用的兼容性和健壮性。
  4. Spring Boot : 提供了极其便捷的WebSocket支持,通过@EnableWebSocketMessageBroker等注解即可快速配置一个功能强大的WebSocket服务器。
  5. Vue : 轻量级、易上手的前端框架,配合stompjssockjs-client库可以轻松连接WebSocket服务。
  6. Nginx: 高性能的HTTP和反向代理服务器,我们将用它来代理前端静态资源(Vue打包后的文件)和后端API/WebSocket请求。

二、实战

我们将通过一个经典的消息收发场景来串联所有技术点。

第一部分:Spring Boot 后端实现

1. 创建项目并引入依赖

引入 Spring WebWebSocket 依赖。

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.confsites-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...
});

相关推荐
渣哥3 小时前
Java ConcurrentHashMap vs Hashtable:差异、性能与应用场景
java
金銀銅鐵3 小时前
[Java] 浅析可重复注解(Repeatable Annotation) 是如何实现的
java·后端
柯南二号3 小时前
【设计模式】【观察者模式】实例
java·开发语言·servlet
用户8174413427483 小时前
Kubernetes集群核心概念 Service
后端
掘金一周3 小时前
凌晨零点,一个TODO,差点把我们整个部门抬走 | 掘金一周 9.11
前端·人工智能·后端
yeyong3 小时前
日志告警探讨,我问deepseek答,关于elastalert
后端
Cyan_RA93 小时前
SpringMVC 执行流程分析 详解(图解SpringMVC执行流程)
java·人工智能·后端·spring·mvc·ssm·springmvc
索迪迈科技3 小时前
Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战
java·开发语言·spring
练习时长一年4 小时前
自定义事件发布器
java·前端·数据库