> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oddspapi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket 压缩（zstd）

> 使用zstd降低WebSocket带宽。两种可选模式：receiveType: zstd（无字典，一行代码解码）或receiveType: zstd-dict（按频道训练的字典，压缩比最佳）。

<Note>
  **Beta —— 按区域逐步推出。** zstd 正在逐步启用。如果某个网关禁用了 zstd，`receiveType: "zstd"` 或 `"zstd-dict"` 的连接会被**优雅地降级为 JSON** —— 服务器会在 `login_ok.receiveType` 中告知协商后的模式。请**始终信任该回显**来选择解码器；切勿假设您请求的模式即为生效模式。
</Note>

## 两种模式

两种模式都用 zstd 压缩每个数据帧；控制帧保持 JSON 文本。请根据您愿意承担的客户端工作量来选择：

| `receiveType` | `odds` 上的压缩比 | 客户端工作量                                    | `dict` 帧 |
| ------------- | ------------ | ----------------------------------------- | -------- |
| `"zstd"`      | 约 5–6×       | **一行代码：** `zstd.decompress(frame)` → JSON | 无        |
| `"zstd-dict"` | 约 7–9×       | 缓存服务器推送的字典，按每帧内嵌的 `dictId` 解码             | 连接时发送    |

**从 `"zstd"` 开始。** 它完全不需要处理字典 —— 解压并解析即可。当您想要额外约 30–40% 的压缩收益、并能维护一个小型内存字典存储时，再迁移到 `"zstd-dict"`。

> 有意**不提供**压缩版 MessagePack 模式 —— 在任何层面上，压缩 JSON 都优于压缩 MessagePack。

***

## 模式 1 —— `zstd`（无字典）

平滑接入路径。每个数据帧都是独立的、无字典的 zstd 帧（`dictId = 0`）；服务器**不会**发送 `dict` 控制帧。您的客户端只需解压并解析。

### 登录

```json theme={null}
{
  "type": "login",
  "apiKey": "YOUR_API_KEY",
  "channels": ["odds", "fixtures"],
  "receiveType": "zstd"
}
```

### 解码

```
on text frame   → JSON.parse → control (login_ok, error, …)
on binary frame → JSON.parse(zstd.decompress(frame))   // no dictionary needed
                  route by msg.channel
```

这就是此模式的全部协议。

***

## 模式 2 —— `zstd-dict`（训练字典）

最大压缩比路径。`odds`、`fixtures` 和 `bookmakers` 拥有约 32 KB 的训练字典，可将压缩比提升至约 7–9×。服务器在连接时以控制帧形式下发字典；每个数据帧内嵌了压缩时使用的 `dictId`，因此解码是**自描述的，永远不需要按频道分支**。

### 登录

```json theme={null}
{
  "type": "login",
  "apiKey": "YOUR_API_KEY",
  "channels": ["odds", "fixtures"],
  "receiveType": "zstd-dict"
}
```

### 字典下发（服务器 → 客户端）

在 `login_ok` 之后（且在任何数据帧之前），对于每个拥有训练字典的已订阅频道，服务器推送一个控制帧：

```json theme={null}
{
  "type": "dict",
  "channel": "odds",
  "dictVersion": "odds-v1",
  "dictId": 740826216,
  "encoding": "base64",
  "data": "<base64 of the ~32 KB zstd dictionary>"
}
```

1. 将 `data` 进行 Base64 解码，得到原始字典字节。
2. 用它构建可复用的 zstd 解码器，并**以 `dictId` 为键**存储。

<Note>
  解码由**每个数据帧内嵌的 `dictId`** 驱动，而不是由频道驱动。`dict` 帧上的 `channel`/`dictVersion` 字段仅供参考。
</Note>

字典很小（约 32 KB），并且会在**每次**连接时重新发送 —— 无需维护客户端版本缓存，登录时也无需发送 `dicts` 字段。没有训练字典的频道（例如 `scores`、`clocks`）不发送 `dict` 帧；其数据帧携带 `dictId = 0`，以无字典方式解码。

### 解码

