Skip to content

L01-05 WebSocket 双向流 ​

全局中心内容:Disposable 是「可打断」的支点,dispose() 一下流就停。 全局讲解主线:SSE 哪里不够 → 会话与中断模型 → 写出可打断的端点 → 现场打断一次。


P1 · 产出页 ​

中心内容:用户在模型说话中途喊停,它立刻停。

  • 页面内容

    • /ws/chat WebSocket 端点
    • 发 {"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/chat
    • setOrder(1) 优先于静态资源映射
    • WebSocketHandlerAdapter
  • 讲解技巧

    • 先翻车:去掉 setOrder 跑一次,看到「连接建立后立刻关闭」。这个现象极难排查,演示出来价值很高。
    • 说明排查思路而不是结论:先看握手是否成功,再看是否被其他 HandlerMapping 抢走。
  • 时长:5min


P7 · 编码:Disposable 与中断 ​

中心内容:subscribe() 返回的句柄,就是中断开关。

  • 页面内容

    • ConcurrentHashMap<String, Disposable> running
    • running.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 命令行演示,排除前端问题