首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >关于Socket,看我这几篇就够了(三)原来你是这样的Websocket

关于Socket,看我这几篇就够了(三)原来你是这样的Websocket

作者头像
chouheiwa
发布2026-05-06 21:07:18
发布2026-05-06 21:07:18
1090
举报

在前面的文章中,我们聊了HTTP协议的基础知识。HTTP作为一个无状态、请求-响应模式的协议,在Web发展的早期阶段确实解决了很多问题。

回顾一下HTTP的演进历程:早期的HTTP/1.0每次请求都要重新建立TCP连接,这在网络开销上是相当昂贵的。想象一下,每发送一个简单的GET请求,都要经历TCP的三次握手和四次挥手,这对服务器资源来说是个不小的负担。

HTTP/1.1的出现带来了连接复用(Connection: keep-alive),让我们可以在同一个TCP连接上发送多个请求,这确实是个不错的改进。但即便如此,HTTP协议本身的一些限制依然存在。

HTTP协议的痛点

虽然HTTP协议经过多年发展已经相当成熟,但在某些场景下,它的局限性还是比较明显的:

1. 协议开销相对较重

在实际开发中,我们经常会遇到需要频繁请求数据的场景,比如实时监控、股票行情等。让我们来看一个简单的例子:

代码语言:javascript
复制
GET / HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n

这个最简单的GET请求总共42个字节,其中协议相关的内容就占了相当一部分。如果我们需要每秒发送多次这样的请求来获取实时数据,这些"额外"的字节累积起来就不容忽视了。

当然,这里说的"开销大"是相对的概念。对于传输大量数据的场景,这点开销可能微不足道;但对于只需要传输少量数据却需要高频通信的场景,这个比例就显得有些"浪费"了。

2. 单向通信的限制

HTTP的请求-响应模式有个天然的限制:服务器无法主动向客户端推送数据。这在很多实时应用场景下就显得力不从心了。

虽然我们可以通过轮询的方式来模拟"实时"效果,但这又回到了第一个问题——频繁的HTTP请求会带来不必要的开销。而且轮询的实时性也不够理想。

正是这些痛点,催生了WebSocket协议的诞生。

WebSocket:双向通信的解决方案

WebSocket是一个基于TCP的全双工通信协议。简单来说,就是让客户端和服务器都能随时主动发送数据给对方,而不需要等待对方先"问"。

这种设计很好地解决了HTTP的两个痛点:

  1. 1. 协议开销小:握手完成后,数据传输的协议头部非常精简
  2. 2. 真正的双向通信:服务器可以主动推送数据,客户端也可以随时发送

WebSocket的工作方式很巧妙:它先通过一个HTTP请求完成"握手",告诉服务器"我想升级到WebSocket协议",握手成功后就切换到WebSocket模式进行通信。这样既保证了兼容性,又获得了更高的效率。

协议详解

连接建立过程

WebSocket的连接建立是一个两步走的过程:首先是标准的TCP三次握手,然后是WebSocket特有的HTTP升级握手。

这个设计很有意思:WebSocket并没有重新发明轮子,而是巧妙地利用了现有的HTTP基础设施。这样做的好处是可以复用现有的代理、防火墙等网络设备,降低了部署的复杂度。

第一步:客户端发起升级请求

客户端发送一个特殊的HTTP请求,告诉服务器"我想升级到WebSocket":

代码语言:javascript
复制
GET /访问路径 HTTP/1.1\r\n 
Host: www.example.com\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Version: 13\r\n
Sec-WebSocket-Key: mgZ6+kXU1+mEgOXWDPPsBg==\r\n
\r\n

这个请求看起来就是个普通的HTTP GET请求,但几个关键的头部字段表明了它的特殊意图:

  • Connection: Upgrade - 告诉服务器"我想升级连接"
  • Upgrade: websocket - 具体要升级到WebSocket协议
  • Sec-WebSocket-Version: 13 - 使用WebSocket协议版本13(目前的标准版本)
  • Sec-WebSocket-Key - 一个随机生成的Base64字符串,用于后续的握手验证