```
on text frame   → JSON.parse → control (login_ok, dict, error, …)
on binary frame → id   = zstd.getDictID(frame)        // 0 ⇒ no dict
                  dict = store[id]                     // undefined ⇒ dictless
                  msg  = JSON.parse(zstd.decompress(frame, dict))
                  route by msg.channel
```

***

## 帧规则（两种模式通用）

* 每个数据帧都是一个 WebSocket **Binary** 帧，包含一个独立的 zstd 帧（魔数 `28 B5 2F FD`，内嵌 `dictId`）。
* 控制帧（`login_ok`、`dict`、`error`、`snapshot_required`、`resume_complete`）保持为 **JSON 文本帧**，即使在 zstd 连接上也是如此。规则是固定的：

> **文本帧 → 控制（JSON）。二进制帧 → 数据（zstd）。**

`login_ok.receiveType` 是**协商后**的模式（`"zstd"`、`"zstd-dict"`，或 —— 若网关禁用了 zstd —— `"json"`）。如果返回 `"json"`，请按纯 JSON 文本帧解码；**不要**尝试解压。

***

## 🐍 Python 示例

需要 `pip install zstandard`。

<Tabs>
  <Tab title="zstd (dictless)">
    ```python theme={null}
    import asyncio, json, websockets
    import zstandard as zstd

    WS_URL, API_KEY = "wss://v5.oddspapi.io/ws", "YOUR_API_KEY"
    dctx = zstd.ZstdDecompressor()
    MAX_OUT = 64 * 1024 * 1024  # frames carry no content size — pass an upper bound

    async def main():
        async with websockets.connect(WS_URL, max_size=4194304) as ws:
            await ws.send(json.dumps({
                "type": "login", "apiKey": API_KEY,
                "channels": ["odds", "fixtures"], "receiveType": "zstd",
            }))
            async for raw in ws:
                if isinstance(raw, str):                       # control (JSON)
                    msg = json.loads(raw)
                    if msg.get("type") == "login_ok" and msg.get("receiveType") != "zstd":
                        print("zstd disabled here, negotiated:", msg.get("receiveType"))
                    continue
                data = json.loads(dctx.decompress(raw, max_output_size=MAX_OUT))
                print("DATA:", data.get("channel"), data.get("entryId"))

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="zstd-dict (dictionaries)">
    ```python theme={null}
    import asyncio, json, base64, websockets
    import zstandard as zstd

    WS_URL, API_KEY = "wss://v5.oddspapi.io/ws", "YOUR_API_KEY"
    decoders: dict[int, zstd.ZstdDecompressor] = {}   # dictId -> decoder
    dictless = zstd.ZstdDecompressor()
    MAX_OUT = 64 * 1024 * 1024

    def decode_frame(frame: bytes) -> dict:
        dict_id = zstd.get_frame_parameters(frame).dict_id  # 0 if no dictionary
        dctx = decoders.get(dict_id, dictless)
        return json.loads(dctx.decompress(frame, max_output_size=MAX_OUT))

    async def main():
        async with websockets.connect(WS_URL, max_size=4194304) as ws:
            await ws.send(json.dumps({
                "type": "login", "apiKey": API_KEY,
                "channels": ["odds", "fixtures"], "receiveType": "zstd-dict",
            }))
            async for raw in ws:
                if isinstance(raw, str):                       # control (JSON)
                    msg = json.loads(raw)
                    t = msg.get("type")
                    if t == "login_ok" and msg.get("receiveType") != "zstd-dict":
                        print("downgraded, negotiated:", msg.get("receiveType"))
                    elif t == "dict":
                        d = zstd.ZstdCompressionDict(base64.b64decode(msg["data"]))
                        decoders[msg["dictId"]] = zstd.ZstdDecompressor(dict_data=d)
                    continue
                data = decode_frame(raw)                        # data (zstd)
                print("DATA:", data.get("channel"), data.get("entryId"))

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

***

## 另请参阅

* [认证与过滤器](/zh/websocket/auth) —— 完整的 `login` 字段参考
* [恢复和重放](/zh/websocket/resume-replay) —— 重新连接并恢复丢失的数据
* [故障排除](/zh/websocket/troubleshooting) —— zstd 解码问题
