一文了解 WebSocket

为什么需要webSocket

Http的单向性缺陷

HTTP 基于请求-响应模式,服务器无法主动推送数据,导致实时应用(如股票行情,即时聊天)需要客户端频繁轮询(Polling),浪费带宽和服务器资源

传统轮询的(Polling)的效率问题

  • 短轮询:客户端定期发送请求,即使数据未更新也会接受冗余响应,导致不必要的网络传输和CPU消耗
  • 长轮询(Long Polling):虽减少空响应(新数据到来时才发送响应),但仍有延迟(最快2×RTT),且高频请求仍占用资源

头部开销与低效传输

HTTP头部庞大(可达400+字节),而实际数据可能很小(如10字节),周期性传输大量大部信息浪费带宽。

WebSocket的优势

  • 双向实时通讯:建立连接后,服务端和客户端可随时主动推送数据,实现低延迟交互
  • 轻量级协议:连接建立后,数据传输头部仅2-10字节,显著减少开销
  • 持久连接:避免重复建立连接的开销,适合高频数据的交换场景

总结:

WebSocket弥补了HTTP在实时性,双向通信和传输效率的不足,成为现代web应用(如在线游戏,实时监控,协作工具)的理想选择

OSI模型与TCP/IP

OSI模型的说明

  • 定义:OSI(open system interconnection)模型是由国际标准化组织(ISO)提出的概念性框架,用于标准化不同计算机系统之间的通信。它将网络通信划分为7层,每一层负责特定的功能,并通过接口与相邻层交互
  • 7层结构及功能
层数 名称 主要功能
7 应用层 提供用户接口,支持应用程序访问网络服务(HTTP,WS)
6 表示层 数据格式转换(加密,压缩,编码等)确保不同系统能正确解析数据
5 会话层 建立,管理或终止应用程序之间的对话
4 传输层 提供端到端的可靠数据传输(tcp)或不可靠但高效的传输UDP
3 网络层 负责IP和路由选择,确保数据包跨网络传输
2 数据链路层 在物理网络上传输数据帧,进行错误检测和流量控制
1 物理层 定义电压和光信号等机械特性,负责比特流的传输

而TCP/IP协议可以看作OSI模型的一种简化

  • 定义:TCP/IP是互联网的实际通信标准,采用4层结构,比OSI更简化,但功能覆盖OSI的多个层
  • 4层结构及对应的OSI
