WebSocket是做LLM流式输出的关键技术,让服务器能一个字一个字地往浏览器推内容而不是等全部生成完。这篇讲清楚WebSocket跟HTTP的核心区别、连接建立的握手过程、消息收发机制和帧格式。你会学到怎么用WebSocket实现流式响应、怎么处理连接断开和重连、怎么做心跳保活。适合做LLM应用和聊天机器人的开发者掌握流式交互基础。

学习提示

需要学习WebSocket,了解AI应用开发能力边界才能评估方案可行性。本篇涵盖LangChain/RAG/Agent、API集成、向量数据库,帮助你构建AI应用系统能力。

目录
  1. 可视化演示
  2. AI 应用场景
  3. Python 演示
  4. 常见误区
  5. 面试题

可视化演示

状态—

WebSocket是做LLM流式输出的关键技术,让服务器能一个字一个字地往浏览器推内容而不是等全部生成完。这篇讲清楚WebSocket跟HTTP的核心区别、连接建立的握手过程、消息收发机制和帧格式。你会学到怎么用WebS...

协议本质——那通不挂断的电话

打电话时,你和对方共享一条持续的线路。你说,他听;他说,你听。不需要每次开口前都重拨一次号码。WebSocket在浏览器和服务器之间做的就是这件事——建立一条"不挂断的电话线"。

WebSocket是一种建立在TCP之上的全双工通信协议。所谓"全双工",就是双方可以同时说话。连接一旦建立,客户端和服务器地位对等,谁都可以随时发送数据,不需要像HTTP那样每次都先打招呼。为什么需要它?因为HTTP是"请求-响应"模式,服务器永远是被动的一方。如果服务器想主动推送消息——比如AI正在生成文字——HTTP做不到。有些系统用"轮询"(前端每隔1秒问一次"生成了吗")来模拟推送,这就像每隔几秒给朋友寄一封信问"下一句念了没",信件满天飞,效率极低。

从开销角度看,WebSocket的长连接特性可以用一个简单的数学关系说明。假设要推送N条消息,HTTP每次请求都要携带大量头部(记为$C_r$),而WebSocket只需一次握手开销$C_h$加上每帧2字节:

通俗说明:HTTP每推一条消息都要付一次"邮费"(请求头),WebSocket拨通电话后,每条消息只需付"2分钱"(2字节帧头)。N越大,差距越明显。

CostHTTP=N×Cr,CostWS=Ch+2×N\text{Cost}_\text{HTTP} = N \times C_r,\qquad \text{Cost}_\text{WS} = C_h + 2 \times N
  1. HTTP总开销N条消息的请求开销总和,每条消息都要携带完整的HTTP头部。
    CostHTTP=N×Cr \text{Cost}_\text{HTTP} = N \times C_r
  2. WS总开销一次握手开销加上N条消息的极简帧开销,每帧最小只需2字节。 记住那通电话——一旦接通,你们就一直聊到想挂为止。WebSocket的本质,就是这条持续存在、随时可说话的双向通道。
    CostWS=Ch+2×N \text{Cost}_\text{WS} = C_h + 2 \times N

HTTP对比——寄信与电话的差距

你寄出一封信:"今天天气如何?"朋友回信"晴"。你又想知道明天天气,于是再写一封信……HTTP就是这样的"寄信"流程:一次请求对应一次响应,服务器从不主动开口。

HTTP有三个痛点,恰好对应WebSocket的三个优势。

第一,连接方式。HTTP是短连接:一次请求-响应结束后连接就基本完成了使命(HTTP/1.1虽有keep-alive,但依然是"一问一答"的节奏)。WebSocket是长连接:握手成功后一直保持,直到某一方主动关闭。第二,通信方向。HTTP是半双工——同一时刻只能一方发送数据,请求发出后只能等待响应。WebSocket是全双工——双方可以同时双向发送,服务器也能主动推送。第三,头部开销。HTTP每个请求都要携带URL、Cookie、User-Agent等大量头部,动辄几百字节;WebSocket建立连接后,一条数据帧最小只需要2字节的开销。寄信时每次都要写地址贴邮票,而电话接通后说十个字和说一千个字,成本几乎一样。

