深入拆解:从逐字打印效果看 SSE 流式技术奥秘

923 阅读3分钟

🔍 深入拆解:从逐字打印效果看 SSE 流式技术奥秘


一、现象观察:GPT 的逐字打印魔法

当我们与 ChatGPT 对话时,最令人着迷的体验莫过于文字的逐字涌现——每个字符像被无形之手逐个敲出。这种看似简单的效果背后藏着怎样的技术玄机?

开发者工具抓包解密

  1. 打开 Chrome 网络面板,过滤 XHR/Fetch 请求
  2. 观察响应类型:Content-Type: text/event-stream
  3. 数据接收方式:分段流式传输(Streaming)

👉 关键发现:使用 fetch() + ReadableStream + SSE 协议组合


效果查看 iShot_2025-02-23_14.50.48.gif

二、技术选型:为什么是 SSE?

协议对决表

维度SSEWebSocket长轮询
连接方式单工 (服务端推流)全双工半双工
协议基础HTTP/HTTPS独立协议(ws/wss)HTTP/HTTPS
数据格式text/event-stream二进制/文本JSON/Text
心跳机制内置自动维护需手动实现
内存消耗低 (TCP 长连接)高 (频繁重建连接)
适用场景实时日志、金融行情、对话流即时聊天、高频交易简单状态更新

三、SSE 核心技术解析

1. 协议工作原理图

sequenceDiagram
    客户端->>+服务端: GET /chat-stream (SSE连接)
    服务端->>+AI引擎: 获取响应内容
    loop 流式处理
        AI引擎-->>服务端: 返回数据分片
        服务端-->>客户端: data: "{content:'xx'}"
    end
    服务端-->>客户端: [end]事件
    deactivate 服务端

2. 消息结构规范

# 标准事件格式
event: message\n
data: {"content": "Hello"}\n\n

# 自定义事件
event: customEvent\n
data: 自定义内容\n\n

# 错误处理
retry: 3000\n  # 重连间隔(毫秒)

四、逐字流式实现架构

五、代码实现

全链路流程图

graph LR
    A[前端] -->|1. 发起 SSE 请求| B(Nginx)
    B -->|2. 代理转发| C[Node 服务]
    C -->|3. 流式生成| D[AI 大模型]
    D -->|分片返回| C
    C -->|streaming 回传| B
    B -->|4. 数据中继| A
    A -->|5. 拆字渲染| E[DOM 元素]

客户端代码(vue + fetch)

new Vue({
        el: "#chat-app",
        data: {
          messages: [],
          inputMessage: "",
          isSending: false,
          currentBotMessage: null,
        },
        methods: {
          async sendMessage() {
            if (!this.inputMessage.trim() || this.isSending) return;

            // 状态重置
            this.isSending = true;
            const userMessage = this.inputMessage;
            this.inputMessage = "";

            // 添加用户消息
            this.messages.push({
              text: userMessage,
              sender: "user",
              timestamp: Date.now(),
            });

            // 添加初始机器人消息
            const botMessage = {
              text: "",
              sender: "bot",
              loading: true,
              timestamp: Date.now(),
            };
            this.messages.push(botMessage);
            this.currentBotMessage = botMessage;

            try {
              const response = await fetch(
                "http://localhost:9527/chat/stream",
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: JSON.stringify({ message: userMessage }),
                }
              );

              if (!response.ok)
                throw new Error(`HTTP error! status: ${response.status}`);

              const reader = response.body
                .pipeThrough(new TextDecoderStream())
                .getReader();

              while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                value.split("\n").forEach((line) => {
                  if (line.startsWith("data: ")) {
                    const payload = line.slice(6).trim();
                    if (payload === "[DONE]") return;

                    try {
                      const data = JSON.parse(payload);
                      this.currentBotMessage.text += data.content;
                      this.keepScrollBottom();
                    } catch (e) {
                      console.warn("Parse error:", e);
                    }
                  }
                });
              }
            } catch (error) {
              console.error("Request failed:", error);
              this.currentBotMessage.text =
                "⚠️ Connection error: " + error.message;
            } finally {
              this.isSending = false;
              this.currentBotMessage.loading = false;
              this.keepScrollBottom();
            }
          },
          keepScrollBottom() {
            this.$nextTick(() => {
              const container = this.$el.querySelector(".chat-window");
              container.scrollTop = container.scrollHeight;
            });
          },
        },
      });

服务端代码(node + express)

const express = require("express");
const cors = require("cors");
const app = express();

// 中间件配置
app.use(express.json()).use(cors({ origin: "*" }));

// 流式接口核心逻辑
app.post("/chat/stream", (req, res) => {
  const { message } = req.body;
  console.log(`Received: ${message}`);

  // 初始化 SSE 协议头
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  // 定义响应数据流
  const responseStream = createStreamResponse();

  // 管道式推送数据
  const interval = setInterval(() => {
    const { done, value } = responseStream.next();

    if (!done) {
      res.write(`data: ${JSON.stringify(value)}\n\n`);
    } else {
      res.write("event: end\ndata: [DONE]\n\n");
      clearInterval(interval);
      res.end();
    }
  }, 100);

  // 客户端断开处理
  //   req.on("close", () => {
  //     clearInterval(interval);
  //     res.end();
  //   });
});

// 模拟流式生成器
function* createStreamResponse(
  input = `你好!我是一名专业的前端工程师,专注于为用户创造高效、直观且美观的网络体验。我精通以下核心技术和工具`
) {
  const segments = input.split("").map((c) => `${c}`);
  for (const segment of segments) {
    yield {
      content: segment,
      timestamp: Date.now(),
      status: "processing",
    };
  }
}

// 启动服务
app.listen(9527, () => {
  console.log("SSE 服务已在 9527 端口启动");
});

六、未来风向:WebTransport 的挑战

特性SSEWebTransport
协议基础HTTP/1.1HTTP/3
传输效率高 (QUIC 协议加持)
多路复用
移动端表现一般优 (弱网环境强适应)
浏览器支持主流浏览器Chrome 87+

🚨 七、常见问题速查表

问题现象排查方向解决方案
连接立即断开检查响应头设置确认Content-Type: text/event-stream
接收数据延迟服务端推送频率调整分片间隔时间,避免事件循环阻塞
跨域无法连接CORS 配置检查添加Access-Control-Allow-Origin
长时间无数据网络防火墙策略检查代理服务器对长连接的保持策略
移动端连接不稳定浏览器兼容性检查使用 polyfill 或降级方案

📦 八、GitHub 代码仓库

🔗 完整实现代码仓库

🔮 结语:流式技术的艺术

SSE 如同数字世界的活水渠,在 HTTP 的土壤上开辟出持久数据流。当需要简单的服务端推送时,它仍是无可争议的首选方案。正如 GPT 的逐字打印展示的:技术选择不在高深,而在恰到好处