<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; SSE</title>
	<atom:link href="https://www.phppan.com/tag/sse/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 25 Apr 2026 00:56:17 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>DeepSeek 和腾讯元宝都选择在用的SSE 到底是什么？</title>
		<link>https://www.phppan.com/2025/06/deepseek-and-tencent-yuanbao-sse/</link>
		<comments>https://www.phppan.com/2025/06/deepseek-and-tencent-yuanbao-sse/#comments</comments>
		<pubDate>Sat, 14 Jun 2025 05:52:21 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[SSE]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2382</guid>
		<description><![CDATA[在我们和 AI 聊天中，AI Chat 都采用了一种「打字机」式效果的实时响应方式，AI 的回答逐字逐句地呈现 [&#8230;]]]></description>
				<content:encoded><![CDATA[<div>
<p>在我们和 AI 聊天中，AI Chat 都采用了一种「打字机」式效果的实时响应方式，AI 的回答逐字逐句地呈现在我们眼前。</p>
<p>在实现这个功能的技术方案选择上，不管是 DeepSeek ，还是腾讯元宝都在这个对话逻辑中选择了使用 SSE，如下面 4 张图：</p>
<p><img class="medium-zoom-image" src="https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/210d4c551ad04417bd5021d4964767a8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5r2Y6ZSm:q75.awebp?rk3s=f64ab15b&amp;x-expires=1749996857&amp;x-signature=d%2FnzFP0FzMtWTHxvPaHkxjTe7v0%3D" alt="" /></p>
<p><img class="medium-zoom-image" src="https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06a2f33b0fe94973a737d036e443db79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5r2Y6ZSm:q75.awebp?rk3s=f64ab15b&amp;x-expires=1749996857&amp;x-signature=Zk26UC8HbQYKhNTJApBJfEexpho%3D" alt="" /></p>
<p><img class="medium-zoom-image" src="https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e3a27c0b99541229e7dd1af77505a0d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5r2Y6ZSm:q75.awebp?rk3s=f64ab15b&amp;x-expires=1749996857&amp;x-signature=UybqtP9x6nc3Bd4VqRmN7pC6Ze4%3D" alt="" /></p>
<p><img class="medium-zoom-image" src="https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d93ef424c557496e91b2835cb4e85280~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5r2Y6ZSm:q75.awebp?rk3s=f64ab15b&amp;x-expires=1749996857&amp;x-signature=hA7vPTueQboxlZahwc%2FishN60BM%3D" alt="" /></p>
<p>这是为啥，它有什么优势，以及如何实现的。</p>
<h2 data-id="heading-0">SSE 的优势</h2>
<p>因为它在该场景下的优势非常明显，主要是以下 4 点：</p>
<p><strong>1.场景的高度匹配。</strong></p>
<p>AI 对话的核心交互模式是：</p>
<ol>
<li><strong>用户发起一次请求</strong>（发送问题）。</li>
<li><strong>AI 进行一次持续的、单向的响应输出</strong>（生成回答）。</li>
</ol>
<p>SSE 的<strong>单向通信</strong>（服务器 → 客户端）模型与这个场景高度切尔西。它就像一条专门为服务器向客户端输送数据的「单行道」，不多一分功能，也不少一毫关键。</p>
<p>相比之下，WebSocket 提供的是<strong>全双工通信</strong>，即客户端和服务器可以随时互相发送消息，这是一条「双向八车道高速公路」。为了实现 AI 的流式回答，我们只需要其中一个方向的车道，而另一方向的车道（客户端 → 服务器）在 AI 回答期间是闲置的。在这种场景下使用 WebSocket，无异于「杀鸡用牛刀」，引入了不必要的复杂性。用户的提问完全可以通过另一个<strong>独立的、常规的 HTTP POST 请求</strong>来完成，这让整个系统的架构更加清晰和解耦。</p>
<p><strong>2.HTTP 原生支持，与生俱来的优势</strong></p>
<p>SSE 是建立在标准 HTTP 协议之上的。这意味着：</p>
<ul>
<li><strong>无需协议升级</strong>：SSE 连接的建立就是一个普通的 HTTP GET 请求，服务器以 <code>Content-Type: text/event-stream</code> 响应。而 WebSocket 则需要一个特殊的「协议升级」（Upgrade）握手过程，从 HTTP 切换到 <code>ws://</code> 或 <code>wss://</code> 协议，过程相对复杂。</li>
<li><strong>兼容性极佳</strong>：由于它就是 HTTP，所以它能天然地穿透现有的网络基础设施，包括防火墙、企业代理、负载均衡器等，几乎不会遇到兼容性问题。WebSocket 有时则会因为代理服务器不支持其协议升级而被阻断。并且云服务商对于 Websocket 的支持并不是很完善。</li>
<li><strong>实现轻量</strong>：无论是前端还是后端，实现 SSE 都非常简单。前端一个 <code>EventSource</code> API 即可搞定，后端也只需遵循简单的文本格式返回数据流。这大大降低了开发和维护的成本。</li>
</ul>
<p><strong>3.断网自动重连，原生容错</strong></p>
<p>这是 SSE 的「王牌特性」，尤其在网络不稳定的移动端至关重要。</p>
<p>想象一下，当 AI 正在为我们生成一篇长文时，我们的手机网络突然从 Wi-Fi 切换到 5G，造成了瞬间的网络中断。</p>
<ul>
<li><strong>如果使用 WebSocket</strong>：连接会断开，我们需要手动编写复杂的 JavaScript 代码来监听断开事件、设置定时器、尝试重连、并在重连成功后告知服务器从哪里继续，实现起来非常繁琐。</li>
<li><strong>如果使用 SSE</strong>：<strong>浏览器会自动处理这一切</strong>。<code>EventSource</code> API 在检测到连接中断后，会自动在几秒后（这个间隔可以通过 <code>retry</code> 字段由服务器建议）发起重连。更棒的是，它还会自动将最后收到的消息 <code>id</code> 通过 <code>Last-Event-ID</code> 请求头发送给服务器，让服务器可以从中断的地方继续推送数据，实现无缝的「断点续传」。当然，Last-Event-ID 的处理逻辑需要服务端来处理。</li>
</ul>
<p>这种由浏览器原生提供的、可靠的容错机制，为我们省去了大量心力，并极大地提升了用户体验。</p>
<p><strong>4. 易于调试</strong></p>
<p>因为 SSE 的数据流是纯文本并通过标准 HTTP 传输，调试起来异常方便：</p>
<ul>
<li>我们可以直接在浏览器地址栏输入 SSE 端点的 URL，就能在页面上看到服务器推送的实时文本流。</li>
<li>我偿可以使用任何 HTTP 调试工具，如 <code>curl</code> 命令行或者 Chrome 开发者工具的「网络」面板，清晰地看到每一次数据推送的内容。</li>
</ul>
<p>而 WebSocket 的数据传输基于帧，格式更复杂，通常需要专门的工具来调试和分析。</p>
<h2 data-id="heading-1">使用 SSE 实现「打字机」效果</h2>
<p><strong>1.后端——调用大模型并开启「流式」开关</strong></p>
<p>当后端服务器收到用户的问题后，它并不等待大语言模型生成完整的答案。相反，它在调用 LLM 的 API 时，会传递一个关键参数：<code>stream=True</code>。</p>
<p>这个参数告诉 LLM：「请不要等全部内容生成完再给我，而是每生成一小部分（通常是一个或几个‘词元’/Token），就立刻通过数据流发给我。」</p>
<p>下面是一个使用 Python 和 OpenAI API 的后端伪代码示例：</p>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn"></div>
<p><span class="code-block-extension-lang">python</span></div>
<div class="code-block-extension-headerRight">
<div class="render" data-v-159ebe90=""><span class="txt" data-v-159ebe90="">代码解读</span></div>
<div class="code-block-extension-copyCodeBtn">复制代码</div>
</div>
</div>
<pre><code class="hljs language-python code-block-extension-codeShowNum" lang="python"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask, Response, request</span>
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import</span> openai</span>
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import</span> json</span>

<span class="code-block-extension-codeLine" data-line-num="5">app = Flask(__name__)</span>

<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-comment"># 假设 OpenAI 的 API Key 已经配置好</span></span>
<span class="code-block-extension-codeLine" data-line-num="8"><span class="hljs-comment"># openai.api_key = "YOUR_API_KEY"</span></span>

<span class="code-block-extension-codeLine" data-line-num="10"><span class="hljs-meta">@app.route(<span class="hljs-params"><span class="hljs-string">'/chat-stream'</span></span>)</span></span>
<span class="code-block-extension-codeLine" data-line-num="11"><span class="hljs-keyword">def</span> <span class="hljs-title function_">chat_stream</span>():</span>
<span class="code-block-extension-codeLine" data-line-num="12">    prompt = request.args.get(<span class="hljs-string">'prompt'</span>)</span>

<span class="code-block-extension-codeLine" data-line-num="14">    <span class="hljs-keyword">def</span> <span class="hljs-title function_">generate_events</span>():</span>
<span class="code-block-extension-codeLine" data-line-num="15">        <span class="hljs-keyword">try</span>:</span>
<span class="code-block-extension-codeLine" data-line-num="16">            <span class="hljs-comment"># 关键：设置 stream=True</span></span>
<span class="code-block-extension-codeLine" data-line-num="17">            response_stream = openai.ChatCompletion.create(</span>
<span class="code-block-extension-codeLine" data-line-num="18">                model=<span class="hljs-string">"gpt-4"</span>, <span class="hljs-comment"># 或其他模型</span></span>
<span class="code-block-extension-codeLine" data-line-num="19">                messages=[{<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: prompt}],</span>
<span class="code-block-extension-codeLine" data-line-num="20">                stream=<span class="hljs-literal">True</span> </span>
<span class="code-block-extension-codeLine" data-line-num="21">            )</span>

<span class="code-block-extension-codeLine" data-line-num="23">            <span class="hljs-comment"># 遍历从大模型返回的数据流</span></span>
<span class="code-block-extension-codeLine" data-line-num="24">            <span class="hljs-keyword">for</span> chunk <span class="hljs-keyword">in</span> response_stream:</span>
<span class="code-block-extension-codeLine" data-line-num="25">                <span class="hljs-comment"># 提取内容部分</span></span>
<span class="code-block-extension-codeLine" data-line-num="26">                content = chunk.choices[<span class="hljs-number">0</span>].delta.get(<span class="hljs-string">'content'</span>, <span class="hljs-string">''</span>)</span>
<span class="code-block-extension-codeLine" data-line-num="27">                <span class="hljs-keyword">if</span> content:</span>
<span class="code-block-extension-codeLine" data-line-num="28">                    <span class="hljs-comment"># 关键：将每个内容块封装成 SSE 格式并 yield 出去</span></span>
<span class="code-block-extension-codeLine" data-line-num="29">                    <span class="hljs-comment"># 使用 json.dumps 保证数据格式正确</span></span>
<span class="code-block-extension-codeLine" data-line-num="30">                    sse_data = <span class="hljs-string">f"data: <span class="hljs-subst">{json.dumps({<span class="hljs-string">'token'</span>: content}</span>)}\n\n"</span></span>
<span class="code-block-extension-codeLine" data-line-num="31">                    <span class="hljs-keyword">yield</span> sse_data</span>

<span class="code-block-extension-codeLine" data-line-num="33">            <span class="hljs-comment"># (可选) 发送一个结束信号</span></span>
<span class="code-block-extension-codeLine" data-line-num="34">            <span class="hljs-keyword">yield</span> <span class="hljs-string">"event: done\ndata: [STREAM_END]\n\n"</span></span>

<span class="code-block-extension-codeLine" data-line-num="36">        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:</span>
<span class="code-block-extension-codeLine" data-line-num="37">            <span class="hljs-comment"># 错误处理</span></span>
<span class="code-block-extension-codeLine" data-line-num="38">            error_message = <span class="hljs-string">f"event: error\ndata: <span class="hljs-subst">{json.dumps({<span class="hljs-string">'error'</span>: <span class="hljs-built_in">str</span>(e)}</span>)}\n\n"</span></span>
<span class="code-block-extension-codeLine" data-line-num="39">            <span class="hljs-keyword">yield</span> error_message</span>

<span class="code-block-extension-codeLine" data-line-num="41">    <span class="hljs-comment"># 返回一个流式响应，并设置正确的 MIME 类型</span></span>
<span class="code-block-extension-codeLine" data-line-num="42">    <span class="hljs-keyword">return</span> Response(generate_events(), mimetype=<span class="hljs-string">'text/event-stream'</span>)</span>

<span class="code-block-extension-codeLine" data-line-num="44"><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:</span>
<span class="code-block-extension-codeLine" data-line-num="45">    app.run(threaded=<span class="hljs-literal">True</span>)</span>
</code></pre>
<p>在这段代码中，有几个关键点：</p>
<ol>
<li><strong>stream=True</strong>：向 LLM 请求流式数据。</li>
<li><strong>生成器函数（<code>generate_events</code>）</strong>：使用 <code>yield</code> 关键字，每从 LLM 收到一小块数据，就立即将其处理成 SSE 格式（<code>data: ...\n\n</code>）并发送出去。</li>
<li><code>Response(..., mimetype='text/event-stream')</code>：告诉浏览器，这是一个 SSE 流，请保持连接并准备接收事件。</li>
</ol>
<h5 data-id="heading-2"><strong>2.SSE 格式的约定</strong></h5>
<p>后端 <code>yield</code> 的每一条 <code>data:</code> 都像是一个个装着文字的信封，通过 HTTP 长连接这个管道持续不断地寄给前端。</p>
<p>前端收到的原始数据流看起来就像这样：</p>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn"></div>
<p><span class="code-block-extension-lang">vbnet</span></div>
<div class="code-block-extension-headerRight">
<div class="render" data-v-159ebe90=""><span class="txt" data-v-159ebe90="">代码解读</span></div>
<div class="code-block-extension-copyCodeBtn">复制代码</div>
</div>
</div>
<pre><code class="hljs language-vbnet code-block-extension-codeShowNum" lang="vbnet"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"当"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"然"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="5"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"，"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"很"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="9"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"乐"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="11"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"意"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="13"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"为"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="15"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"您"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="17"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"解"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="19"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"答"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="21"><span class="hljs-symbol">data:</span> {<span class="hljs-string">"token"</span>: <span class="hljs-string">"。"</span>}</span>

<span class="code-block-extension-codeLine" data-line-num="23"><span class="hljs-symbol">event:</span> done</span>
<span class="code-block-extension-codeLine" data-line-num="24"><span class="hljs-symbol">data:</span> [STREAM_END]</span>
</code></pre>
<p>看 DeepSeek 和腾讯元宝的数据格式，略有不同，不过有一点，都是直接用的 JSON 格式，且元宝的返回值相对冗余一些。 且都没有 data: 的前缀。</p>
<p><strong>3.前端监听并拼接成「打字机」</strong></p>
<p>前端的工作就是接收这些「信封」，拆开并把里面的文字一个个地追加到聊天框里。</p>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn"></div>
<p><span class="code-block-extension-lang">html</span></div>
<div class="code-block-extension-headerRight">
<div class="render" data-v-159ebe90=""><span class="txt" data-v-159ebe90="">代码解读</span></div>
<div class="code-block-extension-copyCodeBtn">复制代码</div>
</div>
</div>
<pre><code class="hljs language-html code-block-extension-codeShowNum" lang="html"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">&lt;!-- HTML 结构 --&gt;</span></span>
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"chat-box"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"user-input"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>&gt;</span></span>
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"sendMessage()"</span>&gt;</span>发送<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>

<span class="code-block-extension-codeLine" data-line-num="6"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span></span>
<span class="code-block-extension-codeLine" data-line-num="7">    <span class="hljs-keyword">let</span> eventSource;</span>

<span class="code-block-extension-codeLine" data-line-num="9">    <span class="hljs-keyword">function</span> <span class="hljs-title function_">sendMessage</span>() {</span>
<span class="code-block-extension-codeLine" data-line-num="10">        <span class="hljs-keyword">const</span> input = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'user-input'</span>);</span>
<span class="code-block-extension-codeLine" data-line-num="11">        <span class="hljs-keyword">const</span> prompt = input.<span class="hljs-property">value</span>;</span>
<span class="code-block-extension-codeLine" data-line-num="12">        input.<span class="hljs-property">value</span> = <span class="hljs-string">''</span>;</span>

<span class="code-block-extension-codeLine" data-line-num="14">        <span class="hljs-keyword">const</span> chatBox = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'chat-box'</span>);</span>
<span class="code-block-extension-codeLine" data-line-num="15">        <span class="hljs-comment">// 创建一个新的 p 标签来显示 AI 的回答</span></span>
<span class="code-block-extension-codeLine" data-line-num="16">        <span class="hljs-keyword">const</span> aiMessageElement = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'p'</span>);</span>
<span class="code-block-extension-codeLine" data-line-num="17">        aiMessageElement.<span class="hljs-property">textContent</span> = <span class="hljs-string">"AI: "</span>;</span>
<span class="code-block-extension-codeLine" data-line-num="18">        chatBox.<span class="hljs-title function_">appendChild</span>(aiMessageElement);</span>

<span class="code-block-extension-codeLine" data-line-num="20">        <span class="hljs-comment">// 建立 SSE 连接</span></span>
<span class="code-block-extension-codeLine" data-line-num="21">        eventSource = <span class="hljs-keyword">new</span> <span class="hljs-title class_">EventSource</span>(<span class="hljs-string">`/chat-stream?prompt=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(prompt)}</span>`</span>);</span>

<span class="code-block-extension-codeLine" data-line-num="23">        <span class="hljs-comment">// 监听 message 事件，这是接收所有 "data:" 字段的地方</span></span>
<span class="code-block-extension-codeLine" data-line-num="24">        eventSource.<span class="hljs-property">onmessage</span> = <span class="hljs-keyword">function</span>(<span class="hljs-params">event</span>) {</span>
<span class="code-block-extension-codeLine" data-line-num="25">            <span class="hljs-comment">// 解析 JSON 字符串</span></span>
<span class="code-block-extension-codeLine" data-line-num="26">            <span class="hljs-keyword">const</span> data = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(event.<span class="hljs-property">data</span>);</span>
<span class="code-block-extension-codeLine" data-line-num="27">            <span class="hljs-keyword">const</span> token = data.<span class="hljs-property">token</span>;</span>

<span class="code-block-extension-codeLine" data-line-num="29">            <span class="hljs-keyword">if</span> (token) {</span>
<span class="code-block-extension-codeLine" data-line-num="30">                <span class="hljs-comment">// 将新收到的文字追加到 p 标签末尾</span></span>
<span class="code-block-extension-codeLine" data-line-num="31">                aiMessageElement.<span class="hljs-property">textContent</span> += token;</span>
<span class="code-block-extension-codeLine" data-line-num="32">            }</span>
<span class="code-block-extension-codeLine" data-line-num="33">        };</span>

<span class="code-block-extension-codeLine" data-line-num="35">        <span class="hljs-comment">// 监听自定义的 done 事件，表示数据流结束</span></span>
<span class="code-block-extension-codeLine" data-line-num="36">        eventSource.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'done'</span>, <span class="hljs-keyword">function</span>(<span class="hljs-params">event</span>) {</span>
<span class="code-block-extension-codeLine" data-line-num="37">            <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'Stream finished:'</span>, event.<span class="hljs-property">data</span>);</span>
<span class="code-block-extension-codeLine" data-line-num="38">            <span class="hljs-comment">// 关闭连接，释放资源</span></span>
<span class="code-block-extension-codeLine" data-line-num="39">            eventSource.<span class="hljs-title function_">close</span>();</span>
<span class="code-block-extension-codeLine" data-line-num="40">        });</span>

<span class="code-block-extension-codeLine" data-line-num="42">        <span class="hljs-comment">// 监听错误</span></span>
<span class="code-block-extension-codeLine" data-line-num="43">        eventSource.<span class="hljs-property">onerror</span> = <span class="hljs-keyword">function</span>(<span class="hljs-params">err</span>) {</span>
<span class="code-block-extension-codeLine" data-line-num="44">            <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"EventSource failed:"</span>, err);</span>
<span class="code-block-extension-codeLine" data-line-num="45">            aiMessageElement.<span class="hljs-property">textContent</span> += <span class="hljs-string">" [出现错误，连接已断开]"</span>;</span>
<span class="code-block-extension-codeLine" data-line-num="46">            eventSource.<span class="hljs-title function_">close</span>();</span>
<span class="code-block-extension-codeLine" data-line-num="47">        };</span>
<span class="code-block-extension-codeLine" data-line-num="48">    }</span>
<span class="code-block-extension-codeLine" data-line-num="49"><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>这段代码主要有如下的点：</p>
<ol>
<li><code>new EventSource(...)</code>：发起连接。</li>
<li><code>eventSource.onmessage</code>：这是主要的处理函数。每当收到一条 <code>data:</code> 消息，它就会被触发。</li>
<li><code>aiMessageElement.textContent += token;</code>：这就是「打字机」效果的精髓所在——<strong>持续地在同一个 DOM 元素上追加内容</strong>，而不是创建新的元素。</li>
<li><code>eventSource.close()</code>：在接收到结束信号或发生错误后，务必关闭连接，以避免不必要的资源占用。</li>
</ol>
<h2 data-id="heading-3">EventSource 的来源与发展</h2>
<p>在 SSE 标准化之前，Web 的基础是 HTTP 的请求-响应模型：客户端发起请求，服务器给予响应，然后连接关闭。这种模式无法满足服务器主动向客户端推送信息的需求。为了突破这一限制，开发者们创造了多种「模拟」实时通信的技术。</p>
<ol>
<li><strong>短轮询</strong>：这是最简单直接的方法。客户端通过 JavaScript 定时（如每隔几秒）向服务器发送一次 HTTP 请求，询问是否有新数据。无论有无更新，服务器都会立即返回响应。这种方式实现简单，但缺点显而易见：存在大量无效请求，实时性差，并且对服务器造成了巨大的负载压力。</li>
<li><strong>长轮询</strong>：为了改进短轮询，长轮询应运而生。客户端发送一个请求后，服务器并不会立即响应，而是会保持连接打开，直到有新数据产生或者连接超时。一旦服务器发送了数据并关闭了连接，客户端会立即发起一个新的长轮询请求。这大大减少了无效请求，提高了数据的实时性，但仍然存在 HTTP 连接的开销，并且实现起来相对复杂。</li>
<li><strong>Comet：一个时代的统称</strong>：在 HTML5 标准化之前，像长轮询和 HTTP 流（HTTP Streaming）这样的技术被统称为 <strong>Comet</strong>。 Comet 是一种设计模式，它描述了使用原生 HTTP 协议在服务器和浏览器之间实现持续、双向交互的多种技术集合。 它是对实现实时 Web 应用的早期探索，为后来更成熟的标准化技术（如 SSE 和 WebSockets）奠定了基础。</li>
</ol>
<p>随着 Web 应用对实时性要求的日益增长，需要一种更高效、更标准的解决方案。</p>
<ul>
<li><strong>WHATWG 的早期工作</strong>：SSE 机制最早由 Ian Hickson 作为「WHATWG Web Applications 1.0」提案的一部分，于 2004 年开始进行规范制定。</li>
<li><strong>Opera 的先行实践</strong>：2006 年 9 月，Opera 浏览器在一项名为“Server-Sent Events”的功能中，率先实验性地实现了这项技术，展示了其可行性。</li>
<li><strong>HTML5 标准化</strong>：最终，SSE 作为 HTML5 标准的一部分被正式确立。它通过定义一种名为 <code>text/event-stream</code> 的 MIME 类型，让服务器可以通过一个持久化的 HTTP 连接向客户端发送事件流。 客户端一旦与服务器建立连接，就会保持该连接打开，持续接收服务器发送的数据。</li>
</ul>
<p>SSE 的本质是利用了 HTTP 的流信息机制。服务器向客户端声明接下来要发送的是一个数据流，而不是一次性的数据包，从而实现了一种用时很长的「下载」过程，服务器得以在此期间不断推送新数据。</p>
<p>其返回内容标准大概如下：</p>
<p>event-source 必须编码成 utf8 的格式，消息的每个字段都是用&#8221;\n&#8221;来做分割，下面 4 个规范定义好的字段：</p>
<ol>
<li>Event: 事件类型</li>
<li>Data: 发送的数据</li>
<li>ID：每一条事件流的ID</li>
<li>Retry: 告知浏览器在所有的连接丢失之后重新开启新的连接等待的事件，在自动重连连接的过程中，之前收到的最后一个事件流ID会被发送到服务器</li>
</ol>
<p>在实际中，大概率不一定按这个标准来实现。对于一些重连的逻辑需要自行实现。</p>
<p>现在大部分的浏览器都兼容这个特性，如图：</p>
<p><img class="medium-zoom-image" src="https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4bec52249e5340438e31bda65cbb9daa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5r2Y6ZSm:q75.awebp?rk3s=f64ab15b&amp;x-expires=1749996857&amp;x-signature=tKm3vUmW9Qg2R9Gk4g0rS5PVMRQ%3D" alt="" /></p>
<h2 data-id="heading-4">参考资料：</h2>
<ol>
<li><a title="https://en.wikipedia.org/wiki/Server-sent_events" href="https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FServer-sent_events" target="_blank">en.wikipedia.org/wiki/Server…</a></li>
<li><a title="https://learn.microsoft.com/zh-cn/azure/application-gateway/for-containers/server-sent-events?tabs=server-sent-events-gateway-api" href="https://link.juejin.cn?target=https%3A%2F%2Flearn.microsoft.com%2Fzh-cn%2Fazure%2Fapplication-gateway%2Ffor-containers%2Fserver-sent-events%3Ftabs%3Dserver-sent-events-gateway-api" target="_blank">learn.microsoft.com/zh-cn/azure…</a></li>
<li><a title="https://www.cnblogs.com/openmind-ink/p/18706352" href="https://link.juejin.cn?target=https%3A%2F%2Fwww.cnblogs.com%2Fopenmind-ink%2Fp%2F18706352" target="_blank">www.cnblogs.com/openmind-in…</a></li>
<li><a title="https://javascript.ruanyifeng.com/htmlapi/eventsource.html" href="https://link.juejin.cn?target=https%3A%2F%2Fjavascript.ruanyifeng.com%2Fhtmlapi%2Feventsource.html" target="_blank">javascript.ruanyifeng.com/htmlapi/eve…</a></li>
</ol>
<p>以上。</p>
</div>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2025/06/deepseek-and-tencent-yuanbao-sse/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
