☰
HTTP 轮询、SSE、WebSocket 与 Webhook:原理、区别与适用场景
2026/10/23 3:05:15 网站建设 项目流程

HTTP 轮询、SSE、WebSocket 与 Webhook:原理、区别与适用场景

文章摘要

订单支付完成后,页面如何及时显示“支付成功”?AI 生成回答时,为什么文字能够逐段出现?GitHub 收到代码提交后,又如何通知部署系统?这些需求都涉及数据更新通知,但通信双方和交互方式并不相同。本文从普通 HTTP 请求与响应出发,介绍短轮询、长轮询、SSE、WebSocket 和 Webhook 的通信过程,结合简短示例比较它们的适用场景,并说明自动重连、消息补发、Webhook 验签与幂等处理的边界。

本文聚焦通信机制与选型。JavaScript 示例运行于浏览器,HTTP 报文只展示与讲解有关的字段;示例接口需要由服务端实现,不构成一套可直接部署的完整应用。文中不涉及特定 JDK 或 Java 框架版本。

一、从一次普通 HTTP 查询说起

浏览器通过 HTTP 查询数据时,服务端通常返回查询时的结果,本次响应随后结束。后端的数据还可能继续变化,但页面拿到的结果不会自动跟着更新。

例如,订单页面向服务端查询订单1001的支付状态:

GET /api/orders/1001 HTTP/1.1 Host: shop.example.com

服务端返回的响应体是:

{"orderId":"1001","status":"PENDING"}

PENDING表示待支付,页面据此显示“等待支付”。

如果用户随后完成支付,后端把订单状态更新为PAID,页面仍然显示之前查询到的“等待支付”。因为先前的响应已经结束,服务端不能再往其中追加新的状态。

要让页面显示最新结果,就需要让它获取后续变化。最直接的办法是再次查询:把这个查询过程按一定规则反复执行,就是轮询。

二、HTTP 轮询:客户端不断询问有没有变化

1. 短轮询:每隔一段时间查询一次

短轮询最直观:浏览器每隔几秒查询订单,服务端每次返回当时的状态。

例如,每隔 3 秒查询一次:

查询时刻服务端返回
第 0 秒PENDING,待支付
第 3 秒PENDING,待支付
第 6 秒PAID,已支付

如果支付发生在第 4 秒,页面要等到第 6 秒的查询才会发现变化。

下面用浏览器代码展示这个过程:

