WebSocket实现流式对话与实时语音接口的协议原理与工程实践
WebSocket实现流式对话与实时语音接口的协议原理与工程实践
做「安诊用药AI助手」这个项目,最核心的两个交互场景是流式文字对话和实时语音通话。这两个功能的体验好坏,直接决定了整个产品的用户体验。
最开始做流式对话的时候,我们用的是 SSE。这个方案确实简单,后端一个 SseEmitter,前端一个 EventSource,几十行代码就能跑起来。大模型返回一个字往前推一个字,看起来效果也不错。
但等到做实时语音通话的时候,这个方案就彻底走不通了。用户边说话服务器要边推识别结果,AI 边播用户还能随时打断,这种双向同时进行的通讯需求,本质上已经超出了 HTTP 协议的能力边界。
也正是在解决这些具体问题的过程中,我们对 WebSocket 的理解,才从"一个长连接工具",深入到了协议设计的本质层面。本文就从功能实现的实际需求出发,聊聊为什么最终选择了 WebSocket,以及它和 HTTP 到底有哪些本质的不同。
从功能需求出发看技术选型
任何技术选型都不应该是为了用而用。选择 WebSocket,是因为我们遇到了三个具体的问题,是 HTTP 方案无论如何都解决不好的。
场景一:流式对话窗口
文字流式对话这个场景,其实 SSE 能工作。但做到一定程度,就会遇到瓶颈。
首先是上下文携带的问题。大模型的流式输出,每个 SSE 连接只能对应一次请求,如果用户中途又发了一条新消息,要么等上一条输出完,要么断开重连。而用 WebSocket,同一条连接里可以同时进行多个对话的流式输出,新的请求直接发消息就行,不需要重建连接。
然后是双向控制的问题。用户点一下"停止生成",SSE 方案只能是前端关掉连接,后端其实还在跑,直到输出完才发现连接断了。而用 WebSocket,前端发一个中断消息,后端收到立刻就能停掉大模型的输出,省下来的都是真金白银的 Token。
场景二:实时语音通话
到了语音通话这个场景,HTTP 方案的问题就不是优化的问题了,而是根本做不到。
全双工这个需求,是 HTTP 协议的死穴。
我们的语音通话是这个流程:用户对着小程序说话,每 200 毫秒录好一个音频分片,立刻发给后端;后端收到之后,一边做流式语音识别,一边把识别到的文字推回前端当字幕;识别到用户说完了,立刻调用大模型生成回答,回答的文字推字幕的同时,还要做 TTS 语音合成,合成一句播一句;整个过程中用户随时可以说话打断 AI 的播报。
这个流程里,有上行的音频流,有下行的文字流,有下行的音频流,还有各种控制信令,所有这些数据都是同时、双向、持续流动的。
用 HTTP 做的话,你得至少搞四个接口:上传音频分片的 POST 接口,拉识别结果的轮询接口,拉回答字幕的轮询接口,拉语音数据的轮询接口。就算你能接受这么高的复杂度,轮询带来的延迟也是不可接受的——用户说完了要等一秒才看到识别结果,通话的感觉就全毁了。
所有方案的对比
把这个场景下所有可能的技术方案放在一起对比,答案就非常清晰了:
|
技术方案 |
全双工 |
头部开销 |
连接数 |
延迟 |
实现复杂度 |
|---|---|---|---|---|---|
|
短轮询 |
❌ 否 |
极大,每个请求几百字节 |
N 个请求/秒 |
>1000ms |
低 |
|
长轮询 |
❌ 否 |
大,每个请求几百字节 |
大量挂起连接 |
300-1000ms |
中 |
|
SSE |
❌ 半双工,仅下行 |
小,一次握手 |
1条 |
<100ms |
低 |
|
WebSocket |
✅ 真正全双工 |
极小,每帧2-14字节 |
1条 |
<50ms |
中 |
选择 WebSocket,不是因为它多么"先进",而是因为它是唯一能同时满足我们所有需求的方案。
WebSocket 和 HTTP 的本质区别
很多人觉得 WebSocket 就是"长连接版的 HTTP",这个理解其实是非常表面的。这两个协议,从底层模型上就有着根本的不同。
模型上的差异:半双工 vs 全双工
HTTP 协议的模型是"请求-响应",这是一个严格的半双工模型。任何一个时刻,这条连接上只能有一件事情在发生:要么客户端发请求,要么服务器发响应。响应不回来,客户端就不能发下一个请求。
很多人说 HTTP/1.1 的管线化(pipelining)不是能同时发多个请求吗?但管线化只是能把多个请求一起发出去,响应还是必须严格按照请求的顺序回来。这就像打电话,你必须等对方说完了你才能说。
而 WebSocket 的模型是对等的双向通讯,是真正的全双工。连接建立之后,客户端和服务器的地位是完全平等的,任何一方,在任何时刻,都可以向对方发送任意数量的数据。这才是真正的"打电话"的感觉——你说话的同时,我也可以说话,我们还可以同时说话。
这个模型上的差异,是所有其他差异的根源。
开销上的差异:几个数量级的差距
协议模型的差异,直接体现在开销上。
做一个简单的计算:假设我们每秒要发 5 个音频分片,每个分片 6400 字节。
用 HTTP POST 的话,每个请求的头部至少有 500 字节(Host、User-Agent、Cookie、Token 等等),5 个请求就是 2500 字节。也就是说,仅仅是 HTTP 头部,就要占去有效载荷的 8% 带宽。
用 WebSocket 的话,每个音频分片是一个二进制帧,头部只有 6 个字节,5 个帧加起来才 30 字节,开销占比是 0.09%。
这就是两个数量级的差距。在语音通话这种每秒好几帧的场景下,这个差距是决定性的。
状态上的差异:无状态到有状态
HTTP 是无状态协议,每个请求都是独立的。所以我们才需要 Cookie、Session、Token 这些机制来维护上下文。
WebSocket 是有连接的,自然也就是有状态的。连接建立的那一刻,会话就建立了,后续的所有消息都是在这个会话的上下文中进行的,不需要每个消息都带上用户信息。
这个特性对于实时语音这种场景太重要了——你总不能每个音频分片都带上一遍 JWT Token 吧。
WebSocket 的底层机制
理解了为什么要选 WebSocket,再来看看它到底是怎么做到这一切的。
握手:披着 HTTP 外衣的协议升级
WebSocket 最聪明的设计,就是它的握手过程。它没有搞一套全新的协议,而是完全复用了 HTTP 的基础设施。
客户端先发一个标准的 HTTP GET 请求,只是带上了几个特殊的头:
GET /ws/speech HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
服务器如果支持,就返回 101:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
从这一刻开始,这个 TCP 连接就再也不是 HTTP 了。接下来的所有字节,都按照 WebSocket 的帧格式来解释。
这个设计的好处太大了:所有的防火墙、反向代理、负载均衡器,不需要任何改动,就能让 WebSocket 握手通过。很多技术方案死就死在需要特殊网络环境支持上,WebSocket 完美地避开了这个坑。
工程实践中的关键问题
理解了协议原理,再来看看工程落地的时候有哪些关键问题是必须处理好的。
会话管理
每个 WebSocket 连接对应一个会话,会话对象一定要用 ConcurrentHashMap 来存,用普通 HashMap 并发一上来就炸。
WebSocketSession 本身不是线程安全的,多个线程同时发消息一定会出现帧错乱。所以发送的时候必须加锁,或者用单线程发送器。
最容易踩的坑是资源泄漏。连接断开的情况太复杂了,有正常关闭,有网络超时,有浏览器刷新,有小程序切后台被系统杀掉,每种情况触发的回调都不一样,甚至有些情况什么回调都没有。所以不要指望框架的回调,发消息失败的时候立刻清理资源,这是最稳妥的做法。
线程模型
Spring 调用 Handler 的线程,是 Tomcat 的 NIO 线程池里的 IO 线程。这个线程池非常小,默认也就十几二十个。
如果你把语音识别、大模型调用、TTS 合成这些耗时操作,直接写在 handleMessage 方法里,只要有几个慢请求,IO 线程池就占满了,所有连接的消息就都卡住了。
正确的架构一定是分层的:IO 线程只负责收和发,所有的业务逻辑,全部扔到独立的业务线程池里去异步执行。这个道理说起来每个人都懂,但不踩一次全服务卡住的坑,真的记不住。
端到端可靠性
首先是自动重连。网络切换、切后台、系统杀进程,随时都可能断连。所以一定要做指数退避的自动重连,切前台的时候一定要检查连接状态,断了就立刻重连。
然后是心跳。别管标准里有没有 Ping/Pong,一定要在应用层自己发心跳。90% 的无端断连,都是 Nginx 60 秒超时给掐断的,30 秒一次心跳就能解决 90% 的问题。
永远不要太相信平台的实现。小程序的 WebSocket 实现就有 bug,发太快了内部缓冲会出问题,极端情况下消息顺序都能错。协议标准写得再好,具体实现出问题也是常有的事。
回头来看,WebSocket 这个协议,最可贵的地方就是它的平衡感。
它没有追求彻底的革命,而是非常务实地复用了整个 HTTP 的生态,这才保证了它能够被迅速、广泛地部署。它也没有追求极致的简单,而是在协议层面用非常精巧的设计,解决了一系列深层次的工程问题。
它不是什么银弹,它只是把网络传输的复杂性,从协议层面,转移到了应用层面。简单的 API 背后,是会话管理、线程模型、可靠性保障、运维监控这一整套工程问题。
但也正是这些问题的存在,才让技术有了它应有的深度。当你把所有这些问题都解决掉,最终用户说"这个语音通话跟打电话一样自然"的时候,你就会觉得,所有深入到协议层面的研究,都是值得的。
更多推荐



所有评论(0)