资讯详情

资讯详情

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

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{constresponseawaitfetch(/api/orders/1001,{cache:no-store});if(!response.ok){thrownewError(查询失败HTTP${response.status});}constorderawaitresponse.json();console.log(订单状态,order.status);if(order.statusPAID||order.statusCLOSED){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 请求但服务端返回响应的时机变了浏览器请求“订单是否有更新”。如果已经有更新服务端立即响应。如果没有更新服务端暂时保留请求等待变化。出现变化或达到等待超时时间后服务端结束响应。浏览器收到响应后立即发起下一次长轮询。例如浏览器在第 0 秒发出请求订单在第 4 秒变为已支付服务端便在第 4 秒返回结果。浏览器不必等到固定的下一个查询时刻。为了表示“从哪条消息之后开始等待”长轮询接口也可以携带游标例如?after42。游标的含义、超时响应和历史保留范围都由接口自行约定。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事件类型未指定时默认是messageid事件标识可用于重连时报告接收位置retry设置重连等待时间单位为毫秒浏览器还可以根据网络情况延后重试事件以空行分隔。服务端只写出一行data:却没有发送事件结束的空行浏览器可能还不会触发事件回调。多行data:会用换行符拼接为同一个事件的数据。SSE 的数据格式是文本。二进制内容可以自行编码为文本传输但会增加体积与处理成本。3. 浏览器如何接收 SSE浏览器可以通过原生EventSource接收事件constsourcenewEventSource(/api/orders/1001/events);source.addEventListener(order-status,(event){constorderJSON.parse(event.data);console.log(订单状态,order.status);if(order.statusPAID||order.statusCLOSED){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: s3pPLMBiTxaQ9kYGzzhZRbKxOo随后双方交换 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. 浏览器如何收发消息constsocketnewWebSocket(wss://example.com/ws);socket.addEventListener(open,(){socket.send(JSON.stringify({type:subscribe-order,orderId:1001}));});socket.addEventListener(message,(event){constmessageJSON.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 连接”。图 2WebSocket 在持续连接中双向发送消息Webhook 在事件发生时调用接收方接口并接收本次 HTTP 响应。3. 验签如何判断通知来自可信发送方回调地址能够收到请求并不代表请求一定来自支付平台。接收方应按照平台约定验证签名再处理事件。例如GitHub 使用共享密钥与请求体计算 HMAC-SHA256并通过X-Hub-Signature-256传递签名。接收方应基于原始请求体字节验签避免先解析、重新序列化 JSON 后导致签名不匹配并采用适当的恒定时间比较方法。GitHub 验签文档。不同平台也可能采用证书或非对称签名。若协议提供签名时间戳应按约定检查有效时间范围同时通过事件标识等方式防止重放造成重复业务操作。不能把某个平台的签名字段当作所有 Webhook 的固定格式。支付场景还应核对订单归属、支付流水、金额和币种等业务信息不能只看到payment.succeeded就直接修改任意订单。4. 重试与重复通知为什么必须处理幂等假设商家已经处理成功但返回的 HTTP 响应在网络中丢失。支付平台可能认为通知失败再次发送同一个事件。因此接收方需要考虑同一个通知被重复投递以及同一个业务结果通过不同通知到达的情况。常见做法是按平台、事件 ID 等建立去重记录使用数据库唯一约束应对并发重复请求。结合支付流水和订单状态进行业务幂等控制避免重复记账、重复发货。将去重记录与相关业务变更放在合适的事务边界内避免标记已处理后业务操作却失败。只在内存中放一个“处理过的 ID”通常不足以应对重启和多实例并发。若业务会进一步调用外部系统还需要让下游操作支持幂等或通过可靠任务记录重试。Webhook 也不保证所有平台都会自动重试。截至本文核验时GitHub 文档明确说明不会自动重投失败通知Stripe 则提供自动重试并说明可能出现重复事件和乱序投递。接入时需要分别核对。GitHub 失败投递说明、Stripe Webhook 文档。5. 返回成功是否意味着业务已经完成这取决于接口约定与处理方式。如果业务处理很快可以在事务完成后返回成功。若处理较重可以先完成验签和必要校验将事件可靠地保存为待处理任务或交给具有相应持久化保证的队列再及时响应由后台继续处理。GitHub 实践建议。关键在于确认接收之前应确保后续处理不会因为进程重启而无声丢失。不能先返回成功再把唯一一份事件只放到内存线程池中。在异步接收方案中HTTP 成功响应表示接收方已经按约定接受通知不代表发货、积分发放等后续业务全部完成。后台处理还需要失败重试、告警和对账补偿。六、放在同一个业务中应该怎么选1. 按通信需求比较维度短轮询长轮询SSEWebSocketWebhook主要触发方式客户端定时查询客户端查询服务端等待后响应服务端向响应流写入事件双方主动发送消息事件触发一次回调请求是否长期等待或保持通道单次查询通常快速结束单次请求等待更新或超时持续保持事件流持续保持双向通道通常不为等待事件保留请求更新延迟的主要来源轮询间隔与请求耗时网络、处理和重新发起请求的间隙网络、处理和缓冲网络、处理和排队平台调度、网络和重试客户端上行操作普通 HTTP 请求普通 HTTP 请求通常另发 HTTP 请求可复用同一 WebSocket 通道接收方需要反向调用时另行设计主要维护成本请求量、频率与停止条件等待请求、超时与游标事件流、代理配置与恢复连接、订阅、重连与消息协议回调接口、验签、幂等与补偿常见场景低频状态刷新等待更新、已有系统兼容进度通知、文本流高频双向交互外部系统事件通知这张表比较的是常见使用方式不能据此给出固定的性能排名。数据频率、在线人数、消息大小、代理行为和服务端实现都会影响资源开销与实际延迟。例如一个访问量较小、每十几秒刷新一次的页面短轮询可能已经足够如果为了少量通知引入复杂的连接管理整体收益未必明显。2. 支付平台、业务后端与浏览器如何配合对于前文的支付页面可以设计下面的链路图 3浏览器提前建立 SSE 订阅支付平台用 Webhook 通知业务后端后端提交订单状态后再向页面发送更新。具体过程是浏览器打开订单页面查询当前状态并建立 SSE 订阅。用户完成支付支付平台通过 Webhook 通知业务后端。后端验签、校验业务信息幂等更新订单并提交事务。后端按约定向支付平台响应成功同时安排向页面发送状态事件。页面收到事件后显示“支付成功”并按需要关闭订阅。图中第 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读取并解析流。参考资料IETFRFC 9112 — HTTP/1.1连接管理与持久连接IETFRFC 6202 — Long Polling 与 HTTP Streaming 的问题和实践WHATWG HTML StandardServer-sent eventsMDNUsing server-sent eventsNginxproxy_buffering 与 X-Accel-BufferingIETFRFC 6455 — The WebSocket ProtocolIETFRFC 8441 — Bootstrapping WebSockets with HTTP/2WHATWGWebSockets StandardGitHub DocsAbout webhooksGitHub DocsBest practices for using webhooksGitHub DocsHandling failed webhook deliveriesGitHub DocsValidating webhook deliveriesStripe DocsWebhooks重试、顺序与重复事件处理
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →