WebSocket指南:从原理到生产环境实战

一、为什么选 WebSocket ?

WebSocket是一个在单个TCP连接上实现全双工通信的网络协议,专为web应用设计,由HTML5规范引入前端开发。

  • 全双工 :客户端和服务端可以同时独立发送和接收数据,不需要像HTTP/HTTPS协议那样只能单次请求-响应
  • 持久连接:一旦连接建立之后就会保持连接状态,除非有一端取消连接,避免了HTTP的频繁握手开销,适用于实时聊天、需实时获取数据状态等场景
  • 基于HTTP升级:连接通过HTTP/HTTPS的**"Upgrade"机制**建立(即websocket握手),初始使用ws://或wss://协议

适用场景:实时聊天、在线协作、多人游戏、直播弹幕等需要低延迟、高频率使用的场景

通信模式对比

  • 单工:数据只能单向传输,如服务端推送(SSE)、
  • 半双工:数据可以双向传输、但不能同时进行,如常规的REST API、Fetch/XMLHttpRequest
  • 全双工:数据可以双向同时传输,如WebSocket、WebRTC

本文以小编在一个在线代码评测系统使用websocket来实时获取用户代码的评测信息功能,来介绍小编对于websocket的理解!

二、WebSocket握手与协议升级

"Upgrade"机制 是一种协议协商方式,允许客户端和服务器在已有HTTP连接的基础上,将通信协议从HTTP切换为另一种协议WebSocket。这一过程又被称为协议升级,所以发送websocket请求也可以理解为一次协议升级的过程,具体过程包括:

  1. 客户端发起websocket请求,该请求的类型会显示为websocket请求,请求头通常包含几个upgrade的关键字段,如下图所示为ws连接的请求头:

2. 如果服务器支持websocket协议并同意升级,他就会返回一个101 Switching Protocols 状态码,并且包含特定的响应头:

3. 一旦客户端收到101响应,TCP连接就会从HTTP模式切换到WebSocket模式,就会发生以下变化:

  • 后续所有的数据都以WebSocket帧(frame,如下图)格式传输(不再是原来的HTTP提交)
  • 双方可以随时主动发送消息(全双工)
  • 连接保持打开,直到一方发送关闭帧或连接中断。在小编对接的过程中,由于后端同学并没有在沙箱判题结束之后发送关闭帧主动关闭连接通道,所以小编采用的是判断最后一帧信息的状态来主动关闭连接。

简言之:WebSocket 不是全新连接,而是"借用"HTTP连接完成一次安全握手后,将通道"改造"为实时通信管道。

三、快速上手-基础语法

1、创建 WebSocket 连接

javascript 复制代码
const socket = new WebSocket('ws://localhost:8080'); // 传入后端给你的ws连接地址

2、Websocket 的四个事件

  • onopen:连接建立时触发
  • onmessage:收到服务器消息触发
  • onerror:连接发生错误时触发
  • onclose:连接关闭时触发
javascript 复制代码
socket.onopen = function() {
  console.log('连接建立');
  // socket.send('Hello Server!'); 一般在连接建立成功之后就可以给服务端发消息了
};

socket.onmessage = function(event) {
  console.log('收到消息:', event.data);
  // event.data 是服务器发送的数据
};

socket.onerror = function(error) {
  console.error('错误:', error);
};

socket.onclose = function(event) {
  console.log('连接关闭', event);
};

3、发送消息

javascript 复制代码
socket.send('Hello Server!');

4、关闭连接

javascript 复制代码
socket.close();

5、四种状态

websocket有四种只读的状态:

javascript 复制代码
// WebSocket 的四种状态常量
WebSocket.CONNECTING = 0; // 连接中
WebSocket.OPEN = 1; // 连接已打开,可以通信
WebSocket.CLOSING = 2; // 连接正在关闭中
WebSocket.CLOSED = 3; // 连接已关闭或无法打开

// 实例的readyState属性为当前的状态,使用当前状态和四个状态常量对比即可的得到不同的判断条件
// 如
if(socket.readyState === WebSocket.OPEN){
  // 检测连接正常时触发
}

四、具体实战例子

以下示例为小编自己的项目在通过WebSocket实时获取沙箱判题状态时的实现👇

scss 复制代码
用户提交代码
↓
创建提交记录 (HTTP POST)
↓
建立 WebSocket 连接
↓
接收实时消息 → 更新状态 → UI 刷新
↓
收到 final_result → 自动关闭连接
↓
显示最终结果

1、用户点击提交按钮