那"轮询"能不能解决实时性?可以,但代价巨大。轮询的实时性本质上取决于轮询间隔:

通俗说明:前端每隔T秒问一次"生成了吗?",服务器生成消息后,用户平均要等半个轮询周期加上网络延迟才能感知到。间隔调短则实时但请求泛滥,间隔调长则迟钝——这就是"寄信"模式的死穴。

平均等待=Tinterval2+Lnet\text{平均等待} = \frac{T_{\text{interval}}}{2} + L_{\text{net}}
  1. 轮询间隔影响消息到达后,最坏要等到下一个轮询请求到达才被发现,平均等待半个周期。
    等待=Tinterval/2 \text{等待} = T_{\text{interval}} / 2
  2. 网络延迟叠加再加上请求和响应的网络传输时间。 寄信适合"偶尔问个事",打电话适合"持续聊天"。AI流式输出显然属于后者,这就是为什么HTTP那套"寄信"模式在LLM场景下行不通。
    总等待=Tinterval/2+Lnet \text{总等待} = T_{\text{interval}} / 2 + L_{\text{net}}

握手过程——拨号与身份确认

打电话的第一步是拨号。对方接起说"喂,你好",你说"我是小王",双方确认身份后,通话才正式开始。WebSocket的握手也是这样:先通过HTTP"拨号",确认身份后,协议才从HTTP切换到WebSocket。

握手过程是一个特殊的HTTP GET请求:

```

GET /chat HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Sec-WebSocket-Version: 13

```

其中Upgrade: websocket和Connection: Upgrade表示"我要切换协议";Sec-WebSocket-Key是客户端生成的16字节随机数的Base64编码,相当于"暗号"。服务器收到后,把这个暗号与固定GUID拼接,计算出一个校验值,通过Sec-WebSocket-Accept响应头返回,同时返回101 Switching Protocols状态码——语义是"协议切换成功"。

计算Sec-WebSocket-Accept的公式如下:

通俗说明:服务器把客户端发来的"暗号"加上固定盐值(GUID),做一次SHA1哈希,再Base64编码,得到一个"接头暗号"返回。客户端验证暗号匹配,就确认对面真的是WebSocket服务器,通话开始。

Sec-WebSocket-Accept=Base64(SHA1(Sec-WebSocket-Key+GUID))\text{Sec-WebSocket-Accept} = \text{Base64}(\text{SHA1}(\text{Sec-WebSocket-Key} + \text{GUID}))
  1. 拼接密语把客户端传来的Sec-WebSocket-Key与RFC 6455规定的固定GUID字符串拼接在一起,形成待哈希的输入。
    input=Sec-WebSocket-Key ∣∣ 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 \text{input} = \text{Sec-WebSocket-Key}\ ||\ \text{258EAFA5-E914-47DA-95CA-C5AB0DC85B11}
  2. 计算SHA1哈希对拼接后的字符串做SHA1哈希,得到20字节的摘要。
    hash=SHA1(input) \text{hash} = \text{SHA1}(\text{input})
  3. Base64编码将哈希摘要编码为Base64字符串,作为Sec-WebSocket-Accept响应头的值返回。 握手完成的那一刻,HTTP连接"升级"为WebSocket连接。之后双方就可以在这个连接上随时发送数据帧了——电话拨通,暗号对上,正式通话开始。
    Accept=Base64(hash) \text{Accept} = \text{Base64}(\text{hash})

帧格式——通话中的每一句话

电话里的每一句话,都有开始、内容、结束。WebSocket的"每一句话"叫做帧(Frame)。帧是WebSocket数据传输的最小单位,一条业务消息可能由一个或多个帧组成。

帧的头部结构非常紧凑。第1字节由三部分构成:最高位的FIN位、3个RSV保留位、4位opcode操作码。第2字节包含MASK掩码位和7位payload长度。如果实际数据较长,7位装不下,就采用扩展长度。opcode字段定义了帧的类型:0x1是文本帧、0x2是二进制帧、0x8是关闭帧、0x9是ping、0xA是pong。当一个很大的数据包需要发送时,发送方会先发一个FIN=0的首帧,再发若干FIN=0的继续帧,最后一帧的FIN=1标记结束——就像念一篇长文章时分段念:"第一段……"(还没完)、"第二段……"(还没完)、"第三段,完了"(结束)。