asyncfunctionpollOrder(){try{constresponse=awaitfetch("/api/orders/1001",{cache:"no-store"});if(!response.ok){thrownewError(`查询失败:HTTP${response.status}`);}constorder=awaitresponse.json();console.log("订单状态:",order.status);if(order.status==="PAID"||order.status==="CLOSED"){return;// 到达终态后,停止轮询}}catch(error){console.error(error);}setTimeout(pollOrder,3000);}pollOrder();

这里在本次查询结束后再等待 3 秒,可以避免使用固定间隔的setInterval时,前一次请求尚未完成,下一次请求已经发出的情况。因此,这段代码的两次请求起点之间,还包含前一次查询的耗时。

示例只保留轮询主线。实际页面还需要在退出时取消查询,为请求设置超时,并对连续失败采用退避策略。订单状态接口也应合理设置缓存策略,避免读到过期结果。

轮询频率决定了更新延迟与请求量之间的取舍。

如果有N个页面持续查询,每个页面每隔T秒发出一次请求,忽略请求耗时和重试时:

平均查询请求数/秒 ≈ N / T

例如,10,000 个页面每 5 秒查询一次,大约产生 2,000 次查询请求/秒。即使订单没有任何变化,这些查询也会继续发生。

在“严格每隔T秒查询、状态变化相对查询时刻均匀分布、忽略网络与处理耗时”的简化条件下,发现变化的等待时间平均约为T / 2,最长接近T。这只是估算,不能直接当作接口的实际延迟指标。

短轮询实现简单,适合数据更新不频繁、允许几秒延迟、在线规模可控的场景。

2. 长轮询:没有变化时,先不结束响应

长轮询仍然由客户端发起 HTTP 请求,但服务端返回响应的时机变了:

  1. 浏览器请求“订单是否有更新”。
  2. 如果已经有更新,服务端立即响应。
  3. 如果没有更新,服务端暂时保留请求,等待变化。
  4. 出现变化,或达到等待超时时间后,服务端结束响应。
  5. 浏览器收到响应后,立即发起下一次长轮询。

例如,浏览器在第 0 秒发出请求,订单在第 4 秒变为已支付,服务端便在第 4 秒返回结果。浏览器不必等到固定的下一个查询时刻。

为了表示“从哪条消息之后开始等待”,长轮询接口也可以携带游标,例如?after=42。游标的含义、超时响应和历史保留范围,都由接口自行约定。RFC 6202 讨论了长轮询及 HTTP 流式传输的工作方式与实际问题。

长轮询减少了无更新时的频繁查询,但仍有成本:服务端需要维护等待中的请求、连接资源和超时状态;每次响应结束后,客户端还要重新发起请求。更新非常频繁时,请求与响应也会频繁发生。

“请求一直等待”也不意味着“必须占用一个线程原地睡眠”。采用异步 I/O 的实现可以挂起请求,而不为每个等待请求独占工作线程;具体资源开销取决于服务端实现。

图 1:短轮询定时查询,长轮询等待更新或超时后结束本次响应,再发起下一次请求。时间沿纵向向下。

3. HTTP 长连接与长轮询有什么区别

概念关注的问题
HTTP 长连接、持久连接一次响应结束以后,底层连接能否继续复用
HTTP 长轮询没有更新时,本次请求是否等待一段时间再响应

HTTP/1.1 支持持久连接,一次响应结束后,底层连接仍然可以继续复用,处理后续请求。响应结束与底层连接关闭是两件事。参见 HTTP/1.1 连接管理规范。

短轮询同样可以复用 HTTP 连接。长轮询的一次响应结束后,也不一定需要重新建立 TCP 连接。

因此,不能用“是否反复建立 TCP 连接”来判断短轮询和长轮询。

三、SSE:在一个 HTTP 响应中持续发送事件

SSE 的全称是 Server-Sent Events,即服务端发送事件。

它的核心过程是:客户端先发起 HTTP 请求,服务端返回一个持续输出的事件流。后续事件继续写入这个响应体,不必每发一条事件就结束响应。

1. SSE 与长轮询的关键区别

假设系统先后产生“订单已支付”和“订单已发货”两条更新:

  • 长轮询:响应第一条更新后结束本次响应,客户端再发起请求等待下一条。
  • SSE:可以在同一个响应流中先后发送两条更新。

这里的“持续连接”主要描述应用层的事件流。SSE 也可以运行在 HTTP/2 上,此时一个事件流不必独占一条 TCP 连接,多个 HTTP 流可以复用连接。

2. SSE 发送的数据是什么样的

服务端使用Content-Type: text/event-stream,事件流按 UTF-8 解码。下面展示响应头和解码后的响应体结构,省略 HTTP 传输分帧细节:SSE 规范。

HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache retry: 3000 id: 42 event: order-status data: {"orderId":"1001","status":"PAID"} id: 43 event: order-status data: {"orderId":"1001","status":"SHIPPED"}

这些字段的含义是:

字段含义
data事件内容,可以约定为 JSON 文本
event事件类型;未指定时默认是message
id事件标识,可用于重连时报告接收位置
retry设置重连等待时间,单位为毫秒;浏览器还可以根据网络情况延后重试

事件以空行分隔。服务端只写出一行data:,却没有发送事件结束的空行,浏览器可能还不会触发事件回调。多行data:会用换行符拼接为同一个事件的数据。

SSE 的数据格式是文本。二进制内容可以自行编码为文本传输,但会增加体积与处理成本。

3. 浏览器如何接收 SSE

浏览器可以通过原生EventSource接收事件:

constsource=newEventSource("/api/orders/1001/events");source.addEventListener("order-status",(event)=>{constorder=JSON.parse(event.data);console.log("订单状态:",order.status);if(order.status==="PAID"||order.status==="CLOSED"){source.close();// 本页面只等待支付结果,到达终态后主动关闭}});source.onerror=()=>{console.warn("事件流发生错误,请检查连接状态和服务端响应");};

这里使用addEventListener("order-status", ...),对应服务端的event: order-status。onmessage用来接收类型为message的事件,不能代替所有自定义事件的监听器。EventSource 使用说明。

这个页面收到支付终态后会关闭事件流,因此不会继续接收后续发货事件。如果页面需要持续展示物流变化,就应调整关闭条件。

对于可恢复的连接中断,原生EventSource会尝试重连;调用close()会停止连接与重连。不能把所有错误都理解为“浏览器一定会一直重试”,例如规范规定服务端可以用 HTTP204告知客户端停止重连。SSE 处理模型。

4. 自动重连不等于自动补齐消息

假设客户端已经接收到id: 42,随后断线。原生EventSource在该对象的重连过程中,可以通过Last-Event-ID: 42报告位置。Last-Event-ID 规范。

但这个请求头本身不会保存事件,也不会让数据库自动补发。

如果业务要求从断点继续,服务端需要保存可回放的事件、根据 ID 查找后续记录,并约定历史已过期时如何恢复。客户端也要考虑重复事件的处理。这个 ID 表示事件流的接收位置,不是业务处理成功的确认;重新打开页面并创建新的EventSource,也不能默认继承上一次页面的进度。

对只关心最终状态的订单页面,一种较简单的恢复方式是:重新连接后先查询最新状态,再继续监听变化。对需要保留每一段输出的文本流,则需要设计更明确的序号和补发机制。

5. 为什么服务端已经发送,浏览器却很久没有显示

除了应用层及时写出、刷新数据,还要检查中间代理是否缓冲响应。事件流若被积攒后成批转发,页面就会失去逐条更新的效果。

例如 Nginx 可以通过proxy_buffering off关闭对应代理位置的响应缓冲,也支持上游用X-Accel-Buffering: no控制,但该响应头的处理还受 Nginx 配置影响。应对照实际代理配置排查,不能只加一个响应头就认为已经解决。Nginx 官方文档。

空闲时可以发送: keepalive这样的注释行作为心跳,并匹配网关的空闲超时配置。注释不触发业务消息回调,心跳也不能保证所有基础设施都允许无限时长的连接。

SSE 适合服务端主要负责持续推送文本、客户端操作可以通过普通 HTTP 提交的场景,例如进度通知和 AI 文本流。

还要注意,SSE 事件格式与原生EventSourceAPI 不是同一个概念。原生构造器不提供设置任意请求头和 POST 请求体的选项。如果业务必须通过 POST 提交问题,或携带自定义Authorization头,可以通过fetch读取流并解析事件,但解析、重连与进度恢复需要由所用代码或库承担。

四、WebSocket:连接建立后,双方都能主动发送消息

SSE 主要解决服务端持续向客户端发送数据的问题。如果客户端也需要频繁发送消息,例如聊天、协同编辑或游戏操作,就可以考虑 WebSocket。

WebSocket 提供全双工通信:连接建立后,客户端与服务端都可以主动发送消息,不需要每条消息都对应一次 HTTP 请求与响应。WebSocket 协议规范。

1. WebSocket 如何建立连接

以常见的 HTTP/1.1 握手为例,客户端先请求升级协议:

GET /ws HTTP/1.1 Host: chat.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Origin: https://chat.example.com

服务端接受连接后,返回类似下面的响应:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

随后双方交换 WebSocket 帧。上面的 Key 与 Accept 是规范中的配对示例;实际客户端应为每次连接生成随机 Key。这个握手校验也不能代替用户身份认证。RFC 6455。

因此,WebSocket 可以借助 HTTP 建立连接,但建立后传输的是 WebSocket 消息。不能把它理解成“持续发送普通 HTTP 请求”。

这里的Upgrade和101特指 HTTP/1.1 的握手过程。HTTP/2 上的 WebSocket 建立方式由扩展 CONNECT 机制支持,不能把这份 HTTP/1.1 报文套用于所有版本。RFC 8441。

2. 浏览器如何收发消息

constsocket=newWebSocket("wss://example.com/ws");socket.addEventListener("open",()=>{socket.send(JSON.stringify({type:"subscribe-order",orderId:"1001"}));});socket.addEventListener("message",(event)=>{constmessage=JSON.parse(event.data);console.log("收到服务端消息:",message);});socket.addEventListener("close",()=>{console.log("连接已关闭");});

wss://表示通过 TLS 保护的 WebSocket 连接。示例中的地址是占位地址,subscribe-order是自行约定的业务消息类型,并不是 WebSocket 内置功能。

示例还约定所有收到的消息都是 JSON 文本。WebSocket 本身支持文本和二进制消息,实际应用应按约定判断消息类型并处理解析错误。

3. 双向通信能力之外,还要设计什么

WebSocket 解决了消息通道问题,但应用仍需处理:

  • 连接存活:协议有 Ping/Pong 控制帧。浏览器 JavaScript 原生 API 没有直接发送这类控制帧的方法;用socket.send("ping")发出的是业务消息,需要双方自行约定处理。浏览器 WebSocket 标准。
  • 断线重连:原生WebSocket不提供自动重连,需要应用或库重新创建连接,并控制退避、重试和订阅恢复。
  • 消息确认:send()返回不代表对方已经完成业务处理。关键消息需要按业务要求增加消息 ID、确认与重试。
  • 离线恢复:连接断开期间的历史消息,需要持久化、同步或补拉机制。
  • 服务端路由:多实例部署时,要知道目标用户连接在哪个实例上,再将通知送达持有连接的实例。

如果聊天消息需要离线保存,就应把“消息已入库”“对方已收到”“对方已读”区分开来。使用 WebSocket 本身不会自动完成这些业务状态的管理。

五、Webhook:事件发生后,向对方的接口发送请求

Webhook 常用于系统之间的事件通知。接收方提供回调地址,发送方在事件发生后向这个地址发起 HTTP 请求。GitHub 对 Webhook 的说明。

例如,商家在支付平台配置:

https://shop.example.com/webhooks/payment

支付完成以后,支付平台向商家后端发送通知:

POST /webhooks/payment HTTP/1.1 Host: shop.example.com Content-Type: application/json

请求体示意如下,字段由接口双方约定:

{"eventId":"evt_10001","type":"payment.succeeded","data":{"orderId":"1001","paymentId":"pay_90001"}}

这是一种事件回调模式。它没有像 WebSocket 那样统一的消息帧协议;事件结构、鉴权方式、成功响应、重试规则,都应以具体平台的接口约定为准。

1. Webhook 与 SSE 的“服务端主动发送”有什么区别

对比项SSEWebhook
事先做什么客户端发起请求,建立事件流接收方配置回调接口或订阅
事件发生时服务端向已有 HTTP 响应流写入事件发送方向回调地址发起新的 HTTP 请求
接收方的条件能发起请求并持续读取响应有发送方能够访问的 HTTP 接口
常见接收方浏览器、客户端业务后端、自动化系统

浏览器页面通常没有供外部平台直接调用的入站 HTTP 接口。支付平台因此一般通知商家后端,再由商家后端向页面提供订单状态。

2. Webhook 与 WebSocket 的通信方式有什么区别

WebSocket 先建立持续连接,之后双方都可以通过这个通道主动发送消息。Webhook 则在事件发生时,向接收方的回调地址发送一次 HTTP 请求,并等待本次请求的响应。

Webhook 不要求为了等待事件而一直挂起一个请求,但每次通知的 HTTP 请求仍然可以复用底层连接。不要把它简单理解为“每次通知都要新建 TCP 连接”。

图 2:WebSocket 在持续连接中双向发送消息;Webhook 在事件发生时调用接收方接口,并接收本次 HTTP 响应。

3. 验签:如何判断通知来自可信发送方

回调地址能够收到请求,并不代表请求一定来自支付平台。接收方应按照平台约定验证签名,再处理事件。

例如,GitHub 使用共享密钥与请求体计算 HMAC-SHA256,并通过X-Hub-Signature-256传递签名。接收方应基于原始请求体字节验签,避免先解析、重新序列化 JSON 后导致签名不匹配,并采用适当的恒定时间比较方法。GitHub 验签文档。

不同平台也可能采用证书或非对称签名。若协议提供签名时间戳,应按约定检查有效时间范围;同时通过事件标识等方式防止重放造成重复业务操作。不能把某个平台的签名字段当作所有 Webhook 的固定格式。

支付场景还应核对订单归属、支付流水、金额和币种等业务信息,不能只看到payment.succeeded就直接修改任意订单。

4. 重试与重复通知:为什么必须处理幂等

假设商家已经处理成功,但返回的 HTTP 响应在网络中丢失。支付平台可能认为通知失败,再次发送同一个事件。

因此,接收方需要考虑同一个通知被重复投递,以及同一个业务结果通过不同通知到达的情况。

常见做法是:

  1. 按平台、事件 ID 等建立去重记录,使用数据库唯一约束应对并发重复请求。
  2. 结合支付流水和订单状态进行业务幂等控制,避免重复记账、重复发货。
  3. 将去重记录与相关业务变更放在合适的事务边界内,避免标记已处理后,业务操作却失败。

只在内存中放一个“处理过的 ID”,通常不足以应对重启和多实例并发。若业务会进一步调用外部系统,还需要让下游操作支持幂等,或通过可靠任务记录重试。

Webhook 也不保证所有平台都会自动重试。截至本文核验时,GitHub 文档明确说明不会自动重投失败通知;Stripe 则提供自动重试,并说明可能出现重复事件和乱序投递。接入时需要分别核对。GitHub 失败投递说明、Stripe Webhook 文档。

5. 返回成功,是否意味着业务已经完成

这取决于接口约定与处理方式。

如果业务处理很快,可以在事务完成后返回成功。若处理较重,可以先完成验签和必要校验,将事件可靠地保存为待处理任务,或交给具有相应持久化保证的队列,再及时响应,由后台继续处理。GitHub 实践建议。

关键在于:确认接收之前,应确保后续处理不会因为进程重启而无声丢失。不能先返回成功,再把唯一一份事件只放到内存线程池中。

在异步接收方案中,HTTP 成功响应表示接收方已经按约定接受通知,不代表发货、积分发放等后续业务全部完成。后台处理还需要失败重试、告警和对账补偿。

六、放在同一个业务中,应该怎么选

1. 按通信需求比较

维度短轮询长轮询SSEWebSocketWebhook
主要触发方式客户端定时查询客户端查询,服务端等待后响应服务端向响应流写入事件双方主动发送消息事件触发一次回调请求
是否长期等待或保持通道单次查询通常快速结束单次请求等待更新或超时持续保持事件流持续保持双向通道通常不为等待事件保留请求
更新延迟的主要来源轮询间隔与请求耗时网络、处理和重新发起请求的间隙网络、处理和缓冲网络、处理和排队平台调度、网络和重试
客户端上行操作普通 HTTP 请求普通 HTTP 请求通常另发 HTTP 请求可复用同一 WebSocket 通道接收方需要反向调用时另行设计
主要维护成本请求量、频率与停止条件等待请求、超时与游标事件流、代理配置与恢复连接、订阅、重连与消息协议回调接口、验签、幂等与补偿
常见场景低频状态刷新等待更新、已有系统兼容进度通知、文本流高频双向交互外部系统事件通知

这张表比较的是常见使用方式,不能据此给出固定的性能排名。数据频率、在线人数、消息大小、代理行为和服务端实现,都会影响资源开销与实际延迟。

例如,一个访问量较小、每十几秒刷新一次的页面,短轮询可能已经足够;如果为了少量通知引入复杂的连接管理,整体收益未必明显。

2. 支付平台、业务后端与浏览器如何配合

对于前文的支付页面,可以设计下面的链路:

图 3:浏览器提前建立 SSE 订阅;支付平台用 Webhook 通知业务后端,后端提交订单状态后,再向页面发送更新。

具体过程是:

  1. 浏览器打开订单页面,查询当前状态,并建立 SSE 订阅。
  2. 用户完成支付,支付平台通过 Webhook 通知业务后端。
  3. 后端验签、校验业务信息,幂等更新订单并提交事务。
  4. 后端按约定向支付平台响应成功,同时安排向页面发送状态事件。
  5. 页面收到事件后显示“支付成功”,并按需要关闭订阅。

图中第 4、5 步展示了一种可能的先后顺序。向支付平台响应与向浏览器推送是两个动作,不要求严格按图中的顺序执行,但都应符合各自的业务约定,避免在状态未提交时就通知成功。

如果订单事务提交与消息发送之间存在故障窗口,可以在事务中一并写入待发送事件,再由后台投递。这是一种应用层可靠性设计,并不是 SSE 或 Webhook 自动提供的能力。

订阅也要校验用户是否有权访问订单。多实例部署时,状态事件需要送到持有目标订阅的实例;不能默认处理 Webhook 的实例恰好持有用户的 SSE 连接。

此外,“先查询,再订阅”之间可能刚好发生状态变化。服务端可以在订阅建立后发送最新状态快照,或结合版本、游标协调快照与增量事件;页面重新进入或重连后也应重新校准状态。通知丢失不能改变数据库中的真实订单结果。

如果页面允许几秒延迟,也可以直接轮询订单状态。支付平台到后端仍然使用 Webhook,页面端不必因此使用 SSE。

3. 几类常见需求的选择思路

  • 只需要偶尔刷新状态:先考虑普通查询或短轮询,确认更新延迟与请求量是否可接受。
  • 主要是服务端持续推送文本:考虑 SSE,例如任务进度、告警列表、AI 文本输出;用户提交操作仍可走普通 HTTP。
  • 双方需要频繁实时发送消息:考虑 WebSocket,并设计连接恢复与业务消息协议。
  • 接收外部系统的事件:平台支持时使用 Webhook,并处理验签、重复通知和失败补偿。
  • 已有系统采用长轮询:结合基础设施、兼容性与更新频率评估,不必仅因为存在新方案就重写。

七、几个容易混淆的问题

1. SSE 是单向的,浏览器就不能提交操作了吗

可以提交。“单向”描述 SSE 事件流中的数据方向:服务端向客户端发送事件。浏览器仍然可以通过普通 HTTP 请求创建订单、取消任务或发送问题。

所以,“HTTP 提交操作 + SSE 接收结果”是一种可以配合使用的设计。

2. 使用 WebSocket,就一定比轮询更好吗

不一定。WebSocket 适合持续双向交互,但会引入连接管理、重连、订阅恢复等工作。对更新稀疏、延迟要求不高的页面,轮询可能更容易实现和维护。

比较方案时,需要同时考虑业务实时性、在线规模和运维成本。

3. TCP 可靠,业务消息就一定不会丢了吗

TCP 的可靠传输不等于业务已经执行成功。服务端可能收到消息后崩溃,客户端也可能在处理事件前退出;连接重建后,还会出现历史恢复与重复处理的问题。

需要可靠交付的业务,应明确持久化时机、确认语义、重试条件和幂等边界。

4. Webhook 返回 HTTP 200,就可以认为订单处理完成了吗

需要看约定。同步方案可以在事务完成后返回;异步接收方案可能只是确认事件已被可靠接收,后续业务仍在执行。发送方与接收方应明确“接收成功”和“业务完成”的区别。

5. AI 回答逐字出现,就一定用了 SSE 吗

不能只从页面效果判断。SSE、普通 HTTP 流式响应、WebSocket 都可以实现逐步展示,前端也可能把一次性返回的文本做成逐字动画。

要确认实际机制,应查看请求、响应头、响应内容和客户端读取方式。采用 SSE 格式时,也不代表客户端一定使用原生EventSource,它还可能通过fetch读取并解析流。

参考资料

  1. IETF:RFC 9112 — HTTP/1.1,连接管理与持久连接
  2. IETF:RFC 6202 — Long Polling 与 HTTP Streaming 的问题和实践
  3. WHATWG HTML Standard:Server-sent events
  4. MDN:Using server-sent events
  5. Nginx:proxy_buffering 与 X-Accel-Buffering
  6. IETF:RFC 6455 — The WebSocket Protocol
  7. IETF:RFC 8441 — Bootstrapping WebSockets with HTTP/2
  8. WHATWG:WebSockets Standard
  9. GitHub Docs:About webhooks
  10. GitHub Docs:Best practices for using webhooks
  11. GitHub Docs:Handling failed webhook deliveries
  12. GitHub Docs:Validating webhook deliveries
  13. Stripe Docs:Webhooks,重试、顺序与重复事件处理

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询