用户点击提交之后,首先调用提交代码接口,此时服务端就会启动一个沙箱服务,将用户编写的代码作为沙箱的程序代码,并自动运行测试用例至沙箱内,然后响应本次提交的ID至客户端,客户端立即基于提交ID建立与服务端的WebSocket连接,实时获取沙箱程序的运行状态展示到页面中!

typescript 复制代码
// 调用提交代码接口(传递题目id、代码、语言)
async create(problemId: number, code: string, language: string) {
  // 1. 发送 HTTP 请求创建提交记录
  const response = await api.post('/submit', {
    problem_id: problemId,
    code,
    language
  })
  const submit = response.data

  // 2. 自动建立 WebSocket 连接监听结果(传递响应的提交id)
  this.setupWebSocketConnection(submit.id)
}

2、Websocket连接管理

typescript 复制代码
// 声明一个建立连接类,在类内部再声明一个connect连接方法
class SubmitWebSocket {
  // 建立连接方法
  async connect(): Promise<void> {
    const url = `${baseUrl}/ws/submit/${this.submitId}?token=${this.token}`
    // 1. 实例化一个websocket
    this.ws = new WebSocket(url)

    // 2. 设置消息监听器
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data) as WebSocketMessage
      this.onMessage?.(message) // 触发回调函数
    }
  }
}

3、实时消息处理

typescript 复制代码
// 在 store 中处理 WebSocket 消息
private setupWebSocketConnection(submitId: string) {
  const websocket = new SubmitWebSocket(submitId, this.token)

  websocket.onMessage = (message: WebSocketMessage) => {
    // 根据消息类型更新状态
    switch (message.type) {
      case 'status_update':
        this.currentSubmit!.status = message.data.status
        break
      case 'test_case_result':
        // 更新测试用例结果
        break
      case 'final_result':
        this.currentSubmit!.result = message.data
        websocket.close() // 收到最终结果后关闭连接
        break
    }
  }
}

五、生产环境避坑(重点)

小编在完成这个功能之后,发现开发环境正常 ,但是生产环境会报错无法升级HTTP->WebSocket ,经过一个老师的提醒,发现如果使用nginx来部署前端应用,我们还要完成nginx的相关配置才能使用WebSocket功能,具体配置如下:

nginx 复制代码
server {
  listen 80;
  server_name http://your-domain.com;

  location /ws/ {
    proxy_pass http://localhost:8080; # 你的 WebSocket 服务器地址

    # WebSocket 必需的三行配置
    proxy_http_version 1.1; # 必需:HTTP 1.1 支持协议升级
    proxy_set_header Upgrade $http_upgrade; # 必需:处理升级头
    proxy_set_header Connection "upgrade"; # 必需:设置连接类型
    proxy_read_timeout 3600s; # 重要:长连接超时

    # 可选的基础头信息
    proxy_set_header Host $host;
  }

  # 其它配置...
}

那么为什么开发环境正常,生产环境却需要进行websocket的相关配置呢?

  • 开发环境