长度编码的规则用数学表达式可以写为:

通俗说明:帧头用7位表示长度,装不下时用"126"和"127"两个特殊标记告诉对方"后面还有更多位来精确表示长度"。就像电话通话中先报个大概,接着再补充详细内容。

len={7-bit 直接表示,0≤n<12616-bit 扩展,字段值=12664-bit 扩展,字段值=127\text{len} = \begin{cases} 7\text{-bit 直接表示}, & 0 \le n < 126 \\ 16\text{-bit 扩展}, & \text{字段值} = 126 \\ 64\text{-bit 扩展}, & \text{字段值} = 127 \end{cases}
  1. 直接表示长度小于126时,7位字段直接存储真实长度。
    len=n,0≤n≤125 \text{len} = n, \quad 0 \le n \le 125
  2. 16位扩展长度在126到65535之间时,7位字段置126,后续2字节存真实长度。
    len=uint16,字段值=126 \text{len} = \text{uint16}, \quad \text{字段值} = 126
  3. 64位扩展长度更大时,7位字段置127,后续8字节存真实长度。 理解了帧格式,你就知道电话里的每句话是怎么被"打包"传出去的。数据、心跳、关闭信号,全靠这些帧在线上跑。
    len=uint64,字段值=127 \text{len} = \text{uint64}, \quad \text{字段值} = 127

心跳保活——"喂,还在吗?"

打电话时间长了,你可能会问一句"喂,还在吗?"确保对方没掉线。WebSocket连接也需要这样的"确认",因为网络并非永远可靠。

这里有个关键背景——连接闲置问题。网络路径上的NAT网关、运营商路由器,通常会回收一段时间内没有数据流动的连接。比如某个NAT设备规定"5分钟没有流量就关闭映射",那么即使客户端和服务器都没主动断开,这条WebSocket连接也会在中间被"悄悄掐断",而两端都不知道。这就是"假死"连接。

WebSocket协议为此设计了心跳机制——ping/pong帧。服务端定时(比如每30秒)发送一个ping帧问"还在吗",对端收到后,协议栈会自动回复一个pong帧说"在呢"。如果服务端在超时时间内没收到pong,就判定连接已经断开,触发重连流程。心跳周期的设计有一个必须遵守的约束:

通俗说明:心跳间隔必须小于NAT等中间设备的空闲回收超时时间,才能保证连接一直"看起来活跃",不被静默回收。就像跟朋友约定:每隔5分钟必须互相说句话,这样大家才知道对方没睡着。

Theartbeat<TNAT timeoutT_{\text{heartbeat}} < T_{\text{NAT timeout}}
  1. NAT回收条件中间设备在连接空闲超过超时时间后,关闭连接的映射关系。
    连接被回收  ⟺  Δt>TNAT \text{连接被回收} \iff \Delta t > T_{\text{NAT}}
  2. 心跳保活约束只要心跳间隔短于NAT超时,连接就会因持续的流量而保持存活。 特别提醒:在浏览器环境下,WebSocket API只提供了onmessage、onclose等回调,没有主动发送ping帧的方法。浏览器内核收到ping后会自动回应pong,前端开发者无需干预。如果需要应用层保活(比如发现网络假死就主动重连),可以定时发送业务心跳消息,例如{"type":"heartbeat"},服务器收到后原样回应——这是"电话里的日常寒暄":隔一会儿说两句无关紧要的话,证明线路还通着。 心跳就是那声"喂,还在吗?",让连接两端都确认线路还活着。
    Thb<TNAT⇒连接保持 T_{\text{hb}} < T_{\text{NAT}} \Rightarrow \text{连接保持}

断线重连——电话断了,重新拨

即便是最稳定的通话,也可能因为信号干扰、对方手机没电、进入电梯而中断。WebSocket连接同样可能随时断开:网络抖动、服务器重启、客户端切换Wi-Fi,都会让长连接瞬间失效。

断线重连是生产级WebSocket应用的必修课。重连策略中,最经典的算法是指数退避(Exponential Backoff):第一次失败后等1秒再试,第二次等2秒,第三次等4秒……等待时间指数增长,直到一个上限。这样做的原因是,如果服务器刚刚崩溃重启,客户端们同时疯狂重连,会把服务器再次压垮。退避等待给服务器留出喘息时间。

重连等待时间计算公式如下:

通俗说明:第n次重试前等待的秒数,以初始值d0为基准,每失败一次翻倍,但最多不超过上限dmax。就像打不通电话时,你越来越多等一会儿才重拨,而不是每秒钟疯狂重拨。

delayn=min⁡(d0×2n−1, dmax⁡)\text{delay}_n = \min(d_0 \times 2^{n-1},\ d_{\max})
  1. 初始延迟第1次重试的等待时间,等于初始值d0。
    delay1=d0 \text{delay}_1 = d_0
  2. 指数翻倍每次失败后,等待时间乘2,形成指数增长。
    delayn=d0×2n−1 \text{delay}_n = d_0 \times 2^{n-1}
  3. 上限截断等待时间不能无限增长,超过最大等待时间dmax时取dmax。 重连成功后,要重置退避计数,让下次断线从1秒开始重新计算。此外,重连不等于"恢复原状"——如果应用需要登录态,断线重连后还要重新执行鉴权(比如携带新的token重新握手)。电话断线后你重新拨号,接通后可能还要再确认一次"我是小王",身份验证不能省。
    delayn=min⁡(d0×2n−1, dmax⁡) \text{delay}_n = \min(d_0 \times 2^{n-1},\ d_{\max})

流式应用——一个字一个字念给你听

终于到了最激动人心的环节。LLM生成回答时,并不是等整篇文章生成完才交付——而是像朋友在电话里逐字念给你听:"今""天""天""气""真""好"。你听到第一个字就能开始理解,不用干等。

LLM推理是逐token生成的,每个token约等于一个字(或一个子词)。服务端每生成一个token,立刻通过WebSocket的send()推送出去。前端在onmessage回调中收到这个token,把它追加到对话界面上——这就是"打字机效果"。用户看到AI"正在打字"的过程,而不是傻等几秒后刷出一整段答案。流式体验的核心指标是"首字延迟",也就是用户从发出请求到看到第一个字的耗时:

通俗说明:流式模式下,首字延迟只取决于模型生成第一个token的时间加上网络传输时间;而非流式模式,则要等全部token生成完。两者的体验差距,就像朋友念第一个字和把整封信寄给你在时间上的差异。

TTFTstream=Tfirst token+Tnet\text{TTFT}_\text{stream} = T_{\text{first token}} + T_{\text{net}}
  1. 首token时间流式模式下,用户看到第一个字的等待时间,只和模型首个token生成速度相关。
    TTFT=Tfirst+Tnet \text{TTFT} = T_{\text{first}} + T_{\text{net}}
  2. 完整输出对比非流式模式需要等待全部token生成完才能看到第一个字。 实用做法是约定一个消息协议。比如服务端先推{"type":"start"}表示开始生成,然后每个token推一条{"type":"token","content":"你"},最后推{"type":"end"}表示结束。前端拿到type字段就能区分"流开始""流内容""流结束",从而控制UI状态。这里有个细节:不要把每个token直接往DOM里append。高频DOM操作会导致布局抖动和性能问题(LLM每秒可能推送几十个token)。正确姿势是把token缓存在数组里,用requestAnimationFrame按帧批量渲染,或者用文本节点的appendData方法增量追加——这叫做增量渲染。 WebSocket流式输出,让AI从"寄一封长信"变成"打电话逐字念给你听"。用户体验的差距,就像等一封信和听电话里朋友念故事一样天壤之别。这也是为什么几乎所有AI聊天产品(ChatGPT、Kimi、Cursor等)的流式响应,底层都离不开WebSocket。 ---
    TTFTnon-stream=Tall+Tnet \text{TTFT}_\text{non-stream} = T_{\text{all}} + T_{\text{net}}

AI 应用场景

场景一:LLM聊天机器人的流式对话

阶段:模型推理 + API服务

解决的问题:如果等LLM全部生成完再返回,用户要等几秒甚至几十秒才看到第一个字,体验极差;用普通HTTP轮询又会产生大量无效请求。

具体案例:把Kimi、通义千问这类助手接入WebSocket网关。用户提交问题后,后端调用LLM推理,每生成一个token就通过WebSocket推送到浏览器,界面像打字机一样逐字呈现。用户还能随时发送"停止生成"指令,WebSocket的双向能力保证了这个控制消息能实时到达服务端。

相关角色:后端开发工程师、AI应用工程师

场景二:AI绘画进度的实时推送

阶段:模型推理进度监控

解决的问题:文生图模型需要数十秒推理,用户面对空白页面不知道是卡了还是在生成;定期刷新或轮询又会浪费资源。

具体案例:在Stable Diffusion风格的文生图服务中,后端在每个去噪step完成后,把当前进度(如"35%")和低分辨率预览图通过WebSocket推送到前端。用户实时看到画面从噪声中逐渐浮现,还能通过WebSocket发送参数调整指令,即时生效。

相关角色:平台开发工程师、AIGC产品前端

场景三:AI语音实时转写

阶段:实时推理管道

解决的问题:语音转写需要"边说边出字",音频流需要持续上行,识别文本需要持续下行,WebSocket的全双工特性正好匹配这个双向管道。

具体案例:会议记录工具中,麦克风采集的音频分片通过WebSocket不断上行到语音识别服务,Whisper等模型识别出的文本逐句下行推送,参会者盯着屏幕就能看到实时字幕。断线时,客户端自动重连并补传中断期间的音频切片。

相关角色:实时音视频工程师、语音AI工程师

Python 演示

python33 行
import asyncioimport jsonimport websockets TOKENS = [“你”, “好”, "!", “我”, “是”, "AI", “助”, “手”] async def llm_stream(websocket):    ""“模拟LLM流式生成:像打电话一样,一个字一个字推送”""    await websocket.send(json.dumps({"type": "start"}))    for token in TOKENS:        await asyncio.sleep(0.1)        await websocket.send(json.dumps({"type": "token", "content": token}))    await websocket.send(json.dumps({"type": "end"}))    print(“流式发送完成”) async def chat_client():    ""“客户端:接收流式token,逐字打印,模拟打字机效果”""    async with websockets.connect("ws://localhost:8765") as ws:        async for message in ws:            data = json.loads(message)            if data["type"] == "token":                print(data["content"], end="", flush=True)            elif data["type"] == "end":                print(“\n[流式输出结束]”)                break async def main():    async with websockets.serve(llm_stream, "localhost", 8765):        print(“WebSocket服务已启动:ws://localhost:8765”)        await chat_client() if name == "main":    asyncio.run(main())

常见误区

误区"WebSocket这么强大,以后后端统一用WebSocket吧,HTTP可以退休了。反正它既能实时推送又能双向通信,做什么都行。"

正解WebSocket和HTTP是互补关系,不是替代关系。WebSocket的握手阶段本身就依赖HTTP完成。在REST API、资源缓存、CDN加速、OAuth鉴权等场景,HTTP依然是绝对主流。WebSocket适合"建立后持续双向通信"的场景,HTTP适合"偶尔发起请求获取资源"的场景。就像电话不能替代邮件——寄合同、开发票、留凭证,还得靠邮件。选型的关键在于:你的数据是"持续流动"的还是"按需获取"的。

为何常见:WebSocket看起来"全知全能"——既能双向又能实时,而且LLM场景中效果惊艳,初学者很容易误以为它应该用于所有场景。

误区"Socket编程我学过,WebSocket不就是浏览器里的Socket吗?名字都差不多,底层都是一样的吧。"

正解Socket是操作系统提供的网络编程接口(TCP/UDP),而WebSocket是应用层协议。用Socket编程,你需要自己处理字节流、粘包、半包等问题;用WebSocket,帧格式、掩码、ping/pong这些机制都由协议和库封装好了。借用比喻:Socket是"电话线",WebSocket是"打电话的礼仪规范"。电话线是基础设施,而礼仪规范定义了"怎么拨号、怎么问候、怎么告别"——两者处于完全不同的层次。

为何常见:名字里都带"Socket",且WebSocket底层确实基于TCP Socket,初学者极易混淆概念层级。

误区"我在onmessage回调里直接document.getElementById('content').innerHTML += token,每次收到一个token就往页面追加,照样能实现打字机效果,图省事。"

正解高频DOM操作会引发频繁的重排和重绘,LLM每秒可能推送几十个token,页面很快就会卡顿。而且innerHTML +=会破坏已有的DOM节点结构,还会引发XSS安全风险。正确做法是维护token缓冲区,用requestAnimationFrame按帧批量渲染,或用textContent、appendData等更安全的API做增量追加。这就像朋友在电话里念得飞快,你不能他每吐一个字就手抄一遍——攒一屏再整齐写下来,效率更高也更安全。

为何常见:小规模测试时只有几个token,完全看不出性能问题;innerHTML用起来太顺手,初学者很容易养成坏习惯。

误区"要保活连接,前端得定时用WebSocket发ping帧,不然连接会被回收。"

正解浏览器WebSocket API根本不提供主动发送ping帧的方法——协议层的ping/pong帧由浏览器内核自动处理。前端能做的"应用层心跳"是定时发送业务消息(如{"type":"heartbeat"}),用于探测连接活性并触发上层重连逻辑。而真正的协议级ping帧,通常由服务端(如Node.js的ws库、Python的websockets库)发送给客户端,浏览器收到后自动回pong。理解这个方向,设计心跳策略时才不会搞反。

为何常见:网上教程常说"心跳保活",但很少说清谁ping谁、谁来回应。初学者默认浏览器里能控制一切,就被带偏了。

误区"连接只要建立成功,就能一直用。我从来没处理过onclose,开发时连接也一直没断过。"

正解WebSocket连接虽是长连接,但受网络环境影响随时可能断开:NAT超时回收空闲连接、路由器重启、服务器发布新版本、手机切换Wi-Fi等都会导致连接中断。生产环境必须组合使用心跳保活 + 断线重连 + 消息补偿重发。就像电话线虽然连着,但雷雨天气信号可能中断——你得准备好重新拨号,而不是干等。

为何常见:开发和演示环境中连接能稳定保持数小时,让人产生"永久连接"的错觉;一旦上生产,各种网络环境干扰接踵而至,没有重连机制的应用立刻原形毕露。 ---

面试题

Q1. WebSocket协议工作在OSI模型中的哪一层?

单选题 · easy

  • A.物理层
  • B.应用层← 正确答案
  • C.传输层
  • D.网络层

答案:B。WebSocket属于应用层协议,与HTTP同级,但建立在传输层TCP之上。它定义了应用之间的通信规则,就像"打电话的礼仪规范"属于社会交往层面,而不是电话线本身(传输层)的技术。

Q2. 以下哪项不是WebSocket相对HTTP的优势?

单选题 · easy

  • A.服务器可主动推送数据
  • B.握手后无重复头部开销
  • C.支持全双工通信方式
  • D.能提供更强的数据加密← 正确答案

答案:D。WebSocket并不比HTTP更安全,它同样依赖TLS(wss://)加密,加密强度取决于TLS配置而非协议本身。前三个选项——主动推送、无重复头部、全双工——才是WebSocket的核心优势。就像电话不比信件更保密,保密与否取决于你用的加密线路。

Q3. WebSocket握手中,服务器返回哪个状态码表示协议切换成功?

单选题 · easy

  • A.200表示请求成功
  • B.301表示永久重定向
  • C.101表示切换协议← 正确答案
  • D.400表示请求格式错

答案:C。101 Switching Protocols是WebSocket握手成功的标志,表示服务器同意从HTTP升级为WebSocket。200是普通HTTP成功响应;301是重定向;400是客户端请求错误。就像电话接通后对方说"喂,我是你要找的人",而不是回一句"邮件已收到"。

Q4. Sec-WebSocket-Accept的正确计算方式是?

单选题 · mid

  • A.只对密钥做Base64编码
  • B.密钥拼接GUID后做SHA1
  • C.拼接GUID后SHA1再Base64← 正确答案
  • D.对GUID做SHA1再加密钥

答案:C。服务器将客户端Sec-WebSocket-Key拼接固定GUID,先做SHA1哈希,再做Base64编码,得到Sec-WebSocket-Accept。三个操作缺一不可,顺序也不能颠倒。就像接头暗号要先加盐、再加密、再编码,每一步都有协议规定的原因。

Q5. WebSocket帧中FIN字段的作用是什么?

单选题 · mid

  • A.标记帧的数据类型
  • B.标记消息的最后一帧← 正确答案
  • C.标记数据是否加密
  • D.保留字段未启用

答案:B。FIN位为1表示这是消息的最后一帧,为0表示之后还有继续帧。opcode负责标记类型,MASK标记是否掩码。这就像说话时告诉对方:"我说了第一段,还没说完"(FIN=0)、"第三段,说完了"(FIN=1)。

Q6. 客户端发送的WebSocket帧,MASK位的取值必须为?

单选题 · mid

  • A.必须设置为0
  • B.必须设置为1← 正确答案
  • C.可协商为0或1
  • D.由服务器指定

答案:B。RFC 6455规定,客户端发送的帧MASK位必须为1,并携带4字节掩码密钥;服务器发送的帧不允许掩码。这是协议强制规则。就像寄信必须贴邮票——客户端发帧必须带掩码,没有例外。

Q7. 如果帧的第2字节低7位值为126,那么真实负载长度存储在哪里?

单选题 · hard

  • A.隐藏在第2字节的高位中
  • B.存储在后续的2字节扩展中← 正确答案
  • C.存储在后续的8字节扩展中
  • D.无法表示自动截断

答案:B。当7位长度字段的值为126时,真实的payload长度由紧接着的2字节表示;值为127时则由8字节表示;小于126时直接就是长度。这就像信封上写着"超重"时,邮局需要用秤(扩展字段)来称出具体重量。

Q8. 以下关于WebSocket心跳保活的做法,正确的是?

多选题 · mid

  • A.服务器定时发送ping帧← 正确答案
  • B.前端JS调用ws.ping()保活
  • C.前端定时发送业务心跳消息← 正确答案
  • D.连接建立后不做额外处理

答案:AC。服务器定时发送ping帧、浏览器内核自动回pong是协议层心跳,正确。前端定时发送应用层业务心跳消息也是常见做法。B错误,浏览器WebSocket API没有ping方法。D错误,不做处理连接可能被中间设备静默回收。A和C分别覆盖了协议层和应用层两条保活路径。

Q9. 关于WebSocket断线重连,下列说法正确的是?

多选题 · hard

  • A.重连应该用指数退避避免过载← 正确答案
  • B.重连成功应重置退避计数← 正确答案
  • C.每次重连间隔应固定不变
  • D.重连后需重新执行应用鉴权← 正确答案

答案:ABD。指数退避是标准重连策略,避免服务器恢复时被集中重连压垮(A对)。重连成功后需要重置退避计数,让下次从短等待重新开始(B对)。固定间隔重连可能造成"惊群"效应(C错)。WebSocket重连后应用层会话可能失效,需重新鉴权(D对)。就像电话断线后越等越久才重拨,接通后还要再确认身份。

Q10. 在LLM流式输出场景中,WebSocket的正确实践包括?

多选题 · mid

  • A.每个token发一条消息← 正确答案
  • B.用type字段区分消息阶段← 正确答案
  • C.收到token立即追加到DOM
  • D.流结束后由客户端关闭连接

答案:AB。每个token一条WebSocket消息是流式输出的核心模式(A对)。用type字段区分start/token/end,前端才能正确渲染(B对)。C错在"立即追加到DOM"——高频DOM操作会卡顿,应批量增量渲染。D错在"客户端关闭"——流结束应由服务端发end消息,客户端只管展示,是否关闭由业务约定。