Skip to content

WebSocket 实战学习文档

更新: 6/6/2026 字数: 0 字 时长: 0 分钟

面向前端 / 全栈开发者的科普向技术解读。本文把 WebSocket 是什么、原理、前后端怎么用、以及如何用心跳重连让连接更稳定,讲清楚、讲通透。

一、WebSocket 是什么:从"一问一答"到"双向实时"

WebSocket 是什么

WebSocket 是一种网络通信协议,它能在单个 TCP 连接上实现全双工(双向同时)通信。 它在 2011 年被 IETF 定为标准 RFC 6455。

要理解它的价值,先看传统 HTTP 的局限:

  • 传统 HTTP 是"一问一答":必须客户端先提问,服务器才能回答。服务器无法主动把消息推给客户端。想做实时聊天?只能让前端不停轮询"有新消息吗?有新消息吗?"——既费流量又有延迟。
  • WebSocket 是"双向实时通话":连接一旦建立,就像架起了一条始终在线的双向管道,客户端和服务器都能随时主动发消息,无需对方先开口。

它的三个核心特点:

特点含义
双向实时通讯服务器可主动向客户端推送数据,不必等客户端请求
低延迟省去了 HTTP 每次请求都要重新建立连接的开销,延迟远低于轮询
长连接连接建立后持续保持,直到任一方主动关闭

通俗类比:HTTP 像发短信——你发一条、对方才回一条;WebSocket 像打电话——接通后双方随时都能说话。

典型应用场景:实时聊天、在线游戏、股票/数据实时推送、协同编辑、扫码登录、监控大屏等一切需要"实时"的场景。

二、工作原理:握手、数据交换、关闭三阶段

工作原理三阶段

一次完整的 WebSocket 通信分为三个阶段:

1. 握手(建立连接) 客户端先用一个普通的 HTTP 请求向服务器发起连接,但这个请求带了一个特殊的 Upgrade 头部,意思是"请把这条连接升级成 WebSocket"。服务器确认后回应,握手完成——连接就从 HTTP 升级成了 WebSocket。

为什么要借 HTTP 起步?因为这样能复用现有的端口(80/443)和基础设施,更容易穿过防火墙和代理。

2. 数据交换 握手完成后,双方就能自由收发数据了。数据会被封装成一个个**帧(frame)**进行传输——帧是 WebSocket 里传输数据的基本单位,这种结构便于解析、也便于区分消息边界。

3. 关闭连接 任意一方都可以发送一个特定的关闭帧来结束连接,收到关闭帧的一方随即关闭。连接就此优雅地断开。

text
①握手           ②数据交换              ③关闭
HTTP Upgrade →  ⇄ 帧 ⇄ 帧 ⇄ 帧 ⇄  →  关闭帧 → 断开
(升级为 WS)    (双向自由收发)        (任一方发起)

三、服务端实现:Spring Boot 整合 WebSocket

服务端实现

下面以一个聊天室为例,演示后端如何用 Spring Boot 搭起 WebSocket 服务。核心思路:用一张「在线连接表」记录所有连接,靠四个生命周期事件(上线、收消息、下线、异常)来管理它们。

① 引入依赖pom.xml

xml
<!-- Spring WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

② 配置类:注册 ServerEndpointExporter 以启用 WebSocket 端点

java
/**
 * 开启 WebSocket 支持
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

③ 工具类:用一个线程安全的 Map 维护「用户 ID → 会话」的映射,并封装收发消息的方法

java
public class WebsocketUtil {
    /** 记录当前在线的 Session(key 为 userId) */
    private static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>();

    /** 添加 session:同一用户只保留一个连接,多余连接视为无效 */
    public static void addSession(String userId, Session session) {
        ONLINE_SESSION.putIfAbsent(userId, session);
    }

    /** 关闭 session */
    public static void removeSession(String userId) {
        ONLINE_SESSION.remove(userId);
    }

    /** 给单个用户推送消息(异步发送) */
    public static void sendMessage(Session session, String message) {
        if (session == null) {
            return;
        }
        RemoteEndpoint.Async async = session.getAsyncRemote();
        async.sendText(message);
    }

    /** 向所有在线用户广播消息 */
    public static void sendMessageForAll(String message) {
        ONLINE_SESSION.forEach((sessionId, session) -> sendMessage(session, message));
    }
}

