# WebSocket 订阅使用指南

> 原文地址: https://docs.blockvectra.com/zh/guides/websocket-subscriptions/

BlockVectra 提供基于 TLS 的安全 WebSocket（`wss://`）连接，支持在单一长连接中处理实时以太坊事件订阅与标准 JSON-RPC 2.0 请求。

## 可用链

WebSocket 支持按链动态开放。客户端可通过公开的 `GET /v1/chains` 端点读取每个网络的 `ws`（布尔值）和 `subscriptions`（支持的订阅类型数组）确认实时状态。

下表列出当前已启用 WebSocket 的网络：

| 链 | {chain} | WebSocket 端点（路径携带 Key） | 支持的订阅类型 |
| --- | --- | --- | --- |
| Robinhood Chain | `robinhood_mainnet` | `wss://api.blockvectra.com/v1/robinhood_mainnet/{api_key}` | `newHeads, logs` |

## 连接与鉴权

客户端通过标准 TLS 建立 WebSocket 连接（`wss://`）。API key 支持两种传入方式：

* **路径携带 Key**：`wss://api.blockvectra.com/v1/{chain}/{api_key}`
* **请求头携带 Key**：`wss://api.blockvectra.com/v1/{chain}`，在 HTTP 升级（Upgrade）握手阶段传入 `x-api-key: {api_key}` 或 `Authorization: Bearer {api_key}` 请求头。

### 握手阶段准入检查

在建立 WebSocket 会话前，服务端会在 HTTP 升级握手阶段进行准入校验：

