关于websocket

111 阅读8分钟

23年11月份时,在青芒果写过websocket实现医生问诊的消息提醒(JAVA+JS); 24年12月份,在北京实现websocket前后端主动传输消息(JS); 25年2月份开始了解,智能体前端流式传输的全流程。 谨以此文回顾一下历史。

websocket

什么是websocket

WebSocket 是一种基于 TCP 的应用层网络通信协议,它允许在客户端(如浏览器)和服务器之间建立全双工(双向实时通信)​的持久化连接。与传统的 HTTP 请求不同,WebSocket 的设计目标是解决实时通信的效率问题,特别适合需要高频数据交互的场景(如聊天、游戏、实时监控等)。

websocket应用场景

  • 实时聊天(如微信网页版)
  • 多人在线游戏(实时同步玩家状态)
  • 股票行情、监控系统(实时数据推送)
  • 协同编辑工具(如 Google Docs)

核心特点

  1. 双向通信

    • 客户端和服务器可以主动向对方发送数据,无需等待请求-响应模式。
    • 例如:聊天室中用户发送消息(客户端→服务器),同时实时接收他人消息(服务器→客户端)。
  2. 低延迟

    • 建立连接后,数据通过轻量级的帧(Frame)传输,减少了 HTTP 的头部开销。
    • 无需频繁建立/断开连接(HTTP 的短连接会导致重复握手)。
  3. 持久化连接

    • 通过一次 HTTP 握手升级为 WebSocket 连接,之后保持长连接,直到主动关闭。
    • 对比 HTTP 轮询(Polling)或长轮询(Long Polling),性能更优。

与HTTP的区别

特性WebSocketHTTP
通信模式全双工(双向实时)半双工(请求-响应)
连接生命周期持久化长连接短连接(默认关闭)
数据开销轻量(帧头部小)较高(每次携带完整头)
适用场景实时交互静态资源、简单请求

websocket基础介绍

实例属性

image.png

主要方法

  1. url

    • 作用:返回 WebSocket 连接的完整 URL(只读)。
    • 例如:"wss://example.com/chat"
  2. bufferedAmount

    • 作用:返回尚未发送到服务器的缓冲字节数​(用于流量控制)。

    • 用途:避免发送速度超过网络处理能力。

      javascript
      // 检查缓冲区是否为空
      if (socket.bufferedAmount === 0) {
        socket.send(largeData);
      }
      
  3. protocol

    • 作用:返回服务器选择的子协议(在握手时由 Sec-WebSocket-Protocol 头协商决定)。
    • 例如:客户端请求 ["chat", "superchat"],服务器选择 "chat"
  4. extensions

    • 作用:返回服务器选择的扩展(如压缩算法),通常为空字符串。
  5. binaryType

    • 作用:设置或返回二进制数据的接收类型("blob" 或 "arraybuffer")。
    • 默认值为 "blob"

除了 addEventListener,还可以通过以下属性直接绑定事件:

  • onopen:连接建立时触发。
  • onmessage:收到服务器数据时触发(event.data 包含数据)。
  • onerror:发生错误时触发。
  • onclose:连接关闭时触发(event.code 和 event.reason 包含关闭信息)。
socket.binaryType = 'arraybuffer';  // 接收二进制数据为 ArrayBuffer
  1. 几种状态

通过实例的 ​**readyState** 属性获取当前连接状态,值为以下常量之一:

状态常量含义
WebSocket.CONNECTING0连接尚未建立(正在握手或连接中)。
WebSocket.OPEN1连接已建立,可以通信。
WebSocket.CLOSING2连接正在关闭(调用了 close() 方法,但未完全关闭)。
WebSocket.CLOSED3连接已关闭或未能建立(可能是网络错误或握手失败)。
if (socket.readyState === WebSocket.OPEN) {
  socket.send('数据');
}
  1. 使用示例
const socket = new WebSocket('wss://example.com');

// 监听状态变化
socket.onopen = () => {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send('Hello Server!');
  }
};

socket.onmessage = (event) => {
  console.log('收到:', event.data);
};

socket.onclose = (event) => {
  console.log(`关闭原因: ${event.code} - ${event.reason}`);
};

// 主动关闭连接
setTimeout(() => {
  if (socket.readyState === WebSocket.OPEN) {
    socket.close(1000, '用户离开');
  }
}, 5000);

websocket特色使用与封装

心跳检测(保活机制)

作用:防止连接因网络波动或服务器静默断开,通过定期发送心跳包维持连接活性。

实现步骤:
  1. 连接建立后,启动心跳定时器(setInterval)。
  2. 客户端定时向服务器发送心跳消息(如 ping)。
  3. 服务器需响应心跳(如 pong),若超时未响应则判定连接失效,触发重连。
class WebSocketManager {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.pingInterval = 30000; // 30秒发送一次心跳
    this.pongTimeout = 5000;   // 5秒内未收到响应则断开
    this.heartbeatTimer = null;
    this.pongTimer = null;

