WebSocket 全双工通信

一句话总结

WebSocket 就是 HTTP 升级成的”长连接管道”——建立连接后,服务器和浏览器随时可以给对方发消息,不用像 HTTP 那样一问一答来回折腾。


一、先搞懂”为什么需要 WebSocket”

HTTP 的工作方式:

你打开一个网页,网页里有个股票价格。

HTTP 只能:你问 → 服务器答。

要显示实时价格怎么办?

方案一:轮询(Polling)

  • 浏览器每隔 1 秒发一次 HTTP 请求:“现在股价多少?”

  • 服务器返回:“100 块”

  • 1 秒后又问:“现在股价多少?” → 服务器返回:“101 块”

  • 问题:大部分请求是浪费的(价格又没变)。而且 1 秒的延迟对于实时数据已经很慢了。

方案二:长轮询(Long Polling)

  • 浏览器发请求:“股价变了告诉我”

  • 服务器不马上返回,一直等着。价格变了 → 才返回。

  • 浏览器收到后立刻发下一个请求。

  • 比轮询好一点,但每次还要新建 HTTP 连接,还是要发 HTTP 头部(几百字节的”废话”)。

都不好。

像什么?

  • 轮询 = 你每隔 1 分钟打电话给奶茶店:“奶茶好了没?” 一直问,问到好了为止。中间的大部分电话都是废话。
  • WebSocket = 你坐在店里,店员做好了直接喊你。“你的奶茶好了!” 你坐着等就行,不用反复问。店员做好也能叫你。你也能随时喊:“再加一杯!“

二、WebSocket 是什么

WebSocket 是建立在 TCP 之上的全双工通信协议。

什么叫全双工?

  • 双方可以同时发送数据

  • 不像 HTTP 的半双工:你发一句我回一句,不能同时说

  • 更不是单工:只能一方说一方听(收音机)

全双工的例子:

实时语音通话,你说话的同时也能听到对方说话。

WebSocket 就是这个道理,浏览器和服务器能同时发消息。

WebSocket 和 HTTP 的关系:

  • 连接是用 HTTP 升级过来的(这一下是 HTTP)

  • 升级成功后就不再用 HTTP 了(后面全是 WebSocket)

  • 一个连接,一直用,随时发。不需要反复建连接


三、WebSocket 连接建立过程


sequenceDiagram

    participant C as 客户端

    participant S as 服务器

    Note over C,S: ① HTTP 升级请求

    C->>S: GET /chat HTTP/1.1<br/>Host: example.com<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==<br/>Sec-WebSocket-Version: 13

    Note over C,S: 客户端生成的随机密钥

    Note over S: 告诉服务器我要升级到 WebSocket

    S->>C: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    Note over C,S: ② 服务器同意升级

    Note over C,S: ========== 从此以后是 WebSocket ==========

    Note over C,S: ③ 双向通信

    C->>S: 你可以随时发消息

    S->>C: 服务器随时可以发

    Note over C,S: ④ 关闭连接

    C->>S: Close frame

    S->>C: Close frame

关键点:

升级请求:看起来就是一个普通的 HTTP GET 请求。

区别是有 Upgrade: websocket 头部。

Sec-WebSocket-Key / Accept = 对暗号

你要加入一个秘密社团,社团的接头暗号有两种卡片。

  • 第一张卡是你要发的——上面写着一串随机数。这就是 Sec-WebSocket-Key。客户端生成一长串随机 Base64 字符串。

  • 第二张卡是接头人回的——他拿到你的随机数,加上只有真社团才知道的一句话,算出另一串数字给你。这就是 Sec-WebSocket-Accept。服务器把 Key 加上固定字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11,这是 RFC 6455 规定的公开常量),做 SHA-1 哈希,再 Base64 编码。

关键:中间如果有假接头人(缓存代理服务器),他算不出只有真社团才知道的那句话。他发回来的 Accept 就是错的。浏览器一验证不对,就知道对面是假服务器,立刻拒绝。

