WebSocket 使用场景与实现方法:Java + Vue3 全栈实战

一、为什么需要 WebSocket

传统的 HTTP 是「请求---响应」模型:客户端不发请求,服务端就无法主动推送数据。早期实现实时效果通常靠轮询 或长轮询,前者会产生大量无效请求,后者会长期占用连接,两者在消息延迟和服务器资源上都存在明显浪费。

WebSocket 在单个 TCP 连接上提供全双工 通信能力:连接建立后,客户端和服务端都可以随时主动发送消息,且握手阶段复用 HTTP 协议(通过 Upgrade: websocket 升级),因此能很好地穿透代理和防火墙。它的典型特征是:

  • 低延迟:消息到达即可推送,无需等待下一次轮询。
  • 低开销:握手完成后数据帧头部很小,不像 HTTP 每次都要携带完整请求头。
  • 长连接:一次连接可承载大量双向消息,适合高频交互场景。
  • 服务端主动推送:这是它相对 HTTP 最核心的优势。

二、WebSocket 的典型使用场景

2.1 即时通讯与聊天室

单聊、群聊、客服系统是最经典的场景。用户 A 发送消息后,服务端需要立刻把消息推送给用户 B,而不是让 B 不断轮询接口。WebSocket 天然适合这种「一方发送、多方接收」的广播模型。

2.2 实时通知与消息中心

站内信、系统公告、订单状态变更、审批流转提醒等,都要求「事件发生即通知」。相比轮询,WebSocket 可以把服务端的业务事件直接推送到浏览器,用户无需刷新页面即可看到红点或弹窗。

2.3 实时数据大屏与行情推送

股票行情、数字货币价格、监控指标、物流轨迹等场景数据变化频繁。用 WebSocket 推送增量数据,既能保证秒级甚至毫秒级更新,又能避免高频轮询给后端带来的压力。

2.4 协同编辑与在线文档

多人同时编辑同一份文档时,需要把光标位置、文本变更等操作实时同步给其他协作者。WebSocket 的低延迟双向通道是协同编辑(如 OT、CRDT 算法)的基础设施。

2.5 在线游戏与实时互动

棋牌、答题、直播弹幕、在线白板等场景需要玩家操作即时同步。WebSocket 可以承载高频的小数据包交互,配合心跳机制维持连接稳定。

2.6 任务进度与长耗时操作反馈

文件导入导出、视频转码、批量数据处理等耗时任务,可以通过 WebSocket 把进度百分比持续推送给前端,让用户看到实时进度条,而不是干等一个最终结果。

三、整体架构设计

下面以一个「实时消息推送」的最小可用系统为例,技术栈为 Spring Boot + Java WebSocket + Vue3。整体链路如下:

flowchart LR A[Vue3 前端] -- WebSocket 握手 --> B[Spring Boot 服务端] B -- 建立会话 Session --> C[Session 管理器] D[业务事件] --> C C -- 主动推送 --> A A -- 发送消息 --> B B -- 广播/定向 --> C

核心要点有三个:

  1. 连接管理:服务端需要保存每个用户的 WebSocket 会话,支持按用户 ID 定向推送。
  2. 消息协议:约定统一的消息体格式(如 type、from、content、timestamp),便于前后端解析和扩展。
  3. 连接保活:通过心跳检测及时发现断线,并配合前端自动重连。

四、Java 服务端实现

4.1 引入依赖

使用 Spring Boot 提供的 spring-boot-starter-websocket,它封装了 JSR-356 标准 API,使用起来非常简洁。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

4.2 注册 WebSocket 端点

通过 ServerEndpointExporter 把标注了 @ServerEndpoint 的类注册为 WebSocket 端点。

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4.3 编写 WebSocket 端点