    this.ws.onopen = () => {
      this.startHeartbeat();
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') {
        this.clearPongTimeout();
      }
    };
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.ws.send('ping');
      this.waitPong();
    }, this.pingInterval);
  }

  waitPong() {
    this.pongTimer = setTimeout(() => {
      this.ws.close(); // 主动关闭,触发重连逻辑
    }, this.pongTimeout);
  }

  clearPongTimeout() {
    clearTimeout(this.pongTimer);
  }
}

短线自动重连

作用:网络中断或服务器重启后,自动尝试重新建立连接,提升容错性。

实现步骤:
  1. 监听 onclose 事件,根据状态码判断是否需要重连。
  2. 使用指数退避策略(避免高频重连)。
  3. 限制最大重试次数,避免无限重试。
class WebSocketManager {
  constructor(url) {
    this.url = url;
    this.reconnectLimit = 5;     // 最大重连次数
    this.reconnectCount = 0;     // 当前重连次数
    this.reconnectTimer = null;  // 重连定时器

    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onclose = (event) => {
      if (event.code !== 1000) { // 1000 表示正常关闭,不重连
        this.reconnect();
      }
    };
  }

  reconnect() {
    if (this.reconnectCount < this.reconnectLimit) {
      this.reconnectCount++;
      const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000); // 指数退避
      this.reconnectTimer = setTimeout(() => this.connect(), delay);
    }
  }
}

重连数据丢失,重新发送

作用:连接中断期间未发送成功的数据,在重连后重新发送,避免丢失。

实现步骤:
  1. 维护一个待发送消息队列(pendingQueue)。
  2. 连接断开时缓存新消息。
  3. 重连成功后,遍历队列重新发送。
class WebSocketManager {
  constructor(url) {
    this.pendingQueue = []; // 待发送消息队列
    // ...其他初始化

    this.ws.onopen = () => {
      this.resendPendingMessages();
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      this.pendingQueue.push(data); // 缓存到队列
    }
  }

  resendPendingMessages() {
    while (this.pendingQueue.length > 0 && this.ws.readyState === WebSocket.OPEN) {
      const message = this.pendingQueue.shift();
      this.ws.send(message);
    }
  }
}

刷新页面websocket断开

作用:页面刷新或关闭前,优雅关闭 WebSocket 连接,避免资源浪费。

实现步骤:
  1. 监听 beforeunload 事件,主动关闭连接。
  2. 在页面加载时自动初始化连接。
class WebSocketManager {
  constructor(url) {
    // 页面加载时初始化连接
    this.connect();

    // 刷新或关闭页面前关闭连接
    window.addEventListener('beforeunload', () => {
      this.ws.close(1000, '用户离开页面');
    });
  }

  connect() {
    this.ws = new WebSocket(this.url);
    // ...其他事件绑定
  }
}

websocket完整封装示例

class WebSocketManager {
  constructor(url) {
    this.url = url;
    this.pendingQueue = [];
    this.reconnectLimit = 5;
    this.reconnectCount = 0;
    this.reconnectTimer = null;
    this.pingInterval = 30000;
    this.pongTimeout = 5000;
    this.heartbeatTimer = null;
    this.pongTimer = null;

    this.connect();
    this.bindPageEvents();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.bindEvents();
  }

  bindEvents() {
    this.ws.onopen = () => {
      this.reconnectCount = 0; // 重置重连次数
      this.resendPendingMessages();
      this.startHeartbeat();
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') {
        this.clearPongTimeout();
      }
      // 处理业务数据...
    };

    this.ws.onclose = (event) => {
      if (event.code !== 1000) {
        this.reconnect();
      }
      this.clearHeartbeat();
    };
  }

  // 心跳检测、重连、数据重发等方法(参考前文实现)

  bindPageEvents() {
    window.addEventListener('beforeunload', () => {
      this.ws.close(1000, '页面关闭');
    });
  }

  // 清理资源
  destroy() {
    clearTimeout(this.reconnectTimer);
    clearInterval(this.heartbeatTimer);
    clearTimeout(this.pongTimer);
    this.ws.close();
  }
}

// 使用示例
const wsManager = new WebSocketManager('wss://example.com');

websocket前端边界情况

1. 服务端主动断开

场景:服务器主动关闭连接(如用户被踢出、维护)。

方案

  • 监听 onclose 事件,解析 event.code 和 event.reason
  • 状态码处理:根据约定状态码决定是否重连或提示用户(与服务端协商)。
socket.onclose = (event) => {
  if (event.code === 1000) {
    console.log('连接正常关闭');
  } else if (event.code === 4000) { // 自定义状态码(如 token 过期)
    alert('登录已失效,请重新登录');
    redirectToLogin();
  } else {
    this.reconnect(); // 触发自动重连
  }
};

2. 网络关闭导致断开

场景:用户网络断开后恢复,需自动重连。

方案

  • 监听 navigator.onLine 事件,检测网络恢复。
  • 结合自动重连逻辑(指数退避)。
