当服务器学会"挤牙膏":流式输出与SSE的奇幻漂流

0 阅读6分钟

从前有个急性子的用户,每次点击按钮后就开始数秒:"1...2...3...再不出来我就刷新!" 直到他遇到了流式输出——这感觉就像看魔术师从帽子里源源不断拉出彩带,而不是等三个小时才看到一只鸽子扑腾出来。

为什么服务器也爱"挤牙膏"?

在传统Web交互中(如图1),服务器就像个固执的厨师:

image.png

而流式输出(Streaming Output)则像日料店的板前料理(图2):

image.png 主打的就是立即且高效,就像水流一样,持续不断的输出。大家有没有发现现在的大模型采用都是流式输出,当然你要我等个一分钟才有内容反馈给我,谁看啊

这种"挤牙膏式"的数据传输有什么魔力?让我们翻开武林秘籍

流式输出的三大内功心法

  1. 降龙十八掌·体验优化版
    "边生成边输出"是核心奥义。当LLM大模型生成"你好"需要2秒,生成"你好啊"需要4秒时,流式输出能让你在第2秒就先看到"你好",而不是干等4秒——这就像吃火锅时服务员先上毛肚再上牛肉,而不是等所有菜配齐才开火。

  2. 凌波微步·付费轻功
    大模型按token收费(每个词都是钱啊!)。流式输出相当于"分期付款":用户先消费已生成的部分内容,模型后台继续"打工还债"。这招在付费API场景下堪称省钱绝学。

  3. 读心术·用户心理学
    前端工程师最懂人类大脑的bug:

    • 等待2秒 + 立即展示完整内容 = 😠
    • 等待4秒但每秒都有新文字 = 😄
      这就是著名的"进度条安慰剂效应"——哪怕总时间更长,用户反而觉得更快!

2025大厂必考题的玄机

为什么这道题能登上大厂考题C位?

- LLM 聊天机器人(23年的AI爆款 -> 24年 推理 -> 25 年 AI Agent 年)
- 流式输出,属于优化用户体验,前端职责

这揭示了一个技术趋势演变:

  1. 2023:能说话的木偶(基础聊天)
  2. 2024:会思考的鹦鹉(逻辑推理)
  3. 2025:有手有脚的管家(AI Agent)

当AI管家要给你念《三体》全集时,流式输出就是避免你睡着的关键技术——毕竟谁也不想听完"地球往事"四个字就等三小时!

前端 VS 后端的"牙膏战争"

前端的障眼法

<h1>流式输出</h1>
    <div id="message"></div>
<script>
// 创建SSE连接(获得魔法水管)
const source = new EventSource('/sse')

// 水管来水时的处理姿势
source.onmessage = function (event) {
  document.querySelector('#message').innerHTML += event.data
}
</script>

前端在这里扮演"水管工"角色:

  1. 接上/sse这根魔法水管
  2. 每滴水(data)到来就拼接到页面上
  3. 全程保持"哇塞又来一滴"的惊喜表情

后端的挤牙膏术

app.get('/sse', (req, res) => {
  // 设置SSE专属响应头(启动牙膏管)
  res.set({
    'Content-Type': 'text/event-stream', // 声明是事件流
    'Cache-Control': 'no-cache',         // 禁止偷藏牙膏
    'Connection': 'keep-alive'           // 保持牙膏管畅通
  })
  
  // 开始定期挤牙膏
  setInterval(() => {
    const message = `Current time is ${new Date().toLocaleTimeString()}`
    res.write(`data: ${message}\n\n`) // 关键格式!
  }, 1000)
})

后端在此化身为"牙膏厂工人":

  1. text/event-stream头声明:"注意!我要开始挤牙膏了!"
  2. no-cache确保每次都是新鲜出炉的牙膏,不缓存
  3. keep-alive保持牙膏管持续畅通(避免被HTTP协议自动切断)
  4. 每秒钟用res.write挤出一段数据(注意必须data: 然后用\n\n结尾!)

