想象你经营着一家“全双工快递专线”。普通快递(HTTP)来回要排队:你发一单,等快递员回来,才能发下一单。而WebSocket是一条永不间断的双向传送带,双方可以同时往上传包裹、同时取包裹。开通专线前,你得先打电话给邮局(HTTP Upgrade握手),报上暗号(Sec-WebSocket-Key),邮局验证后回一句“专线通了”(101状态码)。之后每个包裹都要按统一规格打包:封口标记(FIN)、类型标签(Opcode)、防伪封印(Mask)、正文(Payload)。沉默太久时,邮局会寄来“还在吗?”的明信片(Ping),你回一张“在”(Pong),专线才不会被认为失联。想结束时就发一封“再会”挂号信(Close帧),礼貌下线。这就是WebSocket,一个全双工、低延迟的实时通信协议。
掌握WebSocket链路握手、帧格式与心跳机制,是搭建实时聊天、推送通知和协同编辑系统的底层能力,也是网络协议面试绕不开的考点。
可视化演示
展示WebSocket的Upgrade握手流程、帧格式FIN/Opcode/Mask/Payload字段解析、以及心跳Ping/Pong保活与关闭帧状态码处理机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+_captions.handshake+'
建立热线:Upgrade握手
想象你给邮局打电话,说“我不是要寄快递,我要开通一条专属双向管道”。电话那头是HTTP协议这位老接线员。你报上姓名和暗号:Sec-WebSocket-Key,这是一串随机生成的base64字符串。老接线员把暗号和固定的“魔法咒语”(GUID常量)拼在一起,用SHA1哈希,再base64编码,算出Sec-WebSocket-Accept。如果回复匹配,就返回101 Switching Protocols,专属管道正式打通。这之后,HTTP接线员退居幕后,把线路交给你和服务器直接对话。
这条暗号流程不是身份认证,而是防止“老代理缓存欺骗”:WebSocket的握手必须证明“这是新发起的请求,而非被缓存的旧响应”。RFC 6455规定了一个肉眼可见的GUID常量:258EAFA5-E914-47DA-95CA-C5AB0DC85B11。服务器将客户端发来的Key与GUID拼接后哈希编码,得到的值若与响应头一致,连接即通过校验。
邮局不是随便相信“我是老主顾”,它要核对暗号。任何客户端只要手握有效的Key,就能通过验证;一旦验证通过,HTTP接线员让出线路,WebSocket专线正式开工。这也解释了为什么WebSocket不是HTTP,却必须先借HTTP一只手完成开门。
Sec-WebSocket-Accept的生成过程。- 拼接待确认密钥把客户端发来的
Sec-WebSocket-Key字符串与固定的GUID拼接,得到一个待哈希字符串。 - 计算SHA1摘要对拼接结果做SHA1哈希,得到20字节的二进制摘要。
- Base64编码将摘要编码成base64文本,这正是握手响应头要返回的值。
读懂信封:帧格式基础
专线开通后,每一次传输都像一个标准“信封”——WebSocket帧。信封封面上印着所有关键信息,依次是:FIN封口标记、RSV预留位、Opcode类型标签、MASK封印标记、PayloadLength正文长度,后面跟扩展长度、掩码密钥和正文。FIN告诉对方“这是不是故事最后一页”;Opcode告诉对方“信封里装的是文字、二进制还是控制指令”。
所谓帧格式,就是“信封规格”,收发双方都必须严格遵守。一个几十字节的控制帧,完整解析可能需要先读第1字节的FIN和Opcode,再读第2字节的MASK和短长度;若是大消息,还要扩展到16位或64位长度,再加上4字节掩码密钥。每一步都环环相扣,任何一位错位都会让整封信无法拆解。
信封各栏缺一不可。如果贴错类型标签,解析方可能把“指令”误当“正文”;如果漏了封口标记,对方会一直等你下一封信,消息永远不完整。帧格式就是把“信封规格”写死在协议里,收发双方才敢闭眼拆信。
- FIN消息最后一帧标志。比如一条大消息分成5个帧,第5个的FIN=1,其余为0。
- RSV和OpcodeRSV是扩展预留位,固定为0;Opcode指明帧类型,0x1文本、0x2二进制、0x8关闭、0x9 Ping、0xA Pong。
- MASK和长度MASK标记负载是否被掩码,客户端到服务器的帧必须为1;PayloadLen表示正文长度,用7位、16位或64位扩展表示。
- 扩展与数据长度足够大时使用扩展位;如果MASK=1还要携带4字节MaskKey,随后才是真正的Payload数据。
防拆封:Mask与负载
为了防止老式代理服务器被“伪装请求”欺骗,协议规定:客户端发给服务器的帧必须加“防拆封封印”——掩码。做法很简单:4字节的MaskKey,把正文每个字节和Key循环异或。就像给信件内容套上一次性透明封膜,只有知道Key的服务器才能还原。
掩码把原文变成看起来随机的字节流,让恶意的中间代理无法伪造一个“看起来像HTTP请求”的数据包。服务器收到密文后,用同一个MaskKey再异或一次,正文就还原了。这是对称操作,也是WebSocket防缓存投毒的核心设计。
服务器收到密文后,用信封内附的MaskKey再异或一次,内容即还原。为什么服务器发回的数据不需要掩码?因为服务器是“可信地址”,客户端是公网上的未知寄件人,必须证明自己没夹带违禁品。这条看似简单的异或规则,是WebSocket防缓存投毒的重要防线。
- 取掩码字节把正文下标i对4取模,循环取MaskKey中的对应字节。
- 逐字节异或将正文每个字节与掩码字节做异或,得到发送线上的密文。
- 遍历全文对全部字节重复异或,完成整帧数据遮蔽。
定期问候:心跳保活
专线开通后,如果双方长时间沉默,中间的代理设备可能悄悄把连接回收,造成“半开连接”——线的两头都以为还通着,实际已经断了。WebSocket的心跳机制,就像在电话里每隔一段时间问一句:“喂,你还在吗?” 一方发Ping(Opcode=0x9),另一方必须回Pong(Opcode=0xA)。如果超过超时时间没收到Pong,就可以判定对端失联,主动关闭并清理资源。
心跳间隔要选得既省流量又及时。一般建议心跳周期小于中间代理的超时时间,比如30秒发一次Ping,留出足够等待窗口;如果连续多次没有Pong,就断开并触发重连。Ping/Pong帧属于控制帧,负载不能超过125字节,适合携带时间戳或随机ID用于匹配。
心跳不是“闲聊”,而是廉价的连接体检。Ping/Pong帧不携带业务数据,却能让双方避免在断掉的管线上继续空耗。生产环境里,心跳间隔通常设为30秒,正是为了在代理回收连接之前先发现异常。
- 发送Ping并记录发送方发出Ping帧,记录时间戳 t_ping。
- 接收Pong对端在收到Ping后必须尽快回复Pong,记录到达时间 t_pong。
- 阈值比较若两次时间差小于超时阈值Δ,判定连接存活;否则触发断线重连。
从容告别:优雅关闭
天下没有不散的筵席。WebSocket关闭时,不是直接抡起斧头砍断网线,而是先发一封“再会挂号信”:Close帧(Opcode=0x8)。信封里至少有2字节状态码,说明为什么告别——1000表示“正常收工”,1001表示“我要离开去别的地方”,1002表示“协议出错”,1007表示“数据格式不对”。收到Close帧的一方,也回一帧Close,再关闭底层的TCP连接,才算体面分手。
状态码是双方都能读懂的告别理由。除了1000之外,还有各类异常码:1003表示收到无法接受的数据类型,1008表示违反策略,1009表示消息太大,1011表示服务端异常。客户端和服务器都能发送Close帧,发起方携带状态码,接收方通常回同样的状态码,表示“收到,晚安”。
直接用TCP RST断线,就像话没说完就摔电话,对端不知道你是“掉线”还是“主动挂断”。Close帧给了双方最后一次同步的机会:先告知,再等对方回执,最后同时放下话筒。这种优雅终止,让WebSocket不会留下“僵尸连接”。
- 填类型把帧头Opcode设为0x8,向对方表明这是关闭帧。
- 写状态码在负载前两字节写入关闭原因码,如1000代表正常关闭。
- 附加原因可以追加一段UTF-8文本说明细节,例如"Service going down";没有原因时这部分可为空。
实战应用:全双工专线的用武之地
有了帧格式、心跳和关闭机制,WebSocket变成了实时应用的最佳快递员。实时聊天里,双方消息通过文本帧即时互推,无需轮询;推送通知里,服务器可以随时把新动态塞进客户端管道,客户端不用反复问“有新消息吗”;协同编辑里,多个客户端同时操作同一文档,各自通过二进制或文本帧广播操作,全双工的上下行通道让修改像水波一样扩散。
这三个场景共同点是“低延迟、双向、高频”:聊天要求消息秒达,推送要求服务器主动开口,协同编辑要求多端状态同步。如果换作HTTP轮询,每条消息都要重复建连、带Header,带宽浪费严重;而WebSocket在一条长连接上以最小帧头传输,省去大量重复握手开销。
HTTP像一问一答的传声筒,WebSocket则是双向传送带。聊天、推送、协作这些场景,天然需要“双方都能随时开口”,这正是全双工专线最闪耀的舞台。理解带宽使用,才能在容量规划时推断出连接上限和扩缩容策略。
- 上行速率客户端→服务器的数据速率,记作 R_up。
- 下行速率服务器→客户端的推送速率,记作 R_down。
- 求和因为上下行在同一时刻互不干扰,总吞吐可视为两者相加。
AI 应用场景
实时客服聊天系统
阶段:数据传输与状态同步
解决的问题:传统HTTP长轮询会频繁建立连接、浪费带宽,且消息延迟高。WebSocket建立一条全双工长连接,客服和用户的消息通过文本帧即时互推,配合心跳Ping/Pong及时发现掉线客服,避免消息石沉大海。
具体案例:电商平台在线客服中,顾客发送一条消息,网关立即通过WebSocket帧推送给客服工作台;客服回复时同样走这条管道,全程零轮询。若客服端网络中断,心跳超时后系统自动重新分配会话,并把未读消息暂存待重连后补推。
行情推送通知
阶段:服务端主动推送
解决的问题:股票行情、新闻快讯需要服务器主动推送,HTTP只能客户端先请求再响应。WebSocket的服务器→客户端下行通道让通知在事件发生的瞬间到达,配合二进制帧和掩码机制,确保低延迟且防篡改。
具体案例:证券App推送实时行情,服务器用二进制帧批量发送行情快照,客户端解析帧头并渲染K线。连接空闲时每30秒发送Ping,运维平台通过Pong响应率监控在线用户量,秒级发现异常连接。
多人协同文档编辑
阶段:并发操作广播与状态同步
解决的问题:多人同时编辑文档,需要将每个用户的光标、操作广播给所有协作者。HTTP难以维持大规模长连接,而WebSocket天然支持多客户端双向通信,配合分片帧(FIN=0)逐段同步大文档,保证编辑流畅。
具体案例:在线文档中,用户A输入一个字符,前端把操作封装成文本帧发给协同服务器;服务器通过WebSocket连接将操作转发给用户B和C,实现毫秒级所见即所得。某协作者网络抖动时,心跳超时触发自动重连并增量同步。
Python 演示
import base64import hashlibimport structimport os GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" def compute_accept(key): return base64.b64encode(hashlib.sha1((key + GUID).encode()).digest()).decode() def parse_frame(data): b1, b2 = data[0], data[1] fin = (b1 >> 7) & 1 opcode = b1 & 0x0F masked = (b2 >> 7) & 1 length = b2 & 0x7F offset = 2 if length == 126: length = struct.unpack(">H", data[offset:offset + 2])[0] offset += 2 elif length == 127: length = struct.unpack(">Q", data[offset:offset + 8])[0] offset += 8 mask_key = None if masked: mask_key = data[offset:offset + 4] offset += 4 payload = data[offset:offset + length] if masked: payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload)) return { "fin": fin, "opcode": opcode, "masked": masked, "length": length, "mask_key": mask_key, "payload": payload } if __name__ == "__main__": key = base64.b64encode(os.urandom(16)).decode() print("原始 Key:", key) print("Accept:", compute_accept(key)) raw = bytearray([0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x2b, 0x89, 0x4b, 0x4d]) parsed = parse_frame(bytes(raw)) print("解析结果:", parsed)
return base64.b64encode(hashlib.sha1((key + GUID).encode()).digest()).decode()一行完成Key+GUID的SHA1哈希与Base64编码,输出握手的Accept值。b1, b2 = data[0], data[1]先读前两个字节,随后按位拆解FIN、Opcode、Mask和长度。fin = (b1 >> 7) & 1右移7位取最高位,即FIN封口标记。opcode = b1 & 0x0F取第一个字节低4位,得到帧类型。masked = (b2 >> 7) & 1取第二个字节最高位,判断是否带掩码。if length == 126:短长度不够用时,按16位扩展读取真实负载长度。mask_key = data[offset:offset + 4]抽出4字节掩码密钥,用于后续还原。payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload))用循环异或还原被Mask遮蔽的正文。
常见误区
误区我以为WebSocket就是HTTP加了个Connection: keep-alive,后面还是用HTTP请求收发消息。
正解握手阶段的确走HTTP报文,但101状态码之后协议就切换了。后续数据全部使用WebSocket自己的帧格式,没有HTTP方法、URL、请求头。如果继续按HTTP解析,帧头会被当成乱码。就像专线开通后,你不再走邮局窗口,而是直接通过传送带递包裹。
为何常见:因为浏览器开发者工具里握手请求看起来就是HTTP,让人误以为整个生命周期都是HTTP。
误区WebSocket帧有Mask位,我觉得客户端和服务器都应该对负载做掩码。
正解RFC 6455规定只有客户端→服务器的帧必须Mask=1;服务器→客户端的帧Mask必须为0,且不允许使用掩码。理由是被害者模型不同:客户端可能被恶意网页控制,需要防止伪造请求,而服务器是可信的。若服务器也加Mask,反而会被中间代理误解。
为何常见:因为协议解析代码里统一处理了Mask分支,初学者只看到“异或还原”这个动作,忽略了方向限制。
误区TCP有keepalive机制,WebSocket连接不会断,不需要自己发Ping/Pong。
正解TCP keepalive默认通常需要几小时才触发一次,而且中间NAT或负载均衡可能比TCP更早回收空闲连接。WebSocket的Ping/Pong是应用层心跳,能以秒级频率探测连接活性,及时发现半开连接并触发重连。
为何常见:学过TCP的人容易把底层保活机制错误地当作上层解决方案,忽略了中间代理的实际超时。
误区WebSocket关闭时直接调`close()`或关掉TCP socket就完事了,不用管Close帧。
正解优雅关闭要求先发送带状态码的Close帧,等待对方回一个Close帧,然后再关闭TCP连接。直接断开会让对端收到一个“半截帧”或无法判断是否为网络异常,导致资源泄漏。就好比挂电话前得说“再见”,等对方回应再放下话筒。
为何常见:很多编程库的close()方法隐藏了Close握手细节,让人误以为底层不需要协议协作。
误区只要定期主动发Pong帧,就能保持连接活跃。
正解Pong是Ping的被动响应,协议要求收到Ping后必须回Pong;但反过来,客户端不能主动用Pong来探测连接。主动探测必须发Ping并期待Pong。应用层心跳的正确姿势是“发Ping、收Pong、记时间差”,而不是单向广播Pong。
为何常见:Ping/Pong都出现在心跳代码里,初学者容易混用,只关心“定时发控制帧”而忽略了谁响应谁。
面试题
Q1. WebSocket成功切换协议后,服务器返回的状态码是什么?
- A.200
- B.101← 正确答案
- C.404
- D.301
答案:B。WebSocket通过HTTP Upgrade机制握手成功后,服务器返回101 Switching Protocols,表示“协议已切换”。200是普通HTTP成功响应,404是找不到资源,301是重定向,都不属于WebSocket成功建立的标志。
Q2. 客户端发给服务器的帧,Mask位取值必须是什么?
- A.0
- B.1← 正确答案
- C.随机
- D.由服务器决定
答案:B。RFC 6455规定客户端→服务器的帧必须Mask=1,携带4字节MaskKey用于负载异或。服务器→客户端则必须Mask=0。随机或与服务器协商都不符合协议,所以只有1是正确答案。
Q3. 表示正常关闭WebSocket连接的状态码是什么?
- A.1000← 正确答案
- B.1001
- C.1002
- D.1011
答案:A。1000表示正常关闭,1001表示端点离开,1002表示协议错误,1011表示服务端异常。正常业务收工应该用1000,这与HTTP 200类似,都是“一切顺利”的语义。
Q4. 一个WebSocket帧的第一个字节是0x81,它表示什么?
- A.FIN=1,Opcode=0x1← 正确答案
- B.FIN=0,Opcode=0x1
- C.FIN=1,Opcode=0x8
- D.FIN=1,Opcode=0x2
答案:A。0x81的二进制是10000001,最高位FIN=1表示消息结束,低4位Opcode=0001表示文本帧。选项B的FIN=0与字节不符,C的0x8是关闭帧,D的0x2是二进制帧,因此A正确。
Q5. 当帧的PayloadLen字段值为126时,真实负载长度存放在多少个字节中?
- A.1
- B.2← 正确答案
- C.4
- D.8
答案:B。7位长度只够表示0-125,126表示“后面有16位扩展长度”,占2字节;127表示“后面有64位扩展长度”,占8字节。1字节是普通短长度,4字节在WebSocket帧格式中不存在。
Q6. 心跳机制中,收到Ping帧后应当回复什么?
- A.再发一个Ping
- B.Pong← 正确答案
- C.Close
- D.不回复
答案:B。协议要求收到Ping后必须立刻回Pong,Pong的负载应尽量保持与Ping一致。再发Ping是主动探测动作,Close是关闭连接,不回复会导致对端超时判死。所以正确做法是回Pong。
Q7. 下列哪一个不属于WebSocket帧中的字段?
- A.FIN
- B.Opcode
- C.Checksum← 正确答案
- D.MaskKey
答案:C。WebSocket帧头包含FIN、RSV、Opcode、MASK、PayloadLen,以及可选的扩展长度和MaskKey,但没有校验和字段。传输正确性由下层TCP保证,因此协议不设计Checksum,这是与IP/TCP头部的显著区别。
Q8. 计算Sec-WebSocket-Accept时,"Key+GUID"之后的处理顺序是什么?
- A.SHA1哈希,再Base64编码← 正确答案
- B.Base64编码,再SHA1哈希
- C.直接Hex编码
- D.MD5哈希,再Base64编码
答案:A。协议规定先对"Key+GUID"做SHA1得到20字节摘要,再Base64编码得到Accept值。如果先Base64再SHA1,结果完全不同。Hex编码和MD5都不属于标准流程,因此A正确。
Q9. 以下哪些属于WebSocket控制帧的Opcode?
- A.0x1
- B.0x8← 正确答案
- C.0x9← 正确答案
- D.0xA← 正确答案
答案:BCD。0x8是Close关闭帧,0x9是Ping,0xA是Pong,三者都是控制帧,用于连接管理和心跳。0x1是文本数据帧,不属于控制帧。所以正确答案是B、C、D。
Q10. 下列哪些是WebSocket优雅关闭的必要步骤?
- A.发送Close帧并携带状态码← 正确答案
- B.等待对方返回Close帧回执← 正确答案
- C.忽略状态码直接断开TCP
- D.双方都关闭后再释放底层连接← 正确答案
答案:ABD。优雅关闭要求先发带状态码的Close帧,收到对方的Close回执后再关闭TCP连接,也就是选项A、B、D。直接断开TCP属于粗暴终止,既没有状态码也没有回执,可能让对端误判为网络故障,因此C错误。