TCP/IP层 对应的OSI层 主要功能
应用层 应用层,表示层,会话层 提供应用程序的通信接口
传输层 传输层 提供端到端的可靠数据传输(tcp)或不可靠但高效的传输UDP
网络层 网络层 负责IP和路由选择,确保数据包跨网络传输
网络接口层 数据链路层,物理层 管理物理网络连接,如以太网,Wi-Fi等,处理比特流和帧的传输
  • 特点:

    • 互联网(如HTTP,IP,TCP)均基于 TCP/IP 协议栈
    • 更简化:合并了 OSI 的高层(应用,表示,会话)和底层(数据链路层和物理层
    • 灵活高效:OSI过于复杂,而TCP/IP更简洁,适合实际部署

总结

  1. OSI是理论模型,帮助理解网络通信的分层协议
  2. TCP/IP是实际协议栈,支撑现代互联网的运行
  3. 两者关系:TCP/IP可视为OSI的简化版,但层间界限更模糊,更注重实用性

HTTP与WebSocket 的关系

HTTP,websocket等应用层协议,都是基于TCP协议来传输数据的,我们可以把这些高级协议理解成对 TCP的封装

对于 WebSocket 来说,它必须依赖HTTP协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与HTTP无关了

简单来说,WS由两部分组成:握手 数据传输

握手

  1. 客户端请求握手
arduino 复制代码
GET /chat HTTP/1.1            //1
Host: server.example.com      //2
Upgrade: websocket            //3
Connection: Upgrade           //4
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //5
Origin: http://example.com    //6
Sec-WebSocket-Protocol: chat, superchat //7
Sec-WebSocket-Version: 13     //8

逐行解析:

  1. 请求行:使用HTTP get方法,请求路径为/chat,HTTP版本为1.1
  2. Host头:指定服务器域名
  3. Upgrade头:表明客户端希望升级到web socket 协议
  4. Connection头:值为 upgrade,表明这是一个协议升级请求
  5. Sec-Web-Socket-key:Base64编码的随机16字节值,用于安全认证
  6. Origin头:用于防止跨站攻击,标明请求来源
  7. Sec-WebSocket-Protocol:客户端支持的子协议列表
  8. Sec-WebSocket-Version:指定webSocket协议版本
  1. 服务器响应
arduino 复制代码
HTTP/1.1 101 Switching Protocols //1
Upgrade: websocket              //2
Connection: Upgrade              //3
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  //4
Sec-WebSocket-Protocol: chat     //5

逐行解析:

  1. 状态行:101状态码表示协议切换成功
  2. Upgrade头:确认升级到 webSocket协议
  3. Connection头:确认连接已升级
  4. Sec-WebSocket-Accept:服务器对客户端key的验证响应
  5. Sec-WebSocket-Protocol:服务器选择的子协议
  1. 关键机制说明:
  • 协议升级:通过HTTP upgrade机制实现从HTTP到WebSocket的转换

  • 安全验证:

    • 客户端发送随机key
    • 服务器返回计算后的 Accept 值
    • 防止恶意或非WebSocket连接
  • 子协议协商:

    • 客户端列出支持的协议(如chat,superchat)
    • 服务器选择其中一个或拒绝
  • 版本控制:

    • 通过version字段确保双方使用兼容的协议版本
  • 代理兼容性

    • 使用HTTP兼容的握手方式
    • 通过Connection头确保代理正确处理

握手成功后,TCP连接保持打开字段,双方可以开始WebSocket数据帧的交换,实现全双工的通信

WebSocket协议Url:

ws协议默认使用80端口,wss协议默认使用443端口:

ini 复制代码
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
 
host = <host, defined in [RFC3986], Section 3.2.2>
port = <port, defined in [RFC3986], Section 3.2.3>
path = <path-abempty, defined in [RFC3986], Section 3.3>
query = <query, defined in [RFC3986], Section 3.4>

注:wss协议是WebSocket使用SSL/TLS加密后的协议,类似于HTTP/HTTPS

WebSocket连接准备前的的准备工作:

  1. 连接建立的基本流程:

客户端握手的小要求

  1. 基本要求:

    1. 请求格式:必须是符合RFC 2616(HTTP/1.1)定义的HTTP请求消息
    2. 请求方法:必须是get
    3. HTTP版本:必须大于1.1
  2. 必包含的头字段

    1. host字段:指定服务器的域名
    2. Upgrade:值必须包含websocket关键字
    3. Connection头字段:值必须包含upgrade指令
    4. Sec-WebSocket-Key:值是16字节随机数的Base64编码
    5. Sec-WebSocket-Key:头字段值必须是13,表示WebSocket协议版本是13
  3. 条件必须的头字段

    1. origin头字段:如果客户端时浏览器则必须包含

WebSocket数据帧详解

  1. 发送数据的基本原则

    1. 数据帧的处理要求:

      1. 客户端发送的帧:必须进行掩码处理
      2. 服务端发送的帧:不能进行掩码处理
      3. 违规处理:如果违反上述规则,对方应当发送关闭帧终止连接
    2. 帧的基本组成:

      1. 帧类型标识码(Opcode)
      2. 负载长度(payload length)
      3. 负载内容(payload):包括扩展数据和应用数据
  2. 帧类型

    1. 帧类型由4位Opcode表示,收到Opcode应当立即断开连接

    2. 数据帧类型

      1. 0x0-继续帧:表示当前帧是消息片段,需要与上一个非0x0帧拼接,常用于大消息分片传输
      2. 0x1-文本帧:携带UTF-8编码的文本数据
      3. 0x2-二进制帧:图片,音频等任意二进制数据
      4. 0x3-0x7-保留值:未未来非控制帧预留
    3. 控制帧类型

      1. 0x8-关闭连接帧:
      2. 0x9-ping帧:内容可以是任意数据,用于心跳检测/保活机制
      3. 0xA-pong帧:对ping的响应,内容应与ping帧相同
      4. 0xB-0xF保留值:位未来控制帧预留
  3. 帧格式概述

    1. 基础头部(2字节):

      1. FIN(一位):是否为最终帧
      2. RSV1-3(扩展用)
      3. Opcode(4位):帧类型
      4. Mask(1位):是否掩码
      5. Payload(7位):基础长度
    2. 扩展长度(可选)

    3. 掩码密钥:4字节,仅客户端发送时存在

    4. 有效载荷数据:

      1. 扩展数据:如协商的压缩数据
      2. 应用数据:实际传输内容
  4. 示例

    1. 大文件分片传输:
    ini 复制代码
     Frame1: FIN=0, Opcode=2, Payload=[二进制数据第一部分]
     Frame2: FIN=0, Opcode=0, Payload=[第二部分]
     Frame3: FIN=1, Opcode=0, Payload=[最后部分]
     

项目应用方面的思考

思考一:WebSocket 的连接建立过程,以及在前端代码中如何检测连接状态和处理断线重连

  1. 首先,前端通过 WebSocket API 创建连接

    ini 复制代码
     const ws = new WebSocket('ws://localhost:8080');
  2. 连接确立后,WebSocket 会触发onopen事件,表示连接成功。

  3. 在项目中,我们可以自定义一个Hook用来统一管理这些连接状态:

    1. onopen:连接成功
    2. onclose:连接关闭
    3. onerror:连接异常
    4. onmessage:连接异常
    ini 复制代码
     ws.onopen = () => setStatus('connected');
     ws.onclose = () => setStatus('disconnected');
     ws.onerror = () => setStatus('error');
  4. 短线重连处理:当检测到oncloseonerror 时,可以重新设置定时器尝试重新连接;或者我们可以用 useEffect 监听状态变化,实现自动重连

    scss 复制代码
     ws.onclose = () => {
       setStatus('disconnected');
       setTimeout(() => {
         // 重新创建 WebSocket 实例
         reconnect();
       }, 3000);
     }; 

思考二:在 React 项目中,如何优雅地管理 WebSocket 的生命周期

思路:用 useEffect 钩子来处理连接建立与关闭问题,用 useState 钩子来管理 WebSocket 的连接状态,用 useRef 来保存 WebSocket 的实力,避免不必要的组件重新渲染,并利用 useCallback 优化回调函数。

代码示例如下:

ini 复制代码
import { useEffect, useRef, useState, useCallback } from 'react';

export const useWebSocket = (url) => {
  const socketRef = useRef(null);
  const [state, setState] = useState({
    data: null,
    isConnected: false,
    error: null,
  });

  // 处理消息的回调函数
  const handleMessage = useCallback((event) => {
    try {
      // 尝试解析JSON数据,如果失败则按原始数据处理
      const parsedData = JSON.parse(event.data);
      setState(prevState => ({ ...prevState, data: parsedData }));
    } catch (e) {
      console.error('Failed to parse message data:', e);
      setState(prevState => ({ ...prevState, data: event.data, error: null }));
    }
  }, []);

  useEffect(() => {
    // 1. 在组件挂载时建立连接
    const socket = new WebSocket(url);
    socketRef.current = socket;

    // 监听连接打开事件
    socket.onopen = () => {
      console.log('WebSocket 连接成功');
      setState(prevState => ({ ...prevState, isConnected: true, error: null }));
    };

    // 监听消息事件
    socket.onmessage = handleMessage;

    // 监听连接关闭事件
    socket.onclose = () => {
      console.log('WebSocket 连接已关闭');
      setState(prevState => ({ ...prevState, isConnected: false, error: null }));
    };

    // 监听错误事件
    socket.onerror = (event) => {
      console.error('WebSocket 连接错误:', event);
      setState(prevState => ({ ...prevState, isConnected: false, error: event }));
    };

    // 2. useEffect 的清理函数:在组件卸载时执行
    return () => {
      if (socketRef.current) {
        // 优雅地关闭连接
        socketRef.current.close();
        console.log('WebSocket 连接已清理');
      }
    };
  }, [url, handleMessage]);

  // 发送消息的方法
  const sendMessage = useCallback((message) => {
    if (socketRef.current?.readyState === WebSocket.OPEN) {
      // 如果消息是对象,则将其转换为 JSON 字符串
      const dataToSend = typeof message === 'string' ? message : JSON.stringify(message);
      socketRef.current.send(dataToSend);
    } else {
      console.error('WebSocket is not connected. Message not sent.');
    }
  }, []);

  return {
    ...state,
    sendMessage,
  };
};

思考三:如何保证 WebSocket 通信的安全性

  1. 使用 wss 协议:使用了 TLS/SSL 加密,与 HTTPS 类似。
  2. 身份验证与授权:在 WebSocket 握手时,在协议头中携带一个认证令牌 token ,服务器在接受连接前会验证这个 token 的有效性,可以防止未经授权的用户连接到 WebSocket 服务
  3. 限制连接来源(CORS):服务端可以检查 WebSocket 请求的 Origin 头,只允许来自特定域名,防止恶意网站的攻击。
相关推荐
练习时长一年2 小时前
Spring代理的特点
java·前端·spring
水星记_3 小时前
时间轴组件开发:实现灵活的时间范围选择
前端·vue
2501_930124703 小时前
Linux之Shell编程(三)流程控制
linux·前端·chrome
潘小安3 小时前
『译』React useEffect:早知道这些调试技巧就好了
前端·react.js·面试
@大迁世界4 小时前
告别 React 中丑陋的导入路径,借助 Vite 的魔法
前端·javascript·react.js·前端框架·ecmascript
EndingCoder4 小时前
Electron Fiddle:快速实验与原型开发
前端·javascript·electron·前端框架
EndingCoder4 小时前
Electron 进程模型:主进程与渲染进程详解
前端·javascript·electron·前端框架
Nicholas684 小时前
flutter滚动视图之ScrollNotificationObserve源码解析(十)
前端
@菜菜_达5 小时前
CSS scale函数详解
前端·css
想起你的日子5 小时前
Vue2+Element 初学
前端·javascript·vue.js