下面实现一个支持「连接、断开、接收消息、定向推送」的端点。注意 @ServerEndpoint 的实例不是单例,每个连接都会创建一个新对象,因此会话集合必须用 static 或交给 Spring 单例 Bean 管理。

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocketServer {

    /** 在线会话:userId -> Session */
    private static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();

    private static final ObjectMapper MAPPER = new ObjectMapper();

    private String userId;

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.userId = userId;
        SESSIONS.put(userId, session);
        System.out.println("用户上线:" + userId + ",当前在线人数:" + SESSIONS.size());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            ChatMessage msg = MAPPER.readValue(message, ChatMessage.class);
            msg.setFrom(userId);
            msg.setTimestamp(System.currentTimeMillis());

            if (msg.getTo() == null || msg.getTo().isEmpty()) {
                // 未指定接收人,广播给所有在线用户
                broadcast(msg);
            } else {
                // 定向推送给指定用户
                sendTo(msg.getTo(), msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session) {
        SESSIONS.remove(userId);
        System.out.println("用户下线:" + userId + ",当前在线人数:" + SESSIONS.size());
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("连接异常:" + userId + "," + error.getMessage());
    }

    private void broadcast(ChatMessage msg) throws IOException {
        String json = MAPPER.writeValueAsString(msg);
        for (Session s : SESSIONS.values()) {
            if (s.isOpen()) {
                s.getBasicRemote().sendText(json);
            }
        }
    }

    private void sendTo(String to, ChatMessage msg) throws IOException {
        Session target = SESSIONS.get(to);
        if (target != null && target.isOpen()) {
            target.getBasicRemote().sendText(MAPPER.writeValueAsString(msg));
        }
    }
}

4.4 消息实体

java 复制代码
public class ChatMessage {

    /** 消息类型:CHAT / NOTICE / HEARTBEAT */
    private String type;

    /** 发送者 */
    private String from;

    /** 接收者,为空表示广播 */
    private String to;

    /** 消息内容 */
    private String content;

    /** 时间戳 */
    private Long timestamp;

    public String getType() { return type; }
    public void setType(String type) { this.type = type; }

    public String getFrom() { return from; }
    public void setFrom(String from) { this.from = from; }

    public String getTo() { return to; }
    public void setTo(String to) { this.to = to; }

    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; }
}

4.5 从业务代码主动推送

实际项目中,推送往往由业务事件触发,例如订单支付成功后通知用户。可以把会话管理抽成独立的单例 Bean,供任意业务代码调用。

java 复制代码
import org.springframework.stereotype.Service;

import javax.websocket.Session;
import java.io.IOException;

@Service
public class PushService {

    public void pushToUser(String userId, String json) {
        Session session = ChatWebSocketServer.getSession(userId);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(json);
            } catch (IOException e) {
                throw new RuntimeException("推送失败", e);
            }
        }
    }
}

提示:如果服务端是多实例部署,单机的 Session 集合无法跨节点共享。此时需要引入 Redis 发布订阅、MQ 或专门的网关(如 Spring Cloud Gateway + WebSocket)来做消息路由。

五、Vue3 前端实现

5.1 封装 WebSocket 组合式函数

把连接、重连、心跳、消息分发封装成一个可复用的 useWebSocket,避免在组件里散落大量样板代码。

javascript 复制代码
import { ref, onMounted, onUnmounted } from 'vue'

export function useWebSocket(url, options = {}) {
  const { heartbeatInterval = 30000, reconnectDelay = 3000 } = options

  const socket = ref(null)
  const connected = ref(false)
  const messages = ref([])

  let heartbeatTimer = null
  let reconnectTimer = null
  let manualClose = false

  const connect = () => {
    socket.value = new WebSocket(url)

    socket.value.onopen = () => {
      connected.value = true
      startHeartbeat()
    }

    socket.value.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)
        if (data.type === 'HEARTBEAT') return
        messages.value.push(data)
      } catch (e) {
        console.warn('消息解析失败', event.data)
      }
    }

    socket.value.onclose = () => {
      connected.value = false
      stopHeartbeat()
      if (!manualClose) {
        reconnectTimer = setTimeout(connect, reconnectDelay)
      }
    }

    socket.value.onerror = () => {
      socket.value && socket.value.close()
    }
  }

  const startHeartbeat = () => {
    stopHeartbeat()
    heartbeatTimer = setInterval(() => {
      if (socket.value && socket.value.readyState === WebSocket.OPEN) {
        socket.value.send(JSON.stringify({ type: 'HEARTBEAT' }))
      }
    }, heartbeatInterval)
  }

  const stopHeartbeat = () => {
    if (heartbeatTimer) {
      clearInterval(heartbeatTimer)
      heartbeatTimer = null
    }
  }

  const send = (data) => {
    if (socket.value && socket.value.readyState === WebSocket.OPEN) {
      socket.value.send(JSON.stringify(data))
      return true
    }
    return false
  }

  const close = () => {
    manualClose = true
    stopHeartbeat()
    clearTimeout(reconnectTimer)
    socket.value && socket.value.close()
  }

  onMounted(connect)
  onUnmounted(close)

  return { socket, connected, messages, send, close }
}

