SSE 实时行情与汇率推送 — 亮点难点与百问百答
针对 SSE(Server-Sent Events) 在金融交易所场景下「实时请求当前汇率/行情」的亮点、难点,以及 100 道深度面试题,含具体答案与深度分析。可配合本仓库
apps/web/app/api/sse/quotes/route.ts、apps/web/hooks/useSSEQuotes.ts使用。
目录:一、协议与原理(Q1–Q10)|二、SSE 与 WebSocket 选型(Q11–Q20)|三、前端 EventSource 与重连(Q21–Q30)|四、服务端实现(Q31–Q40)|五、首屏解耦与性能(Q41–Q50)|六、安全与鉴权(Q51–Q60)|七、汇率/行情业务场景(Q61–Q70)|八、故障与弱网(Q71–Q80)|九、扩展与多路复用(Q81–Q90)|十、面试话术与总结(Q91–Q100)
亮点与难点总览
问题
- 交易所需要实时汇率/行情,若用轮询会浪费带宽、延迟高;若与首屏强耦合会导致首屏慢、水合压力大。
- 需要服务端单向推送、自动重连、断线可感知,且与现有 HTTP 体系兼容(网关、鉴权、CDN)。
方案
-
用 SSE 做实时汇率/行情推送
- 服务端通过
Content-Type: text/event-stream维持长连接,按行推送data: {...}\n\n。 - 前端用
EventSource订阅,onmessage收数据;本项目推送type/quote、symbol、price、ts,用于当前汇率/行情展示。
- 服务端通过
-
首屏与实时流解耦
- 首屏用 BFF/React Query 静态或低频数据渲染,不依赖 SSE。
- 页面加载后再建立 SSE 连接,增量更新行情;重连期间继续用 React Query 缓存或上次快照展示。
-
断线重连与连接状态
- 监听
EventSource的error/close,断线后按指数退避重连(如 1s、2s、4s… 上限 30s)。 - UI 展示连接状态(连接中 / 已连接 / 重连中 / 已断开),弱网时用户有预期。
- 监听
面试话术
「我们通过 SSE 实时推送当前汇率/行情,服务端用 text/event-stream 长连接按行推送;前端用 EventSource 订阅,并做了断线指数退避重连和连接状态展示。首屏与实时流解耦:首屏不依赖 SSE,页面加载后再建连,兼顾首屏性能与交易实时性。」
一、协议与原理(Q1–Q10)
Q1:SSE 是什么?和普通 HTTP 请求有什么区别?
答案:SSE(Server-Sent Events)是基于 HTTP 的单向推送机制。客户端发一次 GET,服务端不立即结束响应,而是保持连接,持续写入 Content-Type: text/event-stream 的 body,按「事件行 + 数据行 + 空行」格式推送。与普通 HTTP 的区别:普通请求是「一发一收」;SSE 是「一发多收」,服务端可多次写数据,客户端通过 EventSource API 持续接收。
深度分析:SSE 本质是「长连接 + 流式响应」,利用 HTTP/1.1 的 keep-alive 与 chunked 传输,不升级协议,因此易过网关、易配合现有鉴权(如 Cookie、Header)。
Q2:SSE 的数据格式是什么?为什么是 data: xxx\n\n?
答案:规范格式为「事件类型(可选)+ 数据行 + 空行」。数据行以 data: 开头,多行用多个 data:;空行(\n\n)表示一条消息结束。例如:data: {"price":"40000"}\n\n。这样解析简单、可扩展(可加 event:、id:、retry:)。\n\n 作为分隔符便于流式解析,服务端每写一段就 flush,客户端按行读取即可。
深度分析:若只有 \n 没有空行,无法区分「多行 data」与「下一条消息」;\n\n 是规范约定的消息边界,与 HTTP header 的「空行分隔 header 与 body」一致。
Q3:SSE 基于 HTTP/1.1 还是 HTTP/2?多路复用的影响?
答案:SSE 传统上基于 HTTP/1.1,一个连接一个流。在 HTTP/2 下,一个 TCP 连接可多路复用多条流,但每条 SSE 连接仍是一个流(服务端持续写该流)。HTTP/2 不会「自动」把多条 SSE 合并成一条连接;若要多条 SSE 共用一个 TCP 连接,需要服务端或网关支持多路复用 SSE(如通过 stream id 区分),或前端开多条 EventSource(通常每条一个 TCP,在 H2 下会复用到同一连接)。
深度分析:浏览器对同一域名的 HTTP/1.1 连接数有限制(通常 6),多条 SSE 会占满;HTTP/2 下多条 SSE 可共享一个 TCP 连接,连接数压力小。
Q4:Content-Type: text/event-stream 必须带哪些头?Cache-Control 为什么重要?
答案:必须 Content-Type: text/event-stream;建议 Cache-Control: no-cache, no-transform、Connection: keep-alive。no-cache 避免中间缓存缓存响应体,导致客户端收不到实时数据;no-transform 禁止代理做 gzip 等转换,避免破坏流式解析。部分场景还会加 X-Accel-Buffering: no(Nginx)关闭缓冲,让数据立即推到客户端。
深度分析:代理和 CDN 默认可能缓冲响应,SSE 需要「有数据就下发」,所以必须禁止缓存与转换。
Q5:SSE 的 event、id、retry 字段分别有什么用?
答案:event:自定义事件类型,客户端可用 addEventListener('xxx', handler) 监听,不设则走默认 message。id:消息 ID,断线重连后客户端可带 Last-Event-ID 请求,服务端可从该 ID 之后补发,实现「断点续传」。retry:建议重连间隔(毫秒),浏览器可据此自动重连。例如 retry: 3000\n 表示建议 3 秒后重连。
深度分析:本项目未用 id/retry,重连逻辑在前端自己实现(指数退避);若要做「不丢消息」,需服务端支持 Last-Event-ID 并维护一段消息历史。
Q6:SSE 是「长连接」还是「短连接 + 轮询」?
答案:长连接。客户端发一次 GET,TCP 连接保持打开,服务端在该连接上持续写数据;只有连接断开(超时、错误、主动关闭)才会结束。不是「短连接 + 轮询」:轮询是多次短请求反复拉取,SSE 是一次请求、服务端主动推。
深度分析:长连接会占用服务端 fd 和内存,高并发时需要控制单机连接数或做水平扩展;轮询则请求次数多、延迟高,实时场景优先 SSE/WebSocket。
Q7:SSE 能跨域吗?和 CORS 的关系?
答案:能跨域,但受 CORS 约束。EventSource 的 URL 若为跨域,服务端必须返回 Access-Control-Allow-Origin(或 *)等 CORS 头,否则浏览器会拒绝。与 fetch 类似:浏览器会发请求,但若响应头不允许当前源,则 JS 拿不到数据(EventSource 会触发 error)。同源则无需 CORS。
深度分析:SSE 不支持自定义 Header(如 Authorization),跨域且需鉴权时,通常用 query 参数 或 Cookie(需 Credentials: include 等价能力;EventSource 同源会带 Cookie,跨域需服务端允许 credentials)。
Q8:EventSource 能带自定义 Header(如 Authorization)吗?
答案:不能。标准 EventSource(url) 只接受 URL,不支持配置 Header。因此带 token 的鉴权只能通过 URL query(如 ?token=xxx)或 Cookie(同源自动带)。若必须用 Header 鉴权,可用 fetch + ReadableStream 自己读流,或通过同源 BFF 代理 SSE(BFF 带 Header 请求上游,再转推给前端)。
深度分析:本项目 SSE 走同源 Next.js API Route,鉴权可放在 Cookie 或由 BFF 代理时加 Header;若 SSE 直连第三方行情源,多用 query 或 Cookie。
Q9:SSE 和 WebSocket 在协议层有什么本质区别?
答案:SSE:基于 HTTP,单向(服务端→客户端),文本为主,自动重连与 Last-Event-ID 有规范。WebSocket:基于独立协议(握手后升级),全双工、可二进制,无内置重连。SSE 更简单、易过网关、易配合现有 HTTP 鉴权;WebSocket 适合双向、低延迟、二进制场景(如订单簿、聊天)。
深度分析:选型看业务:只需「服务端推行情/汇率」用 SSE;若要「客户端高频上行 + 服务端下行」用 WebSocket。两者可并存(如 SSE 推行情,WS 推订单状态)。
Q10:为什么说 SSE 是「文本」的?能推二进制吗?
答案:规范里 SSE 数据是 UTF-8 文本,格式是 data: ...\n,所以天然是文本。若要推二进制,需要 Base64 编码 后放在 data 里,客户端解码;或改用 WebSocket。交易所行情/汇率多为数字和字符串,用 SSE 文本即可,无需二进制。
深度分析:二进制可省带宽,但 SSE 的解析与重连逻辑都按行来,引入 Base64 会增加编解码与体积,一般只在确有大量二进制时考虑 WebSocket。
二、SSE 与 WebSocket 选型(Q11–Q20)
Q11:实时汇率/行情为什么我们选 SSE 而不是 WebSocket?
答案:单向推送即可:服务端推价格,客户端只收、不需频繁上行。SSE 基于 HTTP,部署简单、易过网关和 CDN、可用 Cookie/query 鉴权;EventSource 自带重连与解析。WebSocket 需要单独协议与连接管理,对「只推不拉」的场景优势不大,反而增加复杂度。
深度分析:若后续有「客户端频繁下单、撤单」等强双向需求,可再上 WebSocket;当前「行情/汇率推送」SSE 足够。
Q12:SSE 和轮询比,有什么优缺点?
答案:SSE:一次连接、服务端有数据就推,延迟低、请求次数少、服务端可控推送节奏。轮询:定时发请求拉数据,实现简单,但间隔短则请求多、间隔长则延迟大,且很多请求可能无新数据(浪费)。实时性要求高时选 SSE;无长连接能力或仅低频更新时可轮询。
深度分析:交易所行情若用轮询,要「准实时」通常需 1–2 秒一次,请求量和延迟都不如 SSE 一次长连接。
Q13:一条 SSE 连接能推多个 symbol(多对汇率)吗?
答案:能。服务端在同一流里按行推送不同 data 即可,例如 data: {"symbol":"BTC/USDT","price":"40000"}\n\n 和 data: {"symbol":"ETH/USDT","price":"2500"}\n\n。客户端根据 symbol 分发到对应 state 或 UI。若 symbol 很多、推送频率很高,可考虑按 symbol 分连接或分 channel,避免单连接带宽与解析压力过大。
深度分析:本项目当前单连接推 BTC/USDT;扩展多 symbol 只需服务端循环或按订阅推送多条 data,协议无需改。
Q14:SSE 连接数有限制吗?浏览器端、服务端分别怎么考虑?
答案:浏览器:同源 HTTP/1.1 通常约 6 个连接,多开 SSE 会占满,影响其他请求。服务端:单机 fd 与内存有限,每连接一个 fd、一块缓冲,需设 max connections、超时关闭。高并发时用多机 + 负载均衡,或网关限制单 IP 连接数。
深度分析:前端尽量「按页/按模块」合并订阅(如一个页面一个 EventSource 推当前页需要的 symbol),避免开很多连接。
Q15:SSE 适合「请求当前汇率」这种语义吗?和 REST「一次请求一次响应」的区别?
答案:「请求当前汇率」可以有两种实现:REST 一次 GET 返回当前快照;SSE 是「订阅」,连接建立后持续收到后续更新。所以 SSE 适合「持续获得最新汇率」,而不是「只取一次」。若产品是「打开页面看到当前价并持续更新」,用 SSE;若只是「点一下查一次」,用 REST 即可。
深度分析:本项目是「交易页持续展示实时行情」,所以用 SSE 订阅;同时首屏用 BFF 的 REST 拉一次快照,再交给 SSE 增量更新,两者结合。
Q16:WebSocket 在交易所里通常用在什么场景?
答案:订单簿深度、逐笔成交、订单状态推送 等需要低延迟、双向或高频的场景。客户端可能频繁发「下单/撤单」,服务端实时回「成交流」;或服务端推送深度档位变化,数据量大、需二进制时用 WS 更合适。行情/汇率这种以读为主、更新频率中等,SSE 即可。
深度分析:很多交易所后端同时提供 REST(查快照)、SSE/WS(推流),前端按场景选:列表/首屏用 REST,实时牌价用 SSE,订单簿/成交流用 WS。
Q17:SSE 能实现「客户端发一个请求,服务端持续推多条」吗?
答案:能。这就是 SSE 的典型用法:客户端发一次 GET,服务端不关闭连接,持续写多条 data: ...\n\n。例如「订阅 BTC 行情」后,服务端每 2 秒写一条最新价。语义上就是「请求当前并持续更新」。
深度分析:与「请求一次响应一次」的 REST 不同,SSE 的「请求」是「建立订阅」,响应是「流式多条消息」。
Q18:若上游是 WebSocket,前端想要 SSE,怎么做的?
答案:在 BFF 或 API 层做协议转换:BFF 用 WebSocket 连上游行情源,收到消息后通过 HTTP 响应流以 SSE 格式推给前端。即「上游 WS → BFF → 前端 SSE」。这样前端只需 EventSource,鉴权、限流都在 BFF 统一做。
深度分析:本项目当前是 Next.js API Route 直接模拟推送;若真实行情是 WS,可在此 Route 里连上游 WS,把收到的数据转成 data: ...\n\n 写回前端。
Q19:SSE 和 HTTP/2 Server Push 有什么区别?
答案:HTTP/2 Server Push 是服务端在客户端请求一个资源时,主动再推其它资源(如 CSS/JS),用于优化首屏;一次推完即结束,不是长连接流。SSE 是长连接流式推送,持续写数据。两者不同:Server Push 不替代 SSE,SSE 仍是「订阅 + 持续推送」的标准做法。
深度分析:Server Push 在前端用得不多(浏览器支持与缓存行为复杂);实时数据推送仍以 SSE/WebSocket 为主。
Q20:选型时「实时汇率」和「实时订单簿」为什么可能用不同方案?
答案:实时汇率:数据量小、单向、更新频率中等(如秒级),SSE 足够,实现简单。实时订单簿:数据量大、档位多、可能需增量/二进制,延迟要求更高,且可能有「下单/撤单」双向交互,更适合 WebSocket。所以同一项目里可以「汇率用 SSE、订单簿用 WS」。
深度分析:按「数据量、方向、延迟、协议与运维成本」综合选型,不必一刀切。
三、前端 EventSource 与重连(Q21–Q30)
Q21:EventSource 的 onopen、onmessage、onerror 分别何时触发?
答案:onopen:连接建立成功(收到 200 且 Content-Type 正确)时触发。onmessage:每收到一条 SSE 消息(以 \n\n 结尾的一段)触发,event.data 为 data 行内容。onerror:连接出错、断开、或响应非 200/非 event-stream 时触发;具体错误类型浏览器不一定暴露,一般用 onerror 做「断线重连」。
深度分析:本项目在 onerror 里关闭当前连接、设状态为 reconnecting、用 setTimeout 做指数退避重连。
Q22:为什么需要「指数退避」重连?固定间隔不行吗?
答案:固定间隔在服务端故障或过载时,大量客户端同时重连会形成「惊群」,加重压力。指数退避(如 1s、2s、4s、8s… 上限 30s)让重连时间分散,避免同时撞上去;同时给服务端恢复时间。成功连接后应重置退避时间(如回到 1s),否则下次断线会等很久。
深度分析:本项目 MIN_BACKOFF=1000、MAX_BACKOFF=30000、BACKOFF_MULTIPLIER=2,onopen 时重置 backoffRef。
Q23:EventSource 有没有「自动重连」?我们为什么还自己写?
答案:规范里浏览器在连接断开后可以自动重连(且可带 Last-Event-ID),但行为与间隔不可控,且不同浏览器实现不一。自己写可以:控制退避时间、上限、重连前清理、与 React 生命周期绑定(unmount 时不再重连),并展示「重连中」状态,体验更好。
深度分析:生产环境通常不用浏览器默认重连,而是自己管理连接与状态,便于监控与降级。
Q24:在 React 里用 SSE,为什么要在 useEffect 里 subscribe、return 里 cleanup?
答案:避免重复连接与内存泄漏。useEffect 在 mount 时建立 EventSource,在 unmount(或 deps 变)时 return 的 cleanup 里 close 并置空 ref,这样组件销毁时连接会关闭。若在 useEffect 外建连接,组件多次挂载或路由切换会留下悬空连接;且 React 严格模式会 double mount,必须保证 cleanup 能正确关闭。
深度分析:本项目用 mounted 标志位 + cleanup 里 eventSourceRef.current?.close(),保证 unmount 后不再 setState 和重连。
Q25:SSE 的「连接状态」怎么设计?idle / connecting / connected / reconnecting / disconnected 分别表示什么?
答案:idle:未开始连接(如 enabled=false)。connecting:正在建立连接(new EventSource 到 onopen 之前)。connected:已连接,正常收数据。reconnecting:断线后等待退避并准备重连。disconnected:主动关闭(如 unmount)或不再重连。前端根据状态展示不同 UI(如绿点/黄点/灰点、文案「实时连接中/重连中/已断开」)。
深度分析:本项目 useSSEQuotes 返回 status、lastQuote、error,交易页用 status 驱动连接状态展示。
Q26:重连时如何避免「重复订阅」或「状态错乱」?
答案:单连接:用 ref 持有当前 EventSource,重连前先 close 再 new;不要同时存在多个 EventSource 指向同一 URL。mounted 标志:cleanup 里设 mounted=false,重连回调里若 !mounted 则不再 setState 和继续重连。deps:useEffect 的 deps 只放 enabled、symbol 等,避免无谓重建;symbol 变化时 cleanup 会先关旧连接再建新连接。
深度分析:本项目 eventSourceRef 保证同一时刻只有一个连接;mounted 保证 unmount 后不再更新 state。
Q27:EventSource 的 readyState 有哪几种?各表示什么?
答案:CONNECTING (0):正在连接。OPEN (1):已连接,可收数据。CLOSED (2):已关闭。只读,不能主动设为 OPEN;连接断开或 close() 后变为 CLOSED。可用于在 onerror 里判断是否已关闭再决定是否重连。
深度分析:本项目未直接读 readyState,而是用 onerror 统一处理「断线」并重连。
Q28:前端如何解析 SSE 的「多行 data」?
答案:规范允许多行 data:,合并时用 \n 连接成一条 message 的 data。例如 data: line1\ndata: line2\n\n,则 event.data 为 "line1\nline2"。若用 JSON,可在一行内写完,如 data: {"a":1}\n\n,客户端 JSON.parse(e.data) 即可。本项目采用单行 JSON,解析简单。
深度分析:多行 data 常用于大块文本或兼容旧格式;行情类小 payload 单行 JSON 足够。
Q29:useSSEQuotes 的 symbol 参数当前有没有真正传给服务端?如何扩展「按 symbol 订阅」?
答案:当前前端把 symbol 作为 hook 参数,服务端 Next.js API Route 未读 query,是写死推 BTC/USDT。要按 symbol 订阅:前端把 symbol 放在 URL query,如 /api/sse/quotes?symbol=BTC/USDT;服务端 GET 时读 searchParams.get('symbol'),只推该 symbol,或多 symbol 时循环推送。EventSource 不支持自定义 header,所以 symbol 只能放 URL。
深度分析:多 symbol 也可一条连接推多种 symbol,客户端根据 data.symbol 分发;或每 symbol 一条连接(注意浏览器连接数限制)。
Q30:在 Next.js 里 EventSource 的 URL 用相对路径还是绝对路径?为什么?
答案:用 window.location.origin + '/api/sse/quotes' 或相对路径 /api/sse/quotes(相对当前页 origin)。同源时两者等价。用 origin 拼接可避免在 SSR 或 baseURL 异常时出错;相对路径在 SPA 内通常也没问题。本项目用 window.location.origin 保证在客户端且与当前页同源。
深度分析:SSR 阶段没有 window,所以 EventSource 必须在客户端(useEffect 内)创建,此时 origin 正确。
四、服务端实现(Q31–Q40)
Q31:Next.js App Router 里 SSE 用 Route Handler 的 ReadableStream 怎么实现?
答案:在 app/api/sse/quotes/route.ts 里 export async function GET(),返回 new NextResponse(stream, { headers: { 'Content-Type': 'text/event-stream', ... } }),其中 stream 是 new ReadableStream({ start(controller) { ... } })。在 start 里用 setInterval 或从上游拉数据,每次 controller.enqueue(encoder.encode('data: ' + JSON.stringify(obj) + '\n\n')),需要关闭时 controller.close()。
深度分析:本项目用 setInterval 每 2 秒推一条模拟行情,5 分钟后 cleanup 关流;真实场景可改为从 Redis/WS 上游读数据再推。
Q32:为什么 Route Handler 要设 export const dynamic = 'force-dynamic'?
答案:Next.js 默认会静态化或缓存 GET;SSE 是动态、长连接,不能缓存、每次请求都要新开流。dynamic = 'force-dynamic' 表示该路由不参与静态优化,每次 GET 都执行 handler,保证 SSE 行为正确。
深度分析:不设的话可能被缓成静态响应或只执行一次,后续请求拿不到新流。
Q33:SSE 服务端为什么要立即 flush?不 flush 会怎样?
答案:不 flush 时数据可能留在服务端缓冲区,等缓冲区满或连接关闭才发到客户端,导致延迟很大甚至收不到。立即 flush(如 Node 里每次 write 后调用 flush、或使用不缓冲的 API)让「有数据就下发」,满足实时性。Next.js 的 ReadableStream 每次 enqueue 一般会尽快下发,具体依赖运行环境。
深度分析:若前面有 Nginx,需加 X-Accel-Buffering: no 关缓冲,否则 Nginx 可能缓冲响应。
Q34:Node 里除了 ReadableStream,还能用什么写 SSE?
答案:Express:res.writeHead(200, { 'Content-Type': 'text/event-stream', ... }); setInterval(() => res.write('data: ...\n\n'), 2000),注意不要调用 res.end() 直到要关连接。http 模块:同样 res.write 多次,最后 res.end()。ReadableStream 是 Web API,在 Node 和 Edge 都可用,适合 Next.js Route Handler 等统一写法。
深度分析:核心都是「保持连接 + 按格式写 data 行 + 必要时 flush」。
Q35:SSE 连接在服务端什么时候会断开?超时怎么设?
答案:客户端关页面/EventSource.close()、网络中断、服务端主动 close、代理/负载均衡超时。服务端可设空闲超时(如 5 分钟无写则 close)和最大存活时间(如 30 分钟强制关),避免长时间占用 fd。本项目 Route Handler 里 5 分钟后主动 cleanup,即最大存活时间。
深度分析:Nginx 等代理常有 proxy_read_timeout,需设得足够大(如 3600s)否则会主动断 SSE。
Q36:一条 SSE 连接能同时推「行情」和「订单状态」吗?
答案:能。在同一流里写不同 data 即可,用 event: 或 data 里的 type 区分。例如 event: quote\ndata: {...}\n\n 和 event: order\ndata: {...}\n\n,前端用 addEventListener('quote', ...) 和 addEventListener('order', ...)。若业务上希望分离(如权限、频率不同),也可以开两条 SSE 或一条 SSE + 一条 WebSocket。
深度分析:合并可省连接数,分离则职责清晰;按业务与运维需求选。
Q37:Next.js 的 SSE Route 在 serverless 上能跑吗?有什么限制?
答案:有限制。传统 serverless(如 Lambda)是「请求-响应」模型,响应必须在一定时间内结束,且不支持长连接。所以 Vercel 等 serverless 上长时间 SSE 可能超时或被终止。长连 SSE 更适合常驻进程(Node 服务器、Docker、K8s Pod)。若必须 serverless,可考虑「短 SSE + 轮询」或把 SSE 放到单独的长连服务。
深度分析:本项目若部署到 Vercel,SSE 可能在 60s 等限制后被断;生产建议 SSE 走独立服务或同机 Node 服务。
Q38:服务端如何从「上游」拿实时汇率再推给前端?
答案:BFF/API 层连上游(HTTP 轮询、WebSocket、或上游的 SSE),收到数据后转成 SSE 格式推给前端。例如:BFF 用 WS 连行情源,on('message') 里把价格写入前端 SSE 的 ReadableStream;或 BFF 定时请求第三方汇率 API,把结果 enqueue 到前端流。这样前端只连 BFF,鉴权、限流、聚合都在 BFF 完成。
深度分析:本项目当前是模拟数据;真实场景可在同一 Route 里加「请求第三方 API 或订阅 WS」,再推到 controller。
Q39:SSE 服务端怎么做「广播」(多个客户端收同一条消息)?
答案:服务端维护当前所有 SSE 连接的列表(如 Setdata: ...\n\n。Next.js 无状态,需在外部维护连接集(如全局变量、Redis Pub/Sub)。用 Redis Pub/Sub 时:一个进程订阅 channel,收到后推给本机所有 SSE 连接;多机则每机一个 subscriber,各自推给本机连接。
深度分析:单机用内存 Set 即可;多机必须用 Redis 等做进程间广播。
Q40:为什么说 SSE 是「拉模型」?和「推模型」矛盾吗?
答案:拉指连接是由客户端发起的(客户端 GET,服务端响应并保持)。推指数据是服务端主动写的,客户端被动收。所以「拉」指建立连接的方式,「推」指数据方向;不矛盾。SSE 是「客户端拉建立连接 + 服务端推数据」。对比:轮询是「客户端反复拉数据」,没有服务端推。
深度分析:面试时说「SSE 是服务端推送、基于 HTTP 长连接」即可;若问「谁发起」,答「客户端发起 GET,服务端保持连接并推送」。
五、首屏解耦与性能(Q41–Q50)
Q41:为什么要把「首屏」和「实时行情」解耦?
答案:首屏若依赖 SSE,需要等连接建立 + 第一条数据才能渲染,延迟大且弱网时易白屏;SSE 还可能增加首屏 JS 与网络竞争。解耦:首屏用 BFF/React Query 拉一次快照或静态数据先渲染,SSE 在页面加载后单独建连、增量更新,这样首屏快、实时性由 SSE 保证。
深度分析:本项目交易页首屏用 React Query 拉 /api/markets,SSE 在 useSSEQuotes 里单独订阅,两者独立。
Q42:首屏「不依赖 SSE」具体指什么?代码上怎么体现?
答案:指首屏渲染不等待 SSE 连接或 SSE 数据。即:不用「等 EventSource.onopen 或 onmessage 才 setState 渲染列表」;而是用 BFF 或 React Query 的初次 fetch 结果渲染,SSE 只负责后续更新同一块数据(如 lastQuote)。代码上:列表/价格初始值来自 useQuery;useSSEQuotes 的 lastQuote 只做「更新」,不参与首屏是否展示的逻辑。
深度分析:若首屏就展示「Live: xxx」,可接受「连接中」时先不显示或显示上次缓存;不阻塞首屏渲染。
Q43:SSE 和 React Query 怎么配合?谁做主数据源?
答案:React Query 做首屏与缓存:初次拉 BFF 快照,staleTime 内用缓存。SSE 做增量更新:收到新 price 后更新组件 state(如 setLastQuote),或通过 React Query 的 setQueryData 把 SSE 数据写回 cache,这样同一份数据既被 SSE 更新又被其它组件消费。主数据源可以是「React Query cache + SSE 增量写入」。
深度分析:本项目交易页同时用 useQuery(markets) 和 useSSEQuotes(lastQuote),SSE 的 lastQuote 用于「Live: BTC/USDT xxx」展示,与 markets 列表可合并展示逻辑。
Q44:SSE 连接建立时机:一进页面就建还是等用户操作?
答案:一进页面就建更常见:用户打开交易页就期望看到实时价,早建连早收数据。也可「懒建连」:如 Tab 切到「行情」再建,减少非活跃 Tab 的连接。本项目是进入交易页就 useSSEQuotes(true),即立即建连。
深度分析:若连接数紧张,可按可见性(visibilitychange)或 Tab 维度做「不可见时断开、可见时重连」。
Q45:大量组件都消费「当前汇率」时,怎么避免重复建多条 SSE?
答案:单例订阅:在全局或 Context 里只建一条 EventSource,多组件通过 Context 或状态管理(如 Zustand)消费同一份 lastQuote。即「一个 EventSource + 多组件订阅同一 state」。不要在每个组件里各写一个 useSSEQuotes,否则会多连接。
深度分析:本项目目前是交易页一个 useSSEQuotes,若多个页面都要行情,可把 useSSEQuotes 提到 Layout 或 Provider,或封装成 Context。
Q46:SSE 消息频率很高时,前端如何避免渲染过多?
答案:节流:如每 100ms 最多 setState 一次,中间收到的数据取最新一条。requestAnimationFrame:在 rAF 里批量更新,避免同一帧多次渲染。分离「接收」与「展示」:接收层持续更新 ref,展示层用 setInterval 或 rAF 从 ref 读并 setState,降低 React 更新频率。
深度分析:行情若每秒多条,可节流到每秒 2–4 次更新,视觉上足够流畅且省 CPU。
Q47:首屏 LCP 和 SSE 有关系吗?怎么保证 LCP 不受 SSE 影响?
答案:有间接关系:若首屏关键内容等 SSE 才渲染,LCP 会受 SSE 延迟影响。保证 LCP 不受影响:首屏关键内容(标题、首屏列表、核心 CTA)都用同步数据或 BFF 请求渲染,不依赖 SSE;SSE 只更新「实时价格」等非首屏关键或可延迟的区块。这样 LCP 不依赖 SSE 连接速度。
深度分析:Web Vitals 里 LCP 取最大可见元素;若最大元素是「价格数字」且来自 SSE,需改为首屏用快照、SSE 只做替换。
Q48:SSE 的 JS 要不要懒加载?对首屏体积的影响?
答案:EventSource 是浏览器原生 API,不需要额外 JS;但封装 SSE 的 React hook 和业务逻辑会打进 bundle。若交易页是懒加载的,SSE 相关代码会随交易页 chunk 一起加载,不会占首屏主 chunk。若首屏就引了 useSSEQuotes,则 SSE 逻辑在主 chunk,体积通常很小(几十行),影响有限。
深度分析:SSE 逻辑简单,一般不单独拆 chunk;关键是「首屏渲染不依赖 SSE 数据」。
Q49:React 18 的 useSyncExternalStore 和 SSE 有关系吗?
答案:有。若把 SSE 的「最新一条消息」存到外部 store(如全局变量 + 订阅列表),可用 useSyncExternalStore 让组件订阅该 store,这样 React 18 的并发渲染与 SSR 能正确与外部数据同步。本项目目前用 useState,若改为外部 store + useSyncExternalStore,可更好地支持 SSR 与并发。
深度分析:useSyncExternalStore 适合「外部数据源(如 SSE、WebSocket)驱动 React 状态」的场景,避免 tear 和闪烁。
Q50:首屏「可交互」时间(TTI)和 SSE 的关系?
答案:SSE 在 useEffect 里建连,不阻塞主线程;但若 SSE 回调里做重计算或频繁 setState,可能拖慢 TTI。保证 TTI 不受影响:SSE 回调尽量轻量(只解析 + setState),重逻辑放 worker 或节流;且首屏可交互不依赖「已收到 SSE 数据」,即「没收到也能点、能操作」。
深度分析:首屏可交互的定义是「主线程空闲、可响应用户输入」;SSE 建连与收包是异步的,一般不直接阻塞 TTI,除非在收包回调里做了大量同步计算。
六、安全与鉴权(Q51–Q60)
Q51:SSE 连接需要鉴权吗?为什么?
答案:需要。若行情/汇率是非公开或按用户/权限区分,未鉴权则可能越权或滥用。鉴权方式:Cookie(同源自动带)、URL query(如 ?token=xxx,注意 token 进日志)、BFF 代理(前端只连同源 BFF,BFF 带 Header 连上游)。EventSource 不能带自定义 Header,所以 Cookie 或 query 二选一或 BFF 代理。
深度分析:本项目 SSE 走同源 Next.js API Route,可依赖 Cookie 或 Next 的 session;若 Route 内调上游,由服务端带 token。
Q52:用 query 传 token 有什么风险?怎么缓解?
答案:风险:URL 可能进日志、Referer、浏览器历史,token 泄露面大。缓解:用短期 token(如 5 分钟有效)、一次性(用一次即废)、或只用于 SSE 的只读 token,限制权限;生产尽量用 Cookie(HttpOnly)或 BFF 代理,不在 URL 暴露 token。
深度分析:若必须 query,可让服务端签发「仅 SSE 用、范围最小」的 token,并短过期。
Q53:SSE 如何防重放?需要吗?
答案:行情/汇率一般是只读推送,不涉及「操作」,通常不需要防重放。若 SSE 通道里携带「敏感指令」或「一次性数据」,可给每条消息带 nonce 或序号,服务端校验不重复;或连接级 token 一次性。本项目只推价格,无防重放需求。
深度分析:防重放多用于「写操作」或「敏感指令」;读流一般不要求。
Q54:同源 SSE 和跨域 SSE 在鉴权上有什么不同?
答案:同源:Cookie 自动带,可用 session/Cookie 鉴权;无需 CORS。跨域:Cookie 默认不带(需 credentials),且需服务端 CORS;鉴权多用 query token 或服务端允许的 Cookie 配置(如 SameSite、Secure)。前端若跨域连第三方 SSE,通常只能 query 带 token。
深度分析:本项目 SSE 同源,鉴权与主站一致即可;若将来 SSE 独立域名,需设计 token 或 Cookie 策略。
Q55:如何限制「单用户多开 SSE 连接」?
答案:服务端:按 userId(从 Cookie/token 解析)维护「当前连接数」,超过 N 则新连接返回 429 或关闭旧连接。网关:按 IP 或 userId 限连接数。前端:单例封装,确保同一用户同一页只建一条(Context/全局单例)。本项目前端单页单连接;服务端未做限连接数,可加。
深度分析:防止恶意或 bug 导致单用户开很多连接,拖垮服务端。
Q56:SSE 内容需要加密吗?HTTPS 足够吗?
答案:HTTPS 已保证传输层加密,中间人看不到 body。应用层再加密(如对 data 做 AES)一般不需要,除非有「端到端不信任网关」等需求。行情/汇率若为公开或内部,HTTPS 足够。
深度分析:金融场景通常 HTTPS + 鉴权即可;合规若有「传输加密」要求,HTTPS 已满足。
Q57:EventSource 能否带 Cookie?跨域呢?
答案:同源:自动带 Cookie,与 fetch 同源行为一致。跨域:默认不带 Cookie;若要带,需服务端 Access-Control-Allow-Credentials: true 且 Access-Control-Allow-Origin 不能为 *,且前端无法「配置」EventSource 的 credentials(标准未提供),所以跨域 SSE 带 Cookie 依赖浏览器默认策略,部分环境可能不带。稳妥做法是跨域用 query token。
深度分析:本项目同源,Cookie 自动带;若 SSE 迁到子域,需确认 Cookie 的 domain 与 SameSite。
Q58:如何防止 SSE 被滥用(爬虫、刷接口)?
答案:鉴权:必须登录或有效 token 才建连。限连接数:单 IP 或单用户最多 N 条。限流:按连接或按用户限制推送频率(服务端控制)。风控:异常连接(如短时间大量建连)可封禁。本项目可加「按 Cookie/token 限连接数」和「限流」。
深度分析:公开行情可适度限流限连接;付费或敏感数据必须鉴权+限流。
Q59:BFF 代理 SSE 时,BFF 和上游之间的鉴权怎么处理?
答案:BFF 到上游:用 BFF 自己的服务端 token(如 API Key、OAuth client credentials)放在 Header 里请求上游,不暴露给前端。前端到 BFF:用 Cookie 或前端 token 鉴权,BFF 校验后决定是否代理。这样前端不知道上游 token,上游只认 BFF。
深度分析:BFF 作为「鉴权与协议转换」层,前端 SSE 连 BFF,BFF 用服务端身份连上游。
Q60:SSE 和 CORS 预检(preflight)有关系吗?
答案:简单请求(GET、简单 Header)不会发 OPTIONS preflight;EventSource 的 GET 不带自定义 Header,所以一般不会触发 preflight。若 SSE 是跨域,浏览器会发 GET,服务端只需返回 CORS 头(如 Access-Control-Allow-Origin)即可,无需单独处理 OPTIONS。若将来用 fetch + ReadableStream 模拟 SSE 并带自定义 Header,则会 preflight。
深度分析:标准 EventSource 跨域时只需 CORS 响应头,不需要 OPTIONS 逻辑。
七、汇率/行情业务场景(Q61–Q70)
Q61:用 SSE「实时请求当前汇率」在业务上指什么?
答案:指建立订阅后,持续收到最新汇率/行情,而不是「请求一次返回一次」。即「打开交易页 → 建立 SSE → 服务端按一定频率(如每 2 秒)推送当前价」,前端展示「当前价」并持续更新。语义是「订阅当前并跟踪变化」。
深度分析:与 REST「GET 一次当前价」的区别是持续性和服务端主动推。
Q62:汇率/行情的数据源一般从哪里来?
答案:交易所 WebSocket/API、第三方行情供应商、自建撮合引擎。BFF 或中间层订阅这些源,做聚合、校验、限频后,再以 SSE 或 WebSocket 推给前端。前端不直连数据源,便于鉴权、限流与统一协议。
深度分析:本项目是模拟数据;真实项目在 Route 或 BFF 里接上游 WS/HTTP 再转 SSE。
Q63:多币对(BTC/USDT、ETH/USDT…)怎么推?一条连接还是多条?
答案:一条连接:服务端在同一流里轮询或按订阅推送多币对,每条消息带 symbol,客户端按 symbol 分发。多条连接:每币对一条 EventSource,浏览器连接数有限(如 6),币对多时不可取。推荐:一条连接推多 symbol,或按「当前页需要的 symbol」订阅一条连接。
深度分析:本项目当前单 symbol;扩展多 symbol 只需服务端多推几种 data,客户端用 symbol 区分。
Q64:行情推送频率多少合适?前端如何节流?
答案:服务端:交易所级可能毫秒级;一般展示用 1–2 秒一条即可,兼顾实时与带宽。前端:若推送超过刷新率(如 60fps),可节流到 100–200ms 更新一次 UI,或按 requestAnimationFrame 批量更新,避免过度渲染。
深度分析:本项目 2 秒一条,对「牌价展示」足够;订单簿等需更高频可单独用 WebSocket。
Q65:「当前价」和「盘口深度」都用 SSE 吗?
答案:当前价/最新价用 SSE 足够。盘口深度数据量大、更新频繁,常用 WebSocket 且可能用二进制或增量,SSE 也可但不如 WS 灵活。同一项目可「最新价用 SSE、深度用 WS」,或都用 WS 统一管理。
深度分析:按数据量与延迟需求选型;本项目只做「当前价」SSE。
Q66:汇率保留几位小数?前端展示和服务端推送是否一致?
答案:服务端按交易对规则推送(如 BTC 8 位、USDT 2 位);前端用 trading-core 的精度与 formatCurrency 展示,一致。计算用 decimal.js,展示用 Intl.NumberFormat,避免前端二次四舍五入导致与后端不一致。
深度分析:本项目 trading-core 有 CURRENCY_PRECISION,SSE 推送的 price 为字符串,前端展示时按币种精度格式化。
Q67:SSE 推送的 timestamp(ts)用来做什么?
答案:客户端可判断数据新旧(如只渲染比当前更新的)、做延迟监控(收到时间 - ts 为网络延迟)、去重(相同 ts 不重复渲染)。本项目 data 里带 ts,便于扩展延迟统计与去重。
深度分析:生产可上报「推送延迟」到监控,便于发现网络或上游问题。
Q68:行情断线期间,前端展示什么?「最后一次价格」是否还可用?
答案:断线期间可继续展示最后一次收到的价格,并标注「重连中」或「延迟更新」,避免空白或报错。lastQuote 保留上次值,重连成功后覆盖;若业务要求「断线则不展示」,可清空并显示「连接断开」。
深度分析:本项目重连期间 lastQuote 保持,UI 显示「Reconnecting…」+ 上次价格,体验较好。
Q69:交易所「休市」时 SSE 怎么处理?
答案:服务端可推送「休市」事件(如 type: 'market_closed'),然后关闭连接或停止推送;或保持连接但长时间不推,由客户端超时判断。前端收到休市后可展示「休市」、停止重连或降级为轮询。本项目未实现休市逻辑,可扩展 event 类型。
深度分析:业务规则由服务端定义,前端按 type 分支处理。
Q70:实时汇率用于「下单预填总价」时,如何避免脏读?
答案:展示用 SSE 最新价;下单时以提交瞬间服务端校验的价格为准(或带「参考价」由后端再算)。即前端用 SSE 做预填与展示,真正成交价以订单接口返回或后端计算为准,避免「读到旧价却按新价展示」的脏读。金额计算统一走 trading-core,提交 body 里带 price/quantity,后端再校验。
深度分析:金融场景「展示」与「成交」分离,后端为唯一真实源。
八、故障与弱网(Q71–Q80)
Q71:SSE 断线有哪些常见原因?
答案:网络:弱网、切换 WiFi/4G、丢包。服务端:重启、崩溃、超时关闭。代理/负载均衡:超时(如 60s)、连接数限制。客户端:页面最小化/后台、Tab 关闭、用户导航走。浏览器:某些环境下长时间无数据可能关连接。
深度分析:前端能做的是「检测断线 + 重连 + 状态展示」;服务端与网关需合理超时与限连接。
Q72:如何检测 SSE 已经断开?
答案:EventSource.onerror 触发表示连接异常或关闭;readyState === CLOSED 表示已关闭。轮询:若长时间未收到消息(如 30s),可主动认为断开并重连。本项目用 onerror 作为断线信号,不依赖超时。
深度分析:部分环境 onerror 可能不触发,可加「最后一条消息时间」超时作为兜底。
Q73:指数退避的「上限」为什么要有?设多少合适?
答案:无上限时,若服务端长期不可用,退避会无限增大,用户可能永远等不到重连。设上限(如 30s)保证「最多等 30 秒就试一次」,平衡恢复速度与对服务端压力。30s 是常见取值;可依业务调整(如 15s–60s)。
深度分析:本项目 MAX_BACKOFF=30000,退避到 30s 后不再增加,每次失败后 30s 再试。
Q74:重连失败多次后要不要提示用户?怎么提示?
答案:要。例如连续失败 3–5 次后,展示「实时行情连接异常,请检查网络或稍后重试」,并提供「重试」按钮(立即重连)。可同时保留「最后一次价格」展示,避免空白。本项目可扩展「重试次数」state,超阈值后 setError 文案并展示按钮。
深度分析:避免用户一直看到「重连中」却不知道是否正常,提升可感知性。
Q75:弱网下 SSE 消息会不会乱序或重复?
答案:TCP 保证有序,同一连接内不会乱序。重复可能出现在「断线重连后」若服务端支持 Last-Event-ID 补发,可能收到断线前已收过的消息,需客户端按 id 或 ts 去重。本项目未做 Last-Event-ID,重连后从新数据开始,无补发故无重复。
深度分析:若做「不丢消息」,需要 id + 服务端补发,客户端去重。
Q76:移动端或后台 Tab 时,要不要主动断开 SSE 以省电省流量?
答案:可选。断开:页面 visibility 为 hidden 时 close EventSource,visible 时重连;省电省流量,但再切回来会有短暂「重连中」。不断开:保持连接,切回来立即有数据,但后台可能耗电。可根据产品策略选择;本项目未做 visibility 处理。
深度分析:若连接数或电量敏感,建议 visibility 隐藏时断开、显示时重连。
Q77:Nginx 反向代理 SSE 要配什么?
答案:proxy_read_timeout 调大(如 3600s),否则 Nginx 会主动断长连接。proxy_buffering off 关闭缓冲,让数据立即到客户端。X-Accel-Buffering: no(若用 Nginx 的 proxy 模块)禁止缓冲。Connection 与 Cache-Control 透传或按 SSE 要求设置。
深度分析:不关缓冲会导致 SSE 数据被缓冲,延迟很大或收不到。
Q78:服务端重启时,前端如何快速恢复?
答案:前端:onerror 后指数退避重连,成功即恢复。服务端:重启时优雅关闭(先不再接受新连接、等现有连接写完再退出),减少「连接突然断」的数量。网关:若有连接池或多实例,重启时做灰度或 drain,避免全部断。前端侧主要靠重连与状态展示。
深度分析:本项目前端重连已实现;服务端可加 graceful shutdown。
Q79:如何监控 SSE 连接健康度?
答案:前端:上报「连接建立、断开、重连次数、最后消息延迟」到 Datadog/Bugsnag 等。服务端:记录每连接建立/关闭、推送条数、错误数。指标:连接数、断线率、重连成功率、消息延迟分布。本项目可扩展 useSSEQuotes 内上报连接事件。
深度分析:监控便于发现地域/网络/上游问题,做 SLO 与告警。
Q80:SSE 和「离线优先」或 PWA 怎么结合?
答案:离线时 SSE 必然断,可展示上次缓存数据 + 「离线」标识;恢复在线后自动重连。PWA:Service Worker 不拦截 EventSource(或对 SSE URL 放行),保证后台页也能收数据;或 SW 不缓存 SSE 响应。若要做「离线可看上次行情」,用 Cache API 存 lastQuote,离线时读缓存展示。
深度分析:本项目未做 PWA;若做,SSE 与 SW 的配合需保证连接不被 SW 误拦截。
九、扩展与多路复用(Q81–Q90)
Q81:如何实现「按 symbol 订阅」?服务端怎么区分?
答案:前端:URL 带 query,如 /api/sse/quotes?symbol=BTC/USDT;或多条连接每 symbol 一个(不推荐多 symbol)。服务端:GET 时读 searchParams.get('symbol'),只推该 symbol,或维护「连接 → 订阅列表」按需推。Next.js Route 里用 request.nextUrl.searchParams.get('symbol')。
深度分析:本项目 Route 未读 symbol,扩展时在 GET 里读 query 再决定推送内容。
Q82:一条 SSE 里能混推「行情」和「系统通知」吗?
答案:能。用 event: 区分,如 event: quote\ndata: {...}\n\n 和 event: notification\ndata: {...}\n\n;前端 addEventListener('quote', ...) 和 addEventListener('notification', ...)。或 data 里统一用 type 字段,前端按 type 分支。本项目 data 里已有 type: 'quote',可扩展 type: 'notification'。
深度分析:单连接多类型可省连接数,需约定 event/type 规范。
Q83:SSE 能否和 WebSocket 共存在同一页面?连接数怎么控?
答案:能。例如 SSE 推行情、WebSocket 推订单状态,两者独立。连接数:浏览器同源 HTTP/1.1 约 6 个,SSE 占 1、WS 占 1,其余给普通请求;HTTP/2 下多路复用,压力小。同一页尽量 SSE 1 条 + WS 1 条,避免多开。
深度分析:按业务拆通道,避免单通道承载过多类型导致耦合与限流困难。
Q84:服务端用 Redis Pub/Sub 做多实例广播时,SSE 流程是什么?
答案:流程:前端连到某台 API 机 → 该机建 SSE 连接并 subscribe Redis channel(如 quotes:BTC/USDT)→ 上游或其它服务 publish 行情到 Redis → 该机收到消息后 write 到本机所有订阅该 channel 的 SSE 连接。这样多实例共享同一份行情源,每机只推给自己机上的连接。
深度分析:Redis 做「进程间广播」,每台 API 机维护本机 SSE 连接列表,收到 Redis 消息后写回前端。
Q85:SSE 的 Last-Event-ID 机制是什么?我们项目用了吗?
答案:机制:服务端每条消息可带 id: 123\n;断线重连时,浏览器(或手动)在请求头带 Last-Event-ID: 123,服务端从该 id 之后补发消息,实现「不丢消息」。本项目未用:当前是「重连后从新数据开始」,不补发历史。
深度分析:若要做不丢消息,需服务端维护一段消息历史(如环形队列)并按 id 补发;实现与存储成本较高。
Q86:如何做 SSE 的「心跳」?服务端还是客户端?
答案:服务端定期推心跳(如每 30s 一条 data: {"type":"ping"}\n\n),让连接保持活跃,避免中间代理因「长时间无数据」断连。客户端收到心跳可忽略或回写(若用 fetch 模拟 SSE 可回写,标准 EventSource 不能上行)。本项目未做心跳;若遇代理超时,可加服务端心跳。
深度分析:心跳间隔应小于代理/负载均衡的 read_timeout,如 15–30s。
Q87:前端用 fetch + ReadableStream 模拟 SSE 有什么优缺点?
答案:优点:可带 自定义 Header(如 Authorization)、可控制重连、可解析非标准格式。缺点:需自己解析流、自己实现重连与 Last-Event-ID、代码量大;且 fetch 的 body 消费后不能「重试」同一条连接。适用:需要 Header 鉴权且不能接受 query/Cookie 时。
深度分析:本项目用标准 EventSource,简单够用;若后续要带 Header,可考虑 fetch + ReadableStream。
Q88:SSE 和 GraphQL Subscription 的区别?
答案:SSE:通用 HTTP 流式推送,格式自定(如 JSON 行)。GraphQL Subscription:通常是 WebSocket 上跑 GraphQL 协议,订阅「某 query 的变更」。若后端是 GraphQL,Subscription 常用 WS;若后端是 REST/自定义,SSE 更简单。两者可并存:查询用 GraphQL query,推送用 SSE。
深度分析:选型看后端与团队技术栈;交易所前端用 SSE 或 WS 均可,不依赖 GraphQL。
Q89:如何做 SSE 的「多路复用」(一条连接多个逻辑流)?
答案:应用层多路复用:一条 SSE 里用 event: 或 data 里的 streamId/channel 区分,如 event: stream1\ndata: {...}\n\n 和 event: stream2\ndata: {...}\n\n,前端按 event 或 channel 分发。协议层:SSE 规范本身是一条流,多路复用靠应用层字段区分,不是多 TCP 流。
深度分析:本项目单 type: 'quote',扩展多 channel 时在 data 里加 channel 或 event 即可。
Q90:Edge Runtime(如 Vercel Edge)能跑 SSE 吗?
答案:能。Edge 支持 Web API 的 ReadableStream、Response,可写 SSE 流并返回。限制:Edge 有执行时间与内存限制,长时间 SSE 可能超时;且 Edge 无状态,多实例间无法共享连接列表,适合「单连接独立推」或「从上游拉一段再推」。长时间、多连接广播更适合 Node 常驻进程。
深度分析:若 SSE 在 Edge 上且需长连,需确认平台对长连与超时的支持。
十、面试话术与总结(Q91–Q100)
Q91:用一句话说清楚项目里 SSE 的用途和实现。
答案:「我们用 SSE 实时推送当前汇率/行情,服务端是 Next.js API Route 返回 text/event-stream 的 ReadableStream,按行推 data: { type, symbol, price, ts };前端用 EventSource 订阅,并做了断线指数退避重连和连接状态展示,首屏不依赖 SSE,用 BFF 快照先渲染,SSE 只做增量更新。」
深度分析:一句话里包含:用途(实时汇率/行情)、服务端形态(Route + Stream)、前端形态(EventSource + 重连 + 状态)、与首屏解耦。
Q92:SSE 和 WebSocket 选型时,你会怎么回答?
答案:「我们场景是服务端单向推行情,不需要客户端频繁上行,所以选 SSE:基于 HTTP、部署简单、易过网关、EventSource 自带解析与重连语义;WebSocket 更适合双向、低延迟、二进制场景,如订单簿或成交流,我们后续若有需要会再上 WS,和 SSE 并存。」
深度分析:突出「单向 vs 双向」「HTTP 友好 vs 独立协议」「实现成本」。
Q93:首屏和实时数据「解耦」具体怎么做的?
答案:「首屏用 BFF/React Query 拉一次行情快照或静态数据,先渲染;SSE 在页面加载后再建连,只负责后续增量更新,不参与首屏是否展示的逻辑。这样 LCP 和可交互时间不依赖 SSE 连接速度,弱网时也不会白屏。」
深度分析:解耦 = 数据源分离 + 建连时机分离 + 首屏不等待 SSE。
Q94:断线重连为什么用指数退避?
答案:「避免惊群:服务端故障时,固定间隔重连会让大量客户端同时撞上去,加重压力。指数退避让重连时间分散,并给服务端恢复时间;成功连接后重置退避,下次断线再从短间隔开始。我们设了上下限(1s–30s),兼顾恢复速度与对后端压力。」
深度分析:退避是分布式系统里通用的「避免同时重试」手段。
Q95:EventSource 不能带 Header,鉴权怎么做?
答案:「我们 SSE 走同源 Next.js API Route,鉴权用 Cookie 或 session,同源请求自动带 Cookie。若必须跨域或带 token,只能用 URL query(注意 token 进日志风险)或 BFF 代理:前端连同源 BFF,BFF 带 Header 连上游,把上游数据转成 SSE 推给前端。」
深度分析:同源 Cookie 最省事;跨域或强安全时 BFF 代理 + 服务端 token。
Q96:如何向非技术面试官解释 SSE?
答案:「就像订阅:你打开页面相当于『订阅了最新价格』,服务器会持续把最新价格推给你,不用你反复刷新。连接偶尔会断,我们做了自动重连和状态提示,断的时候你会看到『重连中』,连上后继续收最新价。」
深度分析:避免说协议细节,用「订阅、推送、自动重连」三个概念即可。
Q97:若让你从零设计「实时汇率」方案,你会考虑哪些点?
答案:「① 数据源:上游是 WS 还是 HTTP,需不需要 BFF 聚合。② 协议:单向用 SSE,双向用 WS。③ 鉴权与限流:谁能连、单用户连接数、按 IP 限流。④ 首屏:首屏不依赖推送,用快照先渲染。⑤ 断线重连:指数退避、状态展示、可选 Last-Event-ID。⑥ 监控:连接数、断线率、消息延迟。⑦ 多实例:Redis Pub/Sub 或网关做广播。」
深度分析:按「数据源 → 协议 → 安全 → 体验 → 运维」顺序答。
Q98:SSE 的 100 问里,你认为最常被问的是哪几道?
答案:「Q1/Q2(SSE 是什么、数据格式)、Q9/Q11(和 WebSocket 区别、为什么选 SSE)、Q21/Q22(EventSource 事件、指数退避)、Q41(首屏解耦)、Q51/Q55(鉴权、限连接)、Q71/Q73(断线原因、退避上限)、Q91(一句话总结)。」
深度分析:协议、选型、前端实现、安全、故障处理、总结是高频点。
Q99:本项目 SSE 可以继续做哪些优化?
答案:「① 服务端支持 query symbol,按 symbol 订阅。② 服务端 心跳,减少代理超时断连。③ 前端 节流更新,高频时每 100ms 最多渲染一次。④ 监控上报:连接建立/断开、重连次数、消息延迟。⑤ 多实例时 Redis Pub/Sub 做广播。⑥ Last-Event-ID 做断点续传(若业务需要不丢消息)。」
深度分析:按「功能、稳定性、可观测、扩展」列 3–5 条即可。
Q100:总结:SSE 在金融交易所前端的价值是什么?
答案:「在不增加首屏负担的前提下,提供实时汇率/行情,提升交易页的实时性与体验;通过 HTTP 友好的协议降低部署与网关成本,通过 断线重连与状态展示 提升弱网与故障下的可感知性。和 BFF、React Query、trading-core 一起,形成「首屏快、数据准、实时更新、资金安全」的完整链路。」
深度分析:价值 = 实时性 + 首屏解耦 + 协议与运维成本 + 可观测与体验。
本稿对应项目:apps/web/app/api/sse/quotes/route.ts、apps/web/hooks/useSSEQuotes.ts;面试时可结合代码说明「实时请求当前汇率」的实现与重连、首屏解耦策略。