构建实时消息应用: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...
});

相关推荐
阿杰同学1 分钟前
Java NIO 面试题及答案整理,最新面试题
java·开发语言·nio
没有bug.的程序员13 分钟前
GC日志解析:从日志看全流程
java·网络·jvm·spring·日志·gc
WZTTMoon14 分钟前
开发中反复查的 Spring Boot 注解,一次性整理到位
java·spring boot·后端
长沙古天乐16 分钟前
Spring Boot应用中配置消费端随服务启动循环消费消息
spring boot·后端·linq
葡萄城技术团队17 分钟前
Excel 文件到底是怎么坏掉的?深入 OOXML 底层原理讲解修复策略
android·java·excel
照物华22 分钟前
MySQL 软删除 (Soft Delete) 与唯一索引 (Unique Constraint) 的冲突与解决
java·mysql
mjhcsp22 分钟前
C++ 后缀自动机(SAM):原理、实现与应用全解析
java·c++·算法
wadesir25 分钟前
掌握 Rust 中的浮点数处理(Rust f64 浮点数与标准库详解)
开发语言·后端·rust
IT_陈寒27 分钟前
React 18新特性实战:这5个Hook组合让我少写50%状态管理代码
前端·人工智能·后端
HashTang28 分钟前
【AI 编程实战】第 1 篇:TRAE SOLO 模式 10 倍速开发商业级全栈小程序
前端·后端·ai编程