scss 复制代码
前端 (http://localhost:3000)
↓ 直接连接
后端 WebSocket (后端ws接口地址)

开发环境下前端直连后端服务,不经过Nginx的反向代理,所以可以直接升级为WebSocket连接

  • 生产环境(需要Nginx代理)
less 复制代码
用户浏览器 → Nginx (https://your-domain.com) → 多个后端服务器

WebSocket 使用 HTTP 协议升级机制,在生产环境下前端调用接口服务需要经过 Nginx代理,所以Nginx需要正确处理这个升级过程

以上是小编通过自己在websocket功能的实现过程中总结出来的一些东西,如果有误请各位大佬指出,感激不尽!

六、状态管理和重连机制

websocket的连接有时候会意外断开(网络问题、服务器故障等等),为了保证用户体验,对于意外断开的连接一般情况下会有重连机制进行处理

1. 简单实现

声明一个标志变量isManualClose表示是否为正常关闭连接,在用户主动关闭连接事件中设置其值为true,其余情况下皆为false,在连接关闭事件中据此值来决定是否重连

javascript 复制代码
let isManualClose = false; // 默认不是手动关闭

ws.onclose = (event) => {
  if (!isManualClose) {
    // 只有意外断开才重连
    startReconnect();
  }
};

// 用户主动断开连接
disconnectButton.onclick = () => {
  isManualClose = true; // ⭐ 标记为手动关闭
  ws.close(); // 现在 onclose 不会触发重连
};

// 用户跳转到其他页面,主动关闭WebSocket
window.addEventListener('beforeunload', () => {
  isManualClose = true;
  ws.close();
});

isManualClose的值可以有很多标识和判断来决定,小编觉得具体的可以与后端的同学一起来制定约束

2. 指数退避+有限尝试

利用指数退避算法来计算重连的间隔,并设置最大间隔来防止客户端无限重连(浪费带宽)。根据业务场景,如需要的话还可以设置最大重连次数,当达到最大重连次数时,停止重连逻辑(有限尝试),思路与指数退避是类似的!

javascript 复制代码
// 关键参数
let reconnectInterval = 1000; // 初始间隔:1秒
let maxReconnectInterval = 30000; // 最大间隔:30秒
let reconnectDecay = 1.5; // 增长倍数:1.5倍
let reconnectAttempts = 0; // 重连次数
let maxAttempts = 50; // 最大尝试次数

// 每次重连时计算新间隔
reconnectInterval = Math.min(reconnectInterval * reconnectDecay, maxReconnectInterval);

// 第1次重连: 1秒后
// 第2次重连: 1.5秒后 (1000 × 1.5)
// 第3次重连: 2.25秒后 (1500 × 1.5)
// 第4次重连: 3.375秒后 (2250 × 1.5)
// ...
// 直到达到最大30秒间隔
// 当间隔达到30秒时,重连频率很低,相当于软限制

// 判断是否需要重连
shouldReconnect() {
  return !this.isManualClose && this.reconnectAttempts < this.maxAttempts;
}

// 计算下一次重连间隔
calculateNextInterval() {
  // 基础指数退避计算
  reconnectInterval = Math.min(reconnectInterval * reconnectDecay, maxReconnectInterval);
  return reconnectInterval
}

// 启动重连
startReconnect(){
  if (!this.shouldReconnect()) {
    console.log('不满足重连条件,停止重连');
    return;
  }
  // 避免重复启动(每次重连先清理上一次的定时器)
  if (this.reconnectTimer) {
    clearTimeout(this.reconnectTimer);
  }
  // 更新重连状态
  this.reconnectAttempts++;this.isReconnecting = true;
  // 计算下一次重连间隔(指数退避)
  this.currentInterval = this.calculateNextInterval();

  // 设置重连定时器
  this.reconnectTimer = setTimeout(() => {
    this.executeReconnect(); // 需要重新建立ws连接
  },this.reconnectInterval);
}

socket.onclose = (event) => {
  if (shouldReconnect()) {
    // 只有意外断开才重连
    startReconnect();
  }
};

socket.onopen = function(event) {
  // 连接成功时重置所有重连参数
  reconnectInterval = 1000;
  reconnectAttempts = 0;
};

七、心跳机制

websocket的心跳机制作用于检测连接是否真实存活(避免假连接,适用于生产环境),并在连接断开时及时发现。为什么需要心跳?

  • WebSocket 基于 TCP,但 TCP Keep-Alive 默认关闭或周期极长
  • 中间设备(如我们使用的Nginx代理)一般会设置空闲超时
  • 若应用层长时间无数据传输,连接会被静默断开,而客户端毫无感知

以下为基于Ping-Pong模型实现的心跳机制简易版:

javascript 复制代码
let heartbeatInterval; // 心跳定时器
let heartbeatIntervalTime = 30000; // 心跳间隔时间(毫秒),默认30秒
// 注意:心跳间隔 < min(防火墙超时, 服务端超时, 客户端容忍延迟) / 2 ,否则无效
let heartbeatTimeout; // 心跳超时定时器
let heartbeatTimeoutTime = 5000; // 心跳超时时间(毫秒),默认5秒

socket.onopen = (event) => {
  // 其余代码...
  // 在连接成功之后启动心跳机制
  startHeartbeat();
}

// 收到服务器消息时触发判断
socket.onmessage = (event) => {
  // 处理心跳响应
  if(event.data === 'pong'){
    console.log('收到心跳响应!');
    if(heartbeatTimeout){
      clearTimeout(heartbeatTimeout);
    }
    return
  }
  // 其它代码...
}

// 连接关闭时触发
socket.onclose = (event) => {
  // 其它代码...
  // 停止心跳机制
  stopHeartbeat();
  // 判断是否需要重连...
}

// 发生错误时触发
socket.onerror = (event) => {
  // other...
  // 停止心跳机制
  stopHeartbeat();
}

// 启动心跳机制
startHeartbeat() {
  // 清除之前的心跳定时器
  if (heartbeatInterval) {
    clearInterval(heartbeatInterval);
  }
  // 设置心跳定时器,定期发送心跳消息
  heartbeatInterval = setInterval(function() {
    if (socket && socket.readyState === WebSocket.OPEN) {
      socket.send('ping');
      // 设置心跳超时定时器(目的是检测是否收到pong)
      heartbeatTimeout = setTimeout(function() {
        console.log('心跳超时,连接可能已断开');
        addMessage('心跳超时,连接可能已断开', 'received');
        // 主动关闭连接以触发重连机制
        if (socket) {
          socket.close();
        }
      }, heartbeatTimeoutTime);
    }
  }, heartbeatIntervalTime);
}

// 停止心跳机制
stopHeartbeat() {
  if (heartbeatInterval) {
    clearInterval(heartbeatInterval);
    heartbeatInterval = null;
  }
  if (heartbeatTimeout) {
    clearTimeout(heartbeatTimeout);
    heartbeatTimeout = null;
  }
}

八、消息队列与流量控制

如果你使用的websocket涉及到大量、密集的消息需要同段事件发送,为了防止网络拥塞,可以通过消息队列来进行缓冲,将待发送的消息推入消息队列缓冲,以此保证消息数据在网络拥塞/连接断开的时候不会丢失。

消息队列的工作流程大致为:

复制代码
应用层发送消息
↓
检查连接状态
↓
连接可用 → 立即发送
连接不可用 → 加入队列
↓
监听连接恢复事件
↓
连接恢复 → 按顺序发送队列中消息
↓
发送成功 → 从队列移除
发送失败 → 标记重试或丢弃
javascript 复制代码
let messageQueue = []; // 消息队列
let isSending = false; // 是否正在发送消息
let sendRateLimit = 10; // 每秒最大发送消息数
let sendInterval = 1000 / sendRateLimit; // 发送间隔(毫秒)
let sendTimer = null; // 发送定时器

socket.onopen = (event) => {
  // 其余代码...
  // 开始处理消息队列
  processMessageQueue();
}

// 连接关闭时触发
socket.onclose = (event) => {
  // 其它代码...
  // 停止消息推送
  stopMessageSending();
  // 判断是否需要重连...
}

// 发生错误时触发
socket.onerror = (event) => {
  // other...
  // 停止消息推送
  stopMessageSending();
}

// 将消息添加到队列
enqueueMessage(message, priority = 0) {
  // 创建消息对象
  const messageObj = {
    content: message,
    priority: priority, // 优先级,数值越大优先级越高
    timestamp: Date.now() // 时间戳
  };

  //if (priority > 0) {
  // //根据优先级插入消息队列
  //} else {
  // // 普通消息添加到队列末尾
  //}

  messageQueue.push(messageObj);
}

// 处理消息队列
processMessageQueue() {
  if (sendTimer) {
    clearInterval(sendTimer);
  }
  // 定时处理消息队列
  sendTimer = setInterval(function() {
    if (messageQueue.length > 0 && socket && socket.readyState === WebSocket.OPEN) {
      // 取出队列中的第一条消息
      const messageObj = messageQueue.shift();
      socket.send(messageObj.content);
    }
  }, sendInterval);
}

// 停止消息发送
stopMessageSending() {
  if (sendTimer) {
    clearInterval(sendTimer);
    sendTimer = null;
  }
}

// 设置发送速率(每秒消息数)
setSendRate(rate) {
  if (rate > 0 && rate <= 100) {
    sendRateLimit = rate;
    sendInterval = 1000 / sendRateLimit;

    // 重新启动消息处理定时器
    if (socket && socket.readyState === WebSocket.OPEN) {
      processMessageQueue();
    }
  }
}

本文到此结束,文中的理解和总结难免有不足之处,如有纰漏或错误,请大家直接指出,小编虚心求教!

相关推荐
不说别的就是很菜2 小时前
【前端面试】Git篇
前端·git
欧阳码农2 小时前
盘点这两年我接触过的副业赚钱赛道,对于你来说可能是信息差
前端·人工智能·后端
恋猫de小郭2 小时前
Dart 3.10 发布,快来看有什么更新吧
android·前端·flutter
q***47182 小时前
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
java·前端·spring
小光学长2 小时前
基于Web的课前问题导入系统pn8lj4ii(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库
Mintopia3 小时前
🌐 跨模态迁移学习:WebAIGC多场景适配的未来技术核心
前端·javascript·aigc
JarvanMo3 小时前
使用 MediaPipe 在 Flutter web 中识别姿势
前端
saadiya~3 小时前
基于 Vue3 封装大华 RTSP 回放视频组件(PlayerControl.js 实现)
前端·vue3·大华视频相机前端播放
LSL666_3 小时前
spring多配置文件
java·服务器·前端·spring