Key/Accept 不是为了加密。只是为了确保对面真的是懂 WebSocket 的服务器,不是缓存代理在乱回。之后真正的数据加密,靠的是 HTTPS(wss://)那一层的 TLS 加密。

101 状态码:

只有 WebSocket 升级响应用这个状态码。表示”协议切换”。

之后:

HTTP 连接已经升级成 WebSocket 连接。双方通过这个 TCP 连接直接收发 WebSocket 帧。不再有 HTTP 头部(省了很多带宽)。


四、WebSocket 的数据帧结构

什么是”帧”

WebSocket 不直接传原始数据,而是把数据装进一个一个的”帧”里。

帧 = 一个包装盒子。盒子上贴着标签(帧头),里面装着真正的数据(payload)。

就像你寄快递:

你寄的不是光溜溜的东西,而是放进纸箱(帧)里。

纸箱上贴着快递单(帧头):

  • “里面是啥?” → 文本还是图片
  • “多重?” → 数据长度
  • “这箱是不是最后一箱?” → 大数据拆成多箱,这是不是最后一箱
  • “谁发的?” → 浏览器发的还是服务器发的(影响要不要掩码)

有了包装,接收方就不用猜了。

来了一个帧,先看帧头,就知道怎么处理,不用等全部收完。

帧头(快递单)长什么样

帧头只占据 2~14 字节(取决于数据大小)。相比 HTTP 头部动不动几百字节,非常轻量。


packet-beta

0-0: "最后一块?"

1-3: "预留"

4-7: "类型"

8-8: "浏览器发的?"

9-15: "数据长度"

字段解释:

字段意思
FIN1 bit这块数据是不是最后一块? 大数据拆成多帧时,最后一块 FIN=1,前面的 FIN=0
RSV3 bits预留位,平时全是 0。扩展协议时用(比如 WebSocket 压缩)
Opcode4 bits里面是什么? 1 = 文本,2 = 二进制,8 = 关闭连接,9 = Ping,A = Pong
MASK1 bit这是浏览器发的吗? 浏览器→服务器必须=1,服务器→浏览器=0
Pay Len7 bits数据有多长?如果数据 > 125 字节,这个字段只告诉你”去查下一段”

那 7 位(最大 127)不够怎么办?

数据大小有三种情况:

数据大小帧头说明
≤ 125 字节2 字节数据直接跟在帧头后面
126~65535 字节(小图)4 字节Pay Len 写 126,后面还有 2 字节专门写长度
65536~2^63 字节(大文件)10 字节Pay Len 写 127,后面还有 8 字节专门写长度

像什么? 寄快递时:小东西 → 直接贴一张快递单(2 字节帧头)。大箱子 → 快递单上写”见附加单”,再加贴一张。超大箱子 → 再加更长的附加单。

为什么浏览器发的数据要”掩码”(MASK=1)

MASK 位 = 1,表示这个数据被”混淆”过。

混淆规则:

  • 浏览器在帧头里带上一段 4 字节的”掩码密钥”

  • 数据在发送前,跟掩码密钥做一次 XOR 运算

  • 服务器收到后,用同样的掩码密钥做一次 XOR,还原原始数据

问题是:为什么浏览器发的数据要混淆,服务器发的不用?

故事:2008 年左右,有个安全漏洞。你打开了一个恶意网页。这个网页的 JavaScript 给 WebSocket 服务器发了一个请求。但请求的数据被中间缓存代理看到了。这个缓存代理以为数据是一个 HTTP 响应(因为数据碰巧长得像 HTTP 头)。缓存代理把数据缓存起来了。之后其他用户访问同一个网站 → 拿到了这个”假响应”(缓存中毒)。

解决方案就是掩码:

  • 浏览器发出的所有数据都先做 XOR 混淆

  • 中间人看到的是乱码,不可能冒充 HTTP 响应

  • 服务器有掩码密钥,能还原

  • 缓存代理没法”碰巧看懂”了

所以 MASK=1 是”我来自浏览器”的标记。目的是保护中间缓存不被污染。不是为了加密(加密靠 TLS)。

再说 Opcode:为什么分文本和二进制?

计算机只能用二进制(0101),但 WebSocket 让你选择”我怎么理解这些数据”。

文本帧(opcode = 0x1):

  • 数据是 UTF-8 编码的字符串

  • WebSocket 会自动帮你解码成文字

  • 你收到的是可以直接读的”你好”

二进制帧(opcode = 0x2):

  • 数据是原始字节流

  • WebSocket 不做任何解码

  • 你收到的是 0101…,你自己决定怎么解析(图片、文件、ProtoBuf)

区别:

  • 文本帧 = 对方告诉你”这是中文,已经翻译好了”

  • 二进制帧 = 对方给你一堆原材料,你自己加工

实际使用中,聊天消息用文本帧(JSON),文件传输用二进制帧。


五、WebSocket 和 HTTP 的区别


flowchart LR

  subgraph HTTP["HTTP"]

    direction TB

    H1["一问一答"]

    H2["每次都带几百字节头部"]

    H3["只能客户端发请求"]

    H4["用完就断"]

  end

  subgraph WS["WebSocket"]

    direction TB

    W1["随时聊,双方都能主动"]

    W2["帧头只有 2~14 字节"]

    W3["服务器也能主动推"]

    W4["一条连接一直用"]

  end


六、WebSocket 的保活(心跳)

WebSocket 是长连接。长连接有个问题:连接可能被中间路由或防火墙断开。(你那边觉得还连着,实际上中间断了。)

怎么确认连接还活着?使用 Ping / Pong 帧

浏览器或服务器发送 Ping 帧。对方收到后回复 Pong 帧。

像什么? 你和朋友隔着墙聊天。你每隔一段时间喊一声”还在吗?“朋友回”在呢!“如果喊了没回应 → 要么朋友走了,要么墙倒了。

一般服务器每隔一段时间发送 Ping。一段时间内没收到 Pong → 判定连接断开 → 清理资源。客户端超时没收到 Pong → 认为网络出了问题 → 可以自动重连。


七、实际开发中需要注意的点

断线重连

WebSocket 连接会因为网络波动、服务器重启等原因断开。不处理断线的话,用户看起来”网页没反应了”。

常见做法:

  • 浏览器端检测到 oncloseonerror 事件

  • 等待 1 秒 → 重新连接

  • 连续失败 → 等待时间加倍(指数退避)

  • 最多重试 N 次 → 提示用户”网络异常”

心跳保活

如上一条所说。一般服务器 30 秒发一次 Ping。客户端 10 秒没收到 Pong → 判定断开 → 重连。

数据和业务层

WebSocket 只提供”数据通道”。它不关心你传输的是什么格式。

所以业务上需要自己定义协议:

  • 每条消息的格式是什么?

  • JSON 还是二进制?

  • 消息类型(登录、聊天、心跳、通知等)怎么区分?

比如:{ "type": "chat", "from": "user1", "content": "你好" }

服务端根据 type 字段判断怎么处理。


八、实际应用场景

实时聊天

  • 微信、Slack 等即时通讯。消息来了服务器直接推

  • 不需要用户刷新页面才能看到新消息

实时通知

  • 邮箱新邮件通知、系统告警

  • 服务器一有事件就推给浏览器

协同编辑

  • 多人同时编辑一个文档

  • A 改了内容 → WebSocket 推给 B 和 C,B 和 C 的页面实时更新

实时行情

  • 股票、加密货币价格

  • 每秒推送几十次价格变化,HTTP 轮询做不到这种频率

在线游戏

  • 回合制或实时对战

  • 服务器推对手的操作,你推自己的操作给服务器


九、和 SSE(Server-Sent Events)的区别

实践中可能会被问到。

SSE:

  • 也是 HTTP 上的长连接

  • 但是单工的——只能服务器给浏览器发消息,浏览器不能发

  • 比 WebSocket 简单,不需要升级协议,就是普通 HTTP 响应

  • 浏览器自带 EventSource API

  • 适合:服务器单向推送(消息通知、日志流)

WebSocket:

  • 全双工。浏览器和服务器都能主动发

  • 适合:需要双向通信的场景(聊天、游戏)

怎么选:

  • 只需要服务器推 → SSE 就够了,简单

  • 需要双向通信 → WebSocket


一句话讲清

延伸提问:“WebSocket 和 HTTP 有什么区别?”

“HTTP 是半双工的,一问一答。要实现实时数据要么轮询(浪费请求)要么长轮询(延迟高)。

WebSocket 是全双工的,建立连接后双方随时可以发消息。连接是通过 HTTP 升级上来的(101 Switching Protocols),升级后就不再是 HTTP 了,走 WebSocket 自己的二进制帧格式。

WebSocket 的优势是:建立一次连接,一直用,省去了反复建连的开销。服务器可以主动推数据,不需要客户端去轮询。头部信息少(只有几十字节的帧头),省带宽。”

延伸提问:“WebSocket 的连接怎么建立的?”

“通过 HTTP Upgrade 机制。客户端发一个 GET 请求,头部带上 Upgrade: websocket 和 Sec-WebSocket-Key(客户端生成的随机密钥)。服务器回复 101 Switching Protocols,并在 Sec-WebSocket-Accept 返回对密钥计算的哈希值。客户端验证哈希值,确认是真正的 WebSocket 服务器。之后连接升级完成,进入 WebSocket 协议通信。“


记忆口诀

WebSocket = HTTP 升级成的全双工长连接管道。双方随时发,服务器也能主动推。

连接过程:HTTP 升级请求 → 101 Switching Protocols → 一直通信。

帧类型记住三个:文本帧(传 JSON)、二进制帧(传文件)、Ping/Pong(保活心跳)。

客户端发数据必须掩码(防缓存攻击),服务器发数据不用掩码。

需要断线重连和心跳保活,不然连接断了你不知道。SSE 是阉割版的 WebSocket,只能服务器推。

速记卡(面试闪卡)

Q1:一句话讲清「WebSocket 全双工通信」到底是什么?

A:WebSocket 是建立在 TCP 之上的全双工长连接协议,连接后双方随时互发消息,专为实时通信而生。

Q2:一句话总结 —— 怎么理解?

A:像升级成长久热线:WebSocket(全双工通信协议)是 HTTP 升级成的长连接管道,建立后服务器和浏览器随时互发,不像 HTTP 一问一答来回折腾。

Q3:🌰 先搞懂”为什么需要 WebSocket” —— 怎么理解?

A:像坐店里等奶茶 vs 每分钟打电话问:轮询(Polling)反复问”好了没”全是废话;长轮询(Long Polling)稍好但仍要重建连接。WebSocket 让店员做好了直接喊你,双向零废话。

Q4:二、WebSocket 是什么 —— 怎么理解?

A:像一条双向高速公路:WebSocket 是全双工(Full-duplex,双方同时说),建立在 TCP 之上;连接由一次 HTTP 升级而来,升级后不再是 HTTP,不再带几百字节头部。

Q5:三、WebSocket 连接建立过程 —— 怎么理解?

A:像秘密社团对暗号:客户端发带 Upgrade: websocket 和 Sec-WebSocket-Key 的 HTTP 请求,服务器回 101 Switching Protocols 和用 RFC6455 常量算出的 Sec-WebSocket-Accept,验明正身后才升级。

Q6:核心速记主线有哪些?

  • WebSocket 是全双工长连接,建立在 TCP 之上,解决实时通信(聊天、协同、游戏)

  • 靠 HTTP Upgrade 握手:ClientHello 到 101 Switching Protocols 再转 WebSocket 帧

  • Sec-WebSocket-Key 与 Accept 是验明正身的对暗号,不为加密(真加密靠 wss 的 TLS)

  • 数据装进帧(Frame)传输,客户端发必须掩码防缓存攻击,Ping 与 Pong 做心跳保活

口诀

A:HTTP 一问一答烦,升级 101 换长管

全双工两边都能说,轮询废话靠边站

Key Accept 对暗号,验明正身防假站

数据装帧像快递,真加密靠 wss 伞

相关链接