Appearance
L01-05 WebSocket 双向流
全局中心内容:Disposable 是「可打断」的支点,dispose() 一下流就停。 全局讲解主线:SSE 哪里不够 → 会话与中断模型 → 写出可打断的端点 → 现场打断一次。
P1 · 产出页
中心内容:用户在模型说话中途喊停,它立刻停。
页面内容
/ws/chatWebSocket 端点- 发
{"type":"stop"}立即中断 - 回报已生成内容与消耗
讲解技巧
- 现场演示最有说服力:起一个长回答,输出两秒后点停止,立刻安静。
- 明确本节的定位:不是替代 SSE,是补 SSE 做不到的那一块。
时长:20s
P2 · 痛点页:SSE 做不到的那一块
中心内容:SSE 能打字机,但不能让客户端反向控制。
页面内容
- 中途点「停止」:SSE 要另发 HTTP 还要关联,很别扭
- 中途追加条件:SSE 只能断开重连,丢上下文
- 服务端主动推送 / 语音实时对话:SSE 不行
讲解技巧
- 先说清「什么时候不用」:一问一答就别上 WS。这条反向建议能建立信任。
- 用一句话概括判据:需要「客户端在生成过程中反向控制服务端」才值得上 WebSocket。
时长:2min 30s
P3 · 原理图:会话生命周期
中心内容:sessionId → Disposable 的映射,是全部机制的核心。
页面内容
- 握手 → 建立会话
- chat 消息 → 订阅 Flux → 逐帧回推
- stop 消息 → 找到 Disposable → dispose() → 取消
- 连接关闭 → 清理
讲解技巧
- 黑板只画这个映射表:
Map<sessionId, Disposable>。整节课反复指它。 - 提问留白:「如果同一个人开了三个标签页,用 userId 做 key 会怎样?」——答案:一个停全停,引出一个用户多会话的设计。
- 黑板只画这个映射表:
时长:3min
P4 · 原理图:中断怎么传导
中心内容:中断只能省还没生成的部分。
页面内容
- stop 帧 → dispose() → cancel → 关闭厂商 SSE → 停止生成
- 已生成的 Token 照常计费
讲解技巧
- 必须讲清「止损不等于免费」:学员很容易以为打断就不花钱。这句话不说,上线后会有人拿它做成本优化方案然后翻车。
- 类比:「就像取消外卖订单——已经做出来的那份你还得付」。
时长:2min
P5 · 风险表:有状态带来的麻烦
中心内容:WebSocket 把状态带回来了,和弹性伸缩直接冲突。
页面内容
- 多标签页 → 以 sessionId 而非 userId 管理
- 断线重连 → 状态不能只放内存
- 多副本 → 需粘性会话或集中式会话存储
讲解技巧
- 第三条是本节最值钱的提醒:本地跑得通不代表生产跑得通。
- 明确给出做法:状态外置到 Redis。详细方案预告在 04-03。
时长:2min 30s
P6 · 编码:配置与 handler
中心内容:setOrder(1) 漏了,表现是连上就断。
页面内容
SimpleUrlHandlerMapping注册/ws/chatsetOrder(1)优先于静态资源映射WebSocketHandlerAdapter
讲解技巧
- 先翻车:去掉 setOrder 跑一次,看到「连接建立后立刻关闭」。这个现象极难排查,演示出来价值很高。
- 说明排查思路而不是结论:先看握手是否成功,再看是否被其他 HandlerMapping 抢走。
时长:5min
P7 · 编码:Disposable 与中断
中心内容:subscribe() 返回的句柄,就是中断开关。
页面内容
ConcurrentHashMap<String, Disposable> runningrunning.put(id, flux.subscribe())stop()→dispose()doFinally里running.remove(id)
讲解技巧
- 三个必须注意的点一次性给出:并发 Map、doFinally 清理、显式 subscribe。逐条念完接一句「漏了任何一条,中断都不生效」。
- 特别强调
remove(id):不清理会内存泄漏,且下次中断失效。
时长:6min
P8 · 运行与中断验证
中心内容:signal 是 cancel 而不是 onComplete,才算打断成功。
页面内容
- 正常:收到
done+onComplete - 打断:收到
stopped+done+cancel - 关闭标签:服务端日志显示已清理
- 正常:收到
讲解技巧
- 连续点两次停止:验证幂等,不报错。这种边界测试最能体现工程严谨度。
- 演示关闭标签页时服务端
running已无残留——用 actuator 或日志证明。
时长:4min
P9 · 避坑与小结
中心内容:代理会掐断空闲连接,心跳和超时都要配。
页面内容
- 有状态与弹性伸缩冲突 → 状态外置
- Nginx 默认 60s 掐断 → 调超时 + 25s 心跳
- send 前必须判
session.isOpen()
讲解技巧
- 第二条给一个具体的故障现象:「回答到一半突然没了,日志里是连接被重置」。学员遇到时能立刻对上号。
- 结尾悬念:「中断了,但重新开始要重发整个对话——怎么从断点继续?」引出 01-06。
时长:2min
讲师备忘
| 项 | 内容 |
|---|---|
| 课前必做 | 准备「去掉 setOrder 导致连接即断」的翻车录像;准备连续中断的幂等测试 |
| 最容易超时处 | P5 的有状态风险,容易展开讲 K8s 调度——答「04-03 专门讲」 |
| 学员最常问 | 「能不能用 SSE + 一个 stop 接口代替?」答:可以,但要自己维护映射,本质一样 |
| 现场备用 | 浏览器 WS 连不上 → 用 wscat 命令行演示,排除前端问题 |