这里的Sec-WebSocket-Key很有意思,它不是用来做安全认证的,而是用来确保服务器真的理解WebSocket协议。如果服务器只是一个普通的HTTP服务器,它可能会忽略这些特殊头部,但无法正确处理这个Key,客户端就能知道"这个服务器不支持WebSocket"。

第二步:服务器确认升级

如果服务器支持WebSocket,它会返回一个101状态码的响应:

代码语言:javascript
复制
HTTP/1.1 101 Switching Protocols\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Accept: qIs5tRK57T9vTjEtFfTLOSe3K3w=\r\n
\r\n

这个响应的关键是状态码101,表示"协议切换"。从这一刻开始,这个TCP连接就不再使用HTTP协议了,而是切换到WebSocket模式。

服务器需要返回一个Sec-WebSocket-Accept字段,这是对客户端发送的Sec-WebSocket-Key的"回应"。具体的计算方法是:

  1. 1. 将客户端的Key与固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
  2. 2. 对拼接结果进行SHA1哈希
  3. 3. 将哈希结果进行Base64编码

客户端收到响应后,会用同样的方法计算一遍,如果结果匹配,就确认握手成功。

握手完成

整个握手过程其实就是一次HTTP请求-响应,但它的意义重大:

  • • 确保了向后兼容性(可以通过现有的HTTP基础设施)
  • • 验证了双方都理解WebSocket协议
  • • 可以携带Cookie、认证信息等HTTP头部
  • • 完成后就切换到高效的WebSocket数据传输模式

到这里,握手阶段结束,真正的WebSocket通信才开始。接下来我们看看WebSocket是如何传输数据的。

数据传输:帧结构详解

握手完成后,WebSocket就进入了数据传输阶段。这里就能看出WebSocket相比HTTP的优势了。

WebSocket的数据传输以"帧"(Frame)为单位。每个帧都有一个精心设计的结构,既保证了功能的完整性,又尽可能地减少了开销。

WebSocket帧数据结构
WebSocket帧数据结构

WebSocket帧数据结构

从图中可以看到,WebSocket帧的头部信息非常紧凑。最小只需要2个字节,最大也不过14个字节(包含掩码的情况下)。相比HTTP每次请求都要几十个字节的头部,这个开销确实小了很多。

让我们来详细看看每个字段的作用:

FIN(1位)

这是帧的第一个位,用来标识这个帧是否是消息的最后一个片段:

  • • 0:还有后续帧
  • • 1:这是最后一个帧

这个设计支持了消息分片传输,对于大消息很有用。

RSV1-RSV3(各1位)

这三个位是为扩展预留的,目前必须为0。如果你在实现WebSocket客户端时收到了非0的RSV位,而你又不知道如何处理,那就应该关闭连接。

操作码(4位)

这4位定义了帧的类型,总共可以表示16种不同的帧类型:

数据帧(0x0-0x7):

  • 0x0:继续帧(用于分片消息的中间部分)
  • 0x1:文本帧(UTF-8编码的文本数据)
  • 0x2:二进制帧(任意二进制数据)
  • 0x3-0x7:保留给未来的数据帧类型

控制帧(0x8-0xF):

  • 0x8:关闭帧(用于关闭连接)
  • 0x9:Ping帧(心跳检测)
  • 0xA:Pong帧(对Ping的响应)
  • 0xB-0xF:保留给未来的控制帧类型

帧分为两大类:数据帧用于传输实际的业务数据,控制帧用于连接管理和心跳检测。

控制帧的特点

控制帧有几个重要特征:

  • • 数据长度不能超过125字节
  • • FIN位必须为1(不能分片)
  • • 用于连接管理,不是业务数据

关闭帧(0x8)

用于优雅地关闭连接。当一方想要关闭连接时,会发送关闭帧,对方收到后也应该回复一个关闭帧,然后双方关闭TCP连接。关闭帧可以携带关闭原因(错误码+描述文本)。

心跳机制(Ping/Pong)

  • Ping帧(0x9):用于检测连接是否还活着
  • Pong帧(0xA):对Ping的响应

这个机制很实用,特别是在移动网络环境下,可以及时发现连接断开的情况。

数据帧类型

数据帧是我们日常使用最多的,主要有三种:

文本帧(0x1) 传输UTF-8编码的文本数据,比如JSON、XML等。这是Web应用中最常用的类型。