// 检测网络恢复
window.addEventListener('online', () => {
  if (socket.readyState === WebSocket.CLOSED) {
    this.reconnect();
  }
});

3. 页面失活状态(重点)

使用定时器来做心跳检测会有问题,页面失活一段时间定时器就变惰性了(偶尔执行一下甚至不执行),所以页面失活状态下连接是极高概率会断开的。

  • 方案1(我的iframe重载也是这么做的)
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    this.pauseHeartbeat(); // 暂停心跳
  } else {
    this.resumeHeartbeat(); // 恢复心跳
  }
});
  • 方案2(todo)

一般是在电脑息屏后15分钟左右定时器就会自动停止运行,此时心跳也会停止发送,就会出现服务断开的情况,用worker可以解决该问题。

把websocket置于worker中工作,通过JS主线程与worker通讯,来调取websocket的api即可

4. 同一页面多开复用同一websocket实例

场景:多个标签页打开同一应用,共享 WebSocket 连接以减少资源消耗。

方案

  • BroadcastChannel API:跨标签页通信,主页面维持连接,其他页面通过消息代理。
  • SharedWorker:使用 Web Worker 共享连接(兼容性较差)。
// 主页面代码
const channel = new BroadcastChannel('ws_control');

// 主页面维持连接
if (isPrimaryTab()) { 
  const socket = new WebSocket(url);
  socket.onmessage = (event) => {
    channel.postMessage({ type: 'message', data: event.data });
  };
}

// 其他页面监听消息
channel.onmessage = (event) => {
  if (event.data.type === 'message') {
    handleMessage(event.data.data);
  }
};

websocket后端实现

当时记得第一次写这个调试得我好费劲

service方法

package vip.xiaonuo.biz.modular.websocket;

import lombok.extern.slf4j.Slf4j;
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.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


@ServerEndpoint("/webSocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //发送消息
    public void sendMessage(Session session, String message) throws IOException {
        if(session != null){
            synchronized (session) {
                System.out.println("发送数据:" + message);
                session.getBasicRemote().sendText(message);
            }
        }
    }
    //给指定用户发送信息
    public void sendInfo(String userId, String message){
        Session session = sessionPools.get(userId);
        System.out.println("session:" + session);
        System.out.println("session:" + sessionPools);

        try {
            sendMessage(session, message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    // 群发消息
    public void broadcast(String message){
        for (Session session: sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch(Exception e){
                e.printStackTrace();
                continue;
            }
        }
    }

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId){
        sessionPools.put(userId, session);
        addOnlineCount();
        System.out.println(userId + "加入webSocket!当前人数为" + onlineNum);
        System.out.println(userId + "加入webSocket!当前人数为" + sessionPools);
        log.warn("---------------------上线了----------------------"+userId);
        // 广播上线消息

    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);
        // 广播下线消息

    }

    //收到客户端信息后,根据接收人的username把消息推下去或者群发
    // to=-1群发消息
    @OnMessage
    public void onMessage(String userId) throws IOException{
        System.out.println("server get" + userId);

    }

    //错误时调用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("发生错误");
        throwable.printStackTrace();
    }

    public static void addOnlineCount(){
        onlineNum.incrementAndGet();
    }

    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }

    public static AtomicInteger getOnlineNumber() {
        return onlineNum;
    }

    public static ConcurrentHashMap<String, Session> getSessionPools() {
        return sessionPools;
    }
}

如何调用

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void add(BizBookingAddParam bizBookingAddParam) throws IOException {
        BizBooking bizBooking = BeanUtil.toBean(bizBookingAddParam, BizBooking.class);
        SaBaseClientLoginUser clientLoginUser = StpClientLoginUserUtil.getClientLoginUser();
        QueryWrapper<BizBooking>queryWrapper=new QueryWrapper<>();
        queryWrapper.lambda().eq(BizBooking::getCreateUser,clientLoginUser.getId());
        queryWrapper.lambda().eq(BizBooking::getDoctorId,bizBooking.getDoctorId());
        queryWrapper.lambda().eq(BizBooking::getStatus,"0");
        List<BizBooking> list = this.list(queryWrapper);
//        if (list.size()>0){
//            throw new CommonException("您已邀请该医生");
//        }
        JSONObject obj = new JSONObject();
        obj.put("type", "newUnderSeeBooking");//业务类型
        obj.put("msgCreatePatient", clientLoginUser.getId());//消息内容
        //全体发送
//        webSocket.sendAllMessage(obj.toJSONString());
        //单个用户发送 (userId为用户id)
        webSocketServer.sendInfo(bizBooking.getDoctorId(),obj.toJSONString());
        //多个用户发送 (userIds为多个用户id,逗号‘,’分隔)
//        webSocket.sendMoreMessage(userIds, obj.toJSONString());
        this.save(bizBooking);
    }

参考文章

websocket详解(心跳检测+断线重连+useWebSocket源码解析+边界处理)WebSocket 是基于 TC - 掘金