④ 端点处理类:这是 WebSocket 真正的请求地址,userId 相当于"房间名",用户按房间名进入并监听消息

java
@Component
@ServerEndpoint(value = "/chat/{userId}")
public class WebsocketController {

    /** 连接建立:用户上线 */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        // 把当前连接登记到在线表
        WebsocketUtil.addSession(userId, session);
        // 如需广播上线通知,可放开下行
        // WebsocketUtil.sendMessageForAll("[" + userId + "]加入聊天室!");
    }

    /** 连接关闭:用户下线 */
    @OnClose
    public void onClose(@PathParam("userId") String userId, Session session) {
        WebsocketUtil.removeSession(userId);
        WebsocketUtil.sendMessageForAll("[" + userId + "]退出了聊天室...");
    }

    /** 收到用户消息:直接广播给所有人 */
    @OnMessage
    public void onMessage(@PathParam("userId") String userId, Session session, String message) {
        String msg = "[" + userId + "]:" + message;
        System.out.println("接收到信息:" + msg);
        WebsocketUtil.sendMessageForAll(msg);
    }

    /** 处理连接异常:关闭并打印堆栈 */
    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throwable.printStackTrace();
    }
}

⑤ 用 HTTP 接口给指定用户推消息 有时消息源不在 WebSocket 这一侧。比如扫码登录:手机端通过普通 HTTP 通知后端登录成功,而展示二维码、监听结果的是网页端(两者是分开的)。此时就需要一个 HTTP 接口,按房间 ID 找到对应连接并推送:

java
@PostMapping("/send")
public void send(@RequestParam("id") String id, @RequestParam("message") String message) {
    Session session = ONLINE_SESSION.get(id);
    WebsocketUtil.sendMessage(session, message);
    System.out.println("发送成功");
}

对比一下:聊天室这种场景,前端直接 socket.send() 就能把消息发给服务器;而扫码登录因为收/显分离在不同设备,所以触发推送的一端走 HTTP。本节代码思路参考自掘金社区文章[1]

四、前端使用:几行代码连上服务器

前端使用

浏览器原生就内置了 WebSocket 对象,前端只需关注四个关键点:建立连接、监听消息、监听关闭、发送消息

javascript
// ws:// 是 WebSocket 协议,路径里的 1 即后端的 userId(房间名)
const socket = new WebSocket('ws://localhost:8888/chat/1');

// 连接成功打开
socket.onopen = (event) => {
    console.log('WebSocket 连接已建立:', event);
};

// 接收后端推来的消息
socket.onmessage = (event) => {
    const data = event.data;
    console.log('收到消息:', data);
};

// 连接关闭
socket.onclose = (event) => {
    console.log('WebSocket 连接已关闭:', event);
};

// 主动发送消息到后端
function sendMessage(message) {
    socket.send(message);
}
事件/方法作用
new WebSocket(url)发起连接,ws://(明文)或 wss://(加密,对应 HTTPS)
onopen连接建立成功时触发
onmessage收到服务器消息时触发,数据在 event.data
onclose连接关闭时触发
send()主动向服务器发送数据

五、心跳与重连:让连接"始终在线"

心跳与重连机制

1. WebSocket 的"不稳定性"

想象你和朋友打电话,说着说着信号断了,但双方都没收到提示。你继续说,对方却听不见——沟通失效了。WebSocket 也有这个问题:网络波动时连接可能悄悄断开,而客户端和服务器都浑然不觉。

2. 什么是心跳包?

心跳包就是定时发送的"小问候",用来确认双方还活着、连接还在。 比如客户端每隔 5 秒发一句"我还在哦~"(即 ping),服务器回一句"我也在~"(即 pong);如果连续几次收不到回复,就判定连接已断。

心跳包的三大作用:① 检测连接是否存活;② 防止长时间不通信被防火墙/代理切断;③ 像定期体检一样监控连接状态。

3. 关键参数怎么定