SSE:单向传情的HTTP恋人

Server-Sent Events(SSE)的精妙之处在于:

  • 单相思模式:只允许服务器→客户端的单向通信(像极了暗恋)
  • HTTP协议:无需WebSocket那样的复杂婚约,普通HTTP就能谈恋爱
  • 自动重连:网络中断时会自动恢复连接(堪称技术界最持久的舔狗)

对比其他技术:

技术方向协议复杂度适用场景
SSE单向(服务端→客户端)HTTP实时通知、数据流
WebSocket双向独立⭐⭐⭐聊天室、游戏
长轮询伪双向HTTP⭐⭐兼容旧浏览器

手把手打造"牙膏工厂"

让我们启动这个神奇项目:

让我们启动这个神奇项目:

第一步:创建工厂地基

npm init -y # 获得建厂许可证
npm i express # 购买名为Express的施工队(引入express框架)

第二步:建造流水线(index.js)

const express = require('express')
const app = express()

// 首页配送车间
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html') // 发货HTML文件
})

// SSE秘密生产线
app.get('/sse', (req, res) => {
  res.set({ /* 设置响应头 */ })
  setInterval(() => {
    res.write(`data: ${new Date().toLocaleTimeString()}\n\n`) 
  }, 1000)
})

// 启动工厂电源,启动http服务
require('http').Server(app).listen(1314, () => {
  console.log('牙膏厂开始营业!传送带端口:1314')
})

第三步:设计展示柜台(index.html)

<body>
  <h1>流式输出体验馆</h1>
  <div id="message"></div>
  <script>
    const source = new EventSource('/sse') // 连接生产线
    
    source.onmessage = (event) => {
      // 把新鲜到货的牙膏展示在柜台
      document.getElementById('message').innerHTML += event.data + '<br>'
    }
  </script>
</body>

第四步:见证奇迹

  1. 运行node index.js启动工厂
  2. 访问http://localhost:1314
  3. 每秒都会看到新的时间戳冒出来: 我们来看看效果:

B1E595BDE5B620090522_converted.gif 是不是就像流水一样,只要我们生成内容,就好一点点渲染到页面,而不是等他生成所有的内容再渲染,更符合用户的体验

踩坑预警:SSE的特殊仪式

  1. 必须双换行符:每条消息必须以\n\n结尾(这是SSE的摩尔斯电码)
  2. 数据前缀:消息格式必须是data: 内容\n\n(少了前缀消息会迷路)
  3. 连接保活:浏览器默认在连接断开后3秒重连(贴心但可能造成重复连接)

解决方案示例:

// 正确挤牙膏姿势
res.write(`data: 第一段内容\n\n`) 

// 错误示范(会导致牙膏堵住)
res.write('忘了加data前缀!') 
res.write('少了一个换行符\n')

我刚开始接触,以为消息格式data: 内容\n\n并不重要,导致debug老久了,这是SSE必须的格式,不能变

流式输出的江湖地位

当你在这些场景下遇到它,请抱拳说声"久仰":

  1. LLM聊天:ChatGPT一个字一个字"蹦"出来的答案
  2. 日志监控:实时滚动的服务器日志面板
  3. 股票行情:分秒变动的股价瀑布流
  4. 文件下载:看到进度条像蜗牛一样前进时(那也是种流!)

尾声:全栈工程师的牙膏哲学

image.png 这个小小项目揭示了大厂全栈能力的要求:

  1. 后端要掌握"挤牙膏"的力度(数据分块)
  2. 前端要设计"接牙膏"的姿势(渐进渲染)
  3. 协议要确保"牙膏管"的畅通(SSE规范)

所以下次面试被问"如何优化长时间任务体验"时,请自信地回答:"咱们来挤牙膏吧!" 这可比说"使用流式输出技术实现服务端推送"有趣多了——毕竟技术界的幽默感,就是把复杂的事情说得像挤牙膏一样简单。