5.2 在组件中使用

vue 复制代码
<template>
  <div class="chat">
    <p>连接状态:{{ connected ? '已连接' : '未连接' }}</p>

    <ul class="msg-list">
      <li v-for="(msg, index) in messages" :key="index">
        <strong>{{ msg.from }}</strong>:{{ msg.content }}
      </li>
    </ul>

    <div class="input-area">
      <input v-model="inputText" placeholder="输入消息" @keyup.enter="handleSend" />
      <button @click="handleSend">发送</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'

const userId = '1001'
const inputText = ref('')

const { connected, messages, send } = useWebSocket(
  `ws://localhost:8080/ws/chat/${userId}`
)

const handleSend = () => {
  if (!inputText.value.trim()) return
  const ok = send({
    type: 'CHAT',
    to: '',
    content: inputText.value
  })
  if (ok) inputText.value = ''
}
</script>

<style scoped>
.chat { max-width: 600px; margin: 20px auto; }
.msg-list { height: 300px; overflow-y: auto; border: 1px solid #eee; padding: 10px; }
.input-area { display: flex; gap: 8px; margin-top: 10px; }
</style>

5.3 生产环境注意事项

  • 协议选择 :HTTPS 页面必须使用 wss://,否则浏览器会拦截混合内容。
  • 鉴权 :浏览器原生 WebSocket API 不支持自定义请求头,通常把 token 放在 URL 参数或握手后的首条消息中,服务端在 @OnOpen 或拦截器中校验。
  • 断线重连:网络抖动、服务重启都会导致连接断开,必须实现指数退避重连,避免瞬间大量重连压垮服务端。
  • 心跳保活:Nginx 等代理默认会断开长时间无数据的连接,需要客户端定时发送心跳包。
  • 消息幂等:网络重连可能导致消息重复,业务侧应通过消息 ID 做去重。

六、HTTP 与 WebSocket 选型对比

对比项 HTTP 轮询 WebSocket
通信方向 单向(客户端发起) 双向全双工
实时性 取决于轮询间隔 毫秒级推送
连接开销 每次请求都有完整头部 握手后帧头很小
服务端压力 高频轮询压力大 长连接,压力可控
适用场景 低频、非实时数据 聊天、通知、行情、协同

选型建议:如果业务对实时性要求不高(如每分钟同步一次配置),普通 HTTP 接口更简单;一旦涉及「服务端主动推送」和「高频双向交互」,WebSocket 才是更合适的方案。

七、总结

WebSocket 解决的是 HTTP 无法主动推送的问题,在聊天、通知、实时数据、协同编辑等场景中几乎是标配。落地时把握三个关键点:

  1. 服务端做好会话管理,支持定向推送与广播,并考虑多实例下的消息路由。
  2. 前端封装连接逻辑,统一处理心跳、重连和消息解析,避免组件内重复造轮子。
  3. 重视生产环境细节,包括 wss 协议、鉴权方式、心跳保活和消息幂等。

把上面的 Java 端点和 Vue3 组合式函数跑通后,就可以在此基础上扩展群组、离线消息、消息已读等更完整的业务能力。

相关推荐
wdfk_prog2 小时前
lwip教程 38:从 `lwipopts.h` 到 Port Contract——lwIP 裁剪、构建选择、OS Port 与 Network Port
java·开发语言
吹什么轩2 小时前
MySQL 内置函数全景:日期 / 字符串 / 数学 / 其他,一次讲透(附索引杀手排查)
java·数据库·面试
略略略咯咯2 小时前
12333
java
会编程的吕洞宾2 小时前
Spring AI 语义缓存:重复提问不重复花钱
java·人工智能·后端·spring
海绵宝宝转agent2 小时前
leetcodehot100个人题解
java·笔记·学习·算法
sunburn-2 小时前
数据库增删改查:从 SQL 基础到实践要点
java·数据库·mysql
郭小李3 小时前
spring @RequestMapping注解
java·后端·spring
晨曦中的暮雨11 小时前
限流算法——固定窗口、滑动窗口、漏桶、令牌桶
java·开发语言·算法
文丑颜不良啊13 小时前
LeetCode876~900题目解析
java·leetcode