* **身份认证**：未提供 API key 返回 HTTP 401（[`missing_api_key`](https://docs.blockvectra.com/zh/errors/#missing_api_key)）；key 未知、已停用或已吊销返回 HTTP 401（[`invalid_api_key`](https://docs.blockvectra.com/zh/errors/#invalid_api_key)）；若服务端 key 表数据同步暂不可用则返回 HTTP 503（[`auth_unavailable`](https://docs.blockvectra.com/zh/errors/#auth_unavailable)）。
* **账户余额**：账户预付余额为 0 或负数返回 HTTP 402（[`balance_exhausted`](https://docs.blockvectra.com/zh/errors/#balance_exhausted)）；计费状态暂未确认返回 HTTP 503（[`billing_unavailable`](https://docs.blockvectra.com/zh/errors/#billing_unavailable)）。
* **连接数限制**：单 key 连接数超过每实例 20 条，或单账户连接数超过每实例 50 条返回 HTTP 429（[`ws_connection_limit`](https://docs.blockvectra.com/zh/errors/#ws_connection_limit)）。
* **链可用性**：请求未知或未开放的链返回 HTTP 404（[`unknown_chain`](https://docs.blockvectra.com/zh/errors/#unknown_chain)）。
* **实例容量**：当服务端实例连接数饱和或排队推送消息超过阈值时，握手返回 HTTP 503（[`overloaded`](https://docs.blockvectra.com/zh/errors/#overloaded)）并附带 `Retry-After` 头。

连接建立后，客户端可以发送标准 JSON-RPC 2.0 请求（如 `eth_blockNumber` 或 `eth_call`）以及 UTF-8 文本帧格式的订阅控制方法。

## 订阅方法

服务端支持以太坊标准的发布/订阅接口：`eth_subscribe` 与 `eth_unsubscribe`。

### `newHeads`

当新区块追加到链头时，实时推送区块头对象。

* **订阅请求**：
  ```json
  {"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}
  ```
* **订阅响应**：返回十六进制订阅标识符：
  ```json
  {"jsonrpc":"2.0","id":1,"result":"0x1"}
  ```
* **推送通知帧**：
  ```json
  {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x1","result":{"number":"0x1b4","hash":"0x..."}}}
  ```

### `logs`

推送与指定过滤条件匹配的事件日志。

* **过滤条件硬性要求**：每个 `logs` 订阅过滤器**必须**指定 `address`（合约地址或地址数组）或 `topic0`（第一位置的主题，非 null）。两者均未指定的过滤请求（例如 `{}` 或 `{"topics":[null,"0x..."]}`）会被服务端拒绝，返回错误码 `-32602`（[`logs_filter_required`](https://docs.blockvectra.com/zh/errors/#logs_filter_required)）。

* **过滤规模限制**：单次订阅最多指定 100 个地址；主题最多 4 个位置，每个位置最多 16 个候选哈希。

* **实例过滤容量**：若服务端实例上活跃的日志过滤器达到上限，订阅返回错误码 `-32022`（[`ws_filter_capacity`](https://docs.blockvectra.com/zh/errors/#ws_filter_capacity)）。

* **链重组**：发生区块重组时，被移除区块的日志推送通知中会标记 `"removed": true`。

* **订阅请求**：
  ```json
  {"jsonrpc":"2.0","id":2,"method":"eth_subscribe","params":["logs",{"address":"0x1234567890123456789012345678901234567890"}]}
  ```

### `eth_unsubscribe`

通过订阅标识符取消指定订阅。

* **退订请求**：
  ```json
  {"jsonrpc":"2.0","id":3,"method":"eth_unsubscribe","params":["0x1"]}
  ```
* **退订响应**：
  ```json
  {"jsonrpc":"2.0","id":3,"result":true}
  ```

## 运行示例

**viem v2 (TypeScript)**

使用 [viem](https://viem.sh) v2 的 `createPublicClient` 与 `webSocket` 传输层连接。请将 `{chain}` 替换为目标链标识，`{api_key}` 替换为你的 API key：

```ts
import { createPublicClient, webSocket } from 'viem';

const apiKey = process.env.BLOCKVECTRA_API_KEY || '{api_key}';
const url = `wss://api.blockvectra.com/v1/{chain}/${apiKey}`;

const client = createPublicClient({
  transport: webSocket(url),
});

// 1. 订阅新区块头 (newHeads)
const unwatchBlocks = client.watchBlocks({
  onBlock: (block) => {
    console.log('收到新区块头:', block.number, block.hash);
  },
  onError: (error) => {
    console.error('watchBlocks 错误:', error);
  },
});

// 2. 订阅合约事件日志 (logs 过滤必须包含 address 或 topic0)
const unwatchEvents = client.watchEvent({
  address: '0x1234567890123456789012345678901234567890',
  onLogs: (logs) => {
    console.log('收到匹配日志:', logs);
  },
  onError: (error) => {
    console.error('watchEvent 错误:', error);
  },
});
```


  **命令行 (websocat / wscat)**

通过命令行工具 `websocat` 或 `wscat` 建立连接并发送原始 JSON-RPC：

```bash
# 使用 websocat 连接（路径携带 API key）
websocat "wss://api.blockvectra.com/v1/{chain}/{api_key}"

# 或在请求头中传入 API key
websocat -H="x-api-key: {api_key}" "wss://api.blockvectra.com/v1/{chain}"

# 或使用 wscat 连接
wscat -c "wss://api.blockvectra.com/v1/{chain}/{api_key}"
```

在交互会话中发送订阅指令：

```json
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}
{"jsonrpc":"2.0","id":2,"method":"eth_subscribe","params":["logs",{"address":"0x1234567890123456789012345678901234567890"}]}
{"jsonrpc":"2.0","id":3,"method":"eth_unsubscribe","params":["0x1"]}
```


## 关闭码与客户端处理

服务端主动终止 WebSocket 连接时会发送带有特定关闭码与简短原因的 Close 帧。下表列出服务端发出的关闭码及建议的处理方式：

|                      关闭码 | 原因字符串                            | 描述                                                                                                                             | 能否重试 | 客户端动作                                                                         |
| -----------------------: | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | :--: | ----------------------------------------------------------------------------- |
| [1001](https://docs.blockvectra.com/zh/errors/#1001) | `idle`                           | 连接在 3600 秒（1 小时）内无活跃订阅且未收到任何消息                                                                                                 |   是  | 按需重新连接。                                                                       |
| [1003](https://docs.blockvectra.com/zh/errors/#1003) | `binary frames are not accepted` | 客户端发送了二进制帧；服务端仅接收 UTF-8 文本帧                                                                                                    |   否  | 不要自动重连。修改客户端以发送纯文本帧。                                                          |
| [1009](https://docs.blockvectra.com/zh/errors/#1009) | `message too large`              | 客户端入站消息超过 1 MiB（1,048,576 字节）限制                                                                                                |   否  | 不要自动重连。拆分大请求或缩减请求体大小。                                                         |
| [1012](https://docs.blockvectra.com/zh/errors/#1012) | `service restart`                | 服务端实例维护重启，或单连接已达最长生命周期（24 小时 ± 10% 抖动）                                                                                         |   是  | 使用带随机抖动的退避策略重连，重新建立订阅并补全数据。                                                   |
| [1013](https://docs.blockvectra.com/zh/errors/#1013) | `chain unavailable`              | 链在此监听端口停止接受 WebSocket 会话，或上游数据源断开                                                                                              |   是  | 使用带全抖动的指数退避重连，重建订阅并通过 `eth_getLogs` 补全数据。                                     |
| [1013](https://docs.blockvectra.com/zh/errors/#1013) | `overloaded`                     | 服务端实例所有连接的排队通知缓冲（256 MiB）达到上限                                                                                                  |   是  | 使用带全抖动的指数退避重连，重建订阅并通过 `eth_getLogs` 补全数据。                                     |
| [4402](https://docs.blockvectra.com/zh/errors/#4402) | `insufficient balance`           | 账户预付余额已耗尽                                                                                                                      |   否  | 不要自动重连。[充值后再重连](https://docs.blockvectra.com/zh/guides/billing-rules/)。                                   |
| [4404](https://docs.blockvectra.com/zh/errors/#4404) | `invalid api key`                | API key 未知、已停用或已吊销                                                                                                             |   否  | 不要自动重连。在控制台检查并更换有效的 API key。                                                  |
| [4408](https://docs.blockvectra.com/zh/errors/#4408) | `slow consumer`                  | 会话推送队列超过 512 KiB（524,288 字节）时服务端以 4408 关闭并丢弃待发通知；4408 关闭帧排在已缓冲推送之后、最多等 2 秒，停止读取或读得远慢于推送速率的客户端往往收不到 4408，看到的是连接直接断开（浏览器报 1006）。 |   是  | 意外断开（没收到关闭帧，浏览器报 1006）按 4408 处理：退避重连，并减少订阅或加快读取；重新建立订阅并通过 `eth_getLogs` 补全数据。 |
| [4429](https://docs.blockvectra.com/zh/errors/#4429) | `push rate exceeded`             | 推送频率超过 1,000 次/秒（突发容限 10,000 次）                                                                                                |   是  | 减少订阅数或收窄过滤条件；带退避重连后重建订阅并补全数据。                                                 |
| [4503](https://docs.blockvectra.com/zh/errors/#4503) | `billing unavailable`            | 计费状态或 key 表数据短时间同步延迟（> 60 秒）                                                                                                   |   是  | 此为暂时状态，使用带全抖动的指数退避策略重连。                                                       |

## 断线重连与指数退避

当服务端重启或网络断开时，为避免大量客户端同时重连导致流量洪峰，客户端必须遵循 API 规格 §15.4 要求的全抖动（full jitter）指数退避算法：

* **退避公式**：在第 n 次重试连接前（n = 0, 1, 2, ...），在区间内均匀随机选择等待时长：
  ```
  delay = random(0, min(20s, 0.5s * 2^n))
  ```
* **参数规格（API 规格 §15.4）**：
  * **初始基础退避**：`0.5 s`（`0.5 s * 2^0 = 0.5 s`）
  * **指数倍增乘数**：`2^n`（`0.5 s`、`1.0 s`、`2.0 s`、`4.0 s`……）
  * **最大退避上限**：`20 s`
  * **全抖动（Full jitter）**：在 `0` 到 `min(20 s, 0.5 s * 2^n)` 之间均匀选取伪随机值
  * **重置计数器**：仅在会话持续稳定保持连接至少 `60 秒` 后，才将重试计数 n 重置为 0
  * **关闭码 1012**：首次重试时引入随机初始延迟，避免集群重启时所有连接同步打散
  * **不可重连关闭码**：遇到 [4402](https://docs.blockvectra.com/zh/errors/#4402)、[4404](https://docs.blockvectra.com/zh/errors/#4404)、[1003](https://docs.blockvectra.com/zh/errors/#1003) 或 [1009](https://docs.blockvectra.com/zh/errors/#1009) 时**禁止**自动重连。
  * **意外断开**：意外断开（没收到关闭帧，浏览器报 1006）按 [4408](https://docs.blockvectra.com/zh/errors/#4408) 处理：退避重连，并减少订阅或加快读取。

### 断线后的数据补全策略

WebSocket 订阅状态不跨连接持久化；断线期间产生的推送通知不会在服务端保留。客户端重连成功后，应按 API 规格 §15.5 实施补全：

1. **通过 `eth_getLogs` 补全日志**：
   * 持久化记录已处理并提交的最高区块高度（`last_processed_block`）。
   * 重连成功后立即调用 `eth_subscribe("logs", ...)` 恢复实时日志接收。
   * 调用 `eth_getLogs` 查询断线区间：设置 `fromBlock` 为 `last_processed_block + 1`，`toBlock` 为 `"latest"`（或新订阅收到的第一条通知区块），保持地址与主题过滤条件一致。
   * 若断线跨度超过 1,000 个区块（受限于 `max_logs_block_range: 1000`），将查询切分为每个最多 1,000 区块的连续批次。
   * 以 `(blockHash, transactionHash, logIndex)` 三元组对补全日志与实时通知进行边界去重。
2. **通过 `eth_getBlockByNumber` 补全区块头**：
   * 记录断线前收到的最新区块高度与哈希。
   * 重新订阅 `newHeads`。
   * 调用 `eth_getBlockByNumber("latest", false)` 确认当前高度；若存在缺失高度，按顺序拉取中间区块头并核对 `parentHash` 链连续性以排查可能发生的分叉重组。

## 限制与配额

| 限额                             | 数值                                        | 超出时的结果                                                                                                                                                                                                                          |
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 单个 API key 并发 WebSocket 连接数    | 每个服务端实例 20 个                              | 协议升级握手返回 429（[`ws_connection_limit`](https://docs.blockvectra.com/zh/errors/#ws_connection_limit)）                                                                                                                                                          |
| 单个账户并发 WebSocket 连接数           | 每个服务端实例 50 个                              | 协议升级握手返回 429（[`ws_connection_limit`](https://docs.blockvectra.com/zh/errors/#ws_connection_limit)）                                                                                                                                                          |
| 单个 WebSocket 连接订阅数             | 100                                       | `-32022` [`subscription_limit`](https://docs.blockvectra.com/zh/errors/#subscription_limit)                                                                                                                                                                 |
| 单个 WebSocket 连接 `newHeads` 订阅数 | 4                                         | `-32022` [`subscription_limit`](https://docs.blockvectra.com/zh/errors/#subscription_limit)                                                                                                                                                                 |
| `logs` 订阅过滤条件要求                | 必须指定 `address` 或 `topic0`（`topics` 的首个位置） | `-32602` [`logs_filter_required`](https://docs.blockvectra.com/zh/errors/#logs_filter_required)                                                                                                                                                             |
| 入站 WebSocket 消息尺寸              | 1 MiB（1048576 字节）                         | 连接关闭，关闭码 [1009](https://docs.blockvectra.com/zh/errors/#1009)                                                                                                                                                                                               |
| 单连接推送通知队列缓冲                    | 512 KiB（524288 字节）                        | 连接关闭，关闭码 [4408](https://docs.blockvectra.com/zh/errors/#4408)（`slow consumer`）                                                                                                                                                                              |
| 每个服务端实例推送通知队列缓冲（所有连接合计）        | 256 MiB（268435456 字节）                     | 下一条通知无法容纳的连接以关闭码 [1013](https://docs.blockvectra.com/zh/errors/#1013)（`overloaded`）关闭；超过一半时，新 WebSocket 握手返回 503（[`overloaded`](https://docs.blockvectra.com/zh/errors/#overloaded)，带 `Retry-After`），`eth_subscribe` 返回 `-32026` [`ws_push_overloaded`](https://docs.blockvectra.com/zh/errors/#ws_push_overloaded) |
| 单连接未读取应答缓冲                     | 16 MiB                                    | 在客户端读取足够消息前，服务端不再从该连接读取后续消息                                                                                                                                                                                                     |
| WebSocket 客户端未读取应答             | 单次写入阻塞 30 秒                               | 直接断开连接（不发送关闭帧）                                                                                                                                                                                                                  |
| 单连接推送通知速率                      | 1000 次推送/秒（突发 10000）                      | 连接关闭，关闭码 [4429](https://docs.blockvectra.com/zh/errors/#4429)                                                                                                                                                                                               |
| 空闲 WebSocket 连接                | 3600 秒（1 小时）无订阅且无消息                       | 连接关闭，关闭码 [1001](https://docs.blockvectra.com/zh/errors/#1001)                                                                                                                                                                                               |
| WebSocket 连接最长存活时间             | 24 小时（带 ±10% 抖动）                          | 连接关闭，关闭码 [1012](https://docs.blockvectra.com/zh/errors/#1012)                                                                                                                                                                                               |

## 下一步

* [浏览数据集目录](https://blockvectra.com/zh/data/)，查看 BlockVectra 索引的全部数据集。
* [查看免费额度与定价](https://blockvectra.com/zh/pricing/#free)，确认账户可用的方案。
* [登录控制台](https://console.blockvectra.com/zh/login/?next=%2Fzh%2Fkeys%2F)创建 API key。