二进制帧(0x2) 传输任意二进制数据,比如图片、文件、或者自定义的二进制协议数据。

继续帧(0x0) 用于消息分片。当一个消息太大时,可以分成多个帧发送:

  • • 第一个帧:使用文本帧或二进制帧,FIN=0
  • • 中间的帧:使用继续帧,FIN=0
  • • 最后一个帧:使用继续帧,FIN=1

这种设计让大消息的传输更加灵活,也支持了流式处理。

掩码机制

WebSocket有一个有趣的设计:客户端发送的数据必须进行掩码处理,而服务端发送的数据不需要。

掩码的作用 这不是为了加密,而是为了防止某些代理服务器的缓存污染攻击。通过掩码,确保WebSocket数据看起来是"随机"的,避免被误认为是HTTP缓存内容。

掩码算法 很简单的异或操作:

代码语言:javascript
复制
masked_data[i] = original_data[i] XOR mask_key[i % 4]

用4字节的掩码键,循环对数据进行异或。解码时再异或一次就能还原数据。

数据长度编码

WebSocket使用了一种巧妙的变长编码来表示数据长度:

7位基础长度

  • • 0-125:直接表示实际长度
  • • 126:表示长度需要额外的2字节来表示
  • • 127:表示长度需要额外的8字节来表示

扩展长度

  • • 当基础长度为126时:后续2字节(16位)表示实际长度,最大65535字节
  • • 当基础长度为127时:后续8字节(64位)表示实际长度,理论上无限制

这种设计很实用:小消息只用1字节表示长度,大消息才需要更多字节。所有多字节数值都使用网络字节序(大端序)。

掩码键与数据

掩码键(4字节) 当MASK位为1时存在,用于数据的异或运算。

载荷数据 包含两部分:

  • 扩展数据:协商的WebSocket扩展产生的数据(通常为空)
  • 应用数据:我们实际要传输的业务数据

大多数情况下,载荷数据就是我们的业务数据,比如JSON字符串或二进制文件。

实际应用场景

经过这么多年的发展,WebSocket已经在很多场景下得到了广泛应用:

什么时候选择WebSocket

适合的场景:

  • 实时聊天应用:消息需要双向实时传输
  • 在线游戏:需要低延迟的实时交互
  • 实时监控面板:服务器需要主动推送状态更新
  • 协作编辑:多人同时编辑文档,需要实时同步
  • 金融交易:股票价格、交易数据的实时推送

不太适合的场景:

  • 传统的CRUD操作:HTTP已经足够好用
  • 文件上传下载:HTTP的语义更清晰
  • SEO友好的网页:搜索引擎更喜欢HTTP

技术选型的一些思考

作为一个全栈开发者,我觉得选择技术方案时需要考虑几个维度:

  1. 1. 复杂度:WebSocket需要处理连接管理、重连、心跳等问题,比HTTP复杂
  2. 2. 基础设施:负载均衡、代理配置等可能需要额外考虑
  3. 3. 调试难度:WebSocket的调试比HTTP要困难一些
  4. 4. 团队熟悉度:团队对相关技术栈的掌握程度

写这篇文章的初衷

这些年在不同的项目中都有接触到WebSocket,从最初的iOS开发到后来的全栈开发,对这个协议的理解也在不断深化。希望通过这篇文章,能帮助大家更好地理解WebSocket的工作原理,在技术选型时做出更合适的决策。

总结

WebSocket作为一个相对成熟的协议,在合适的场景下确实能解决HTTP的一些痛点。但技术没有银弹,关键是要根据具体的业务需求来选择合适的方案。

希望这篇文章对你有所帮助。如果有任何问题或者不同的看法,欢迎交流讨论。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 猿族技术生活杂谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HTTP协议的痛点
    • 1. 协议开销相对较重
    • 2. 单向通信的限制
  • WebSocket:双向通信的解决方案
    • 协议详解
      • 连接建立过程
      • 数据传输:帧结构详解
      • 掩码机制
      • 数据长度编码
      • 掩码键与数据
  • 实际应用场景
    • 什么时候选择WebSocket
    • 技术选型的一些思考
    • 写这篇文章的初衷
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档