参数推荐值说明
心跳间隔5–30 秒太短浪费流量,太长不能及时发现断开
超时时间心跳间隔的 2–3 倍如间隔 5 秒,超时设 15 秒
重连策略指数退避等待时间逐次翻倍(3→6→12 秒),避免雪崩式重连

4. 常见问题与对策

  • 心跳被防火墙拦截 → 改由服务器主动发心跳(反向心跳),或调整间隔避开防火墙规则。
  • 频繁重连拖垮性能 → 采用指数退避 + 设置最大重连次数。
  • 重连后状态丢失 → 重连成功后补发未完成的请求,并用 localStorage 等缓存关键状态。

5. 完整封装示例

下面这个 WebSocketClient 类把"连接 + 心跳 + 重连"封装到一起,可直接复用:

javascript
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.heartBeatTimer = null;
    this.reconnectTimer = null;
    this.heartBeatInterval = 5000;   // 心跳间隔 5 秒
    this.reconnectInterval = 3000;   // 重连间隔 3 秒
    this.maxReconnectTimes = 10;     // 最多重连 10 次
    this.currentReconnectTimes = 0;
    this.isConnecting = false;
    this.connect();
  }

  connect() {
    if (this.isConnecting) return;
    this.isConnecting = true;
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket 连接已建立');
      this.isConnecting = false;
      this.currentReconnectTimes = 0; // 重置重连计数
      this.startHeartBeat();          // 启动心跳
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') {
        console.log('收到心跳响应');   // 连接正常
      } else {
        this.onMessage(event.data);   // 处理业务消息
      }
    };

    this.ws.onclose = () => {
      console.log('WebSocket 连接已关闭');
      this.stopHeartBeat();
      if (!this.isConnecting) {
        this.scheduleReconnect();     // 安排重连
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket 发生错误:', error);
      this.ws.close();
    };
  }

  startHeartBeat() {
    this.stopHeartBeat();
    this.heartBeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send('ping');         // 定时发心跳
      }
    }, this.heartBeatInterval);
  }

  stopHeartBeat() {
    if (this.heartBeatTimer) {
      clearInterval(this.heartBeatTimer);
      this.heartBeatTimer = null;
    }
  }

  scheduleReconnect() {
    if (this.currentReconnectTimes >= this.maxReconnectTimes) {
      console.log('已达到最大重连次数,停止重连');
      return;
    }
    this.currentReconnectTimes++;
    console.log(`准备第 ${this.currentReconnectTimes} 次重连,${this.reconnectInterval / 1000} 秒后...`);
    this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectInterval);
  }

  send(message) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      console.log('WebSocket 未连接,消息发送失败');
      // 可选:缓存消息,重连后补发
    }
  }

  onMessage(message) {
    // 子类可重写此方法处理业务消息
    console.log('收到消息:', message);
  }

  close() {
    this.stopHeartBeat();
    clearTimeout(this.reconnectTimer);
    this.isConnecting = false;
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// 使用示例
const client = new WebSocketClient('ws://example.com');
client.onMessage = (message) => console.log('处理业务消息:', message);
client.send('Hello, server!');

6. 一句话记住心跳重连

就像两个人约定每隔几分钟说一句"我还在";连续几次没回应就当对方失联,于是尝试重新联系;一次打不通就等一会儿再打,等待时间逐次拉长;打满 10 次还不通就暂时放弃。这套机制大幅提升了实时聊天、监控系统、在线游戏等长连接场景的稳定性。

一页速记

  • WebSocket:单 TCP 连接上的全双工协议,双向、低延迟、长连接,弥补 HTTP "一问一答"的局限。
  • 三阶段:HTTP 握手升级 → 帧形式双向交换 → 任一方发关闭帧断开。
  • 后端(Spring Boot)@ServerEndpoint + 四个事件(@OnOpen/@OnMessage/@OnClose/@OnError),用在线表管理连接、广播消息。
  • 前端new WebSocket() + onopen/onmessage/onclose/send() 四件套。
  • 心跳重连:定时 ping/pong 探活 + 指数退避自动重连 + 最大次数限制,是长连接稳定的关键。