> For the complete documentation index, see [llms.txt](https://docscn.jkidata.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docscn.jkidata.com/websocket-xie-yi-shuo-ming/5.-duan-kai-lian-jie.md).

# ❌ 5. 断开连接

已实际验证：

* 客户端通过标准 `close(1000, reason)` 正常关闭。
* 正常关闭返回代码 `1000`。
* 关闭后等待5秒可以重新建立连接。
* 重连后重新收到 `{"type":"connect","msg":"ok"}`。
* 再等待5秒后，BTCUSDT 订阅恢复成功。
* 不需要发送自定义 `/disconnect` 指令。

以下是完整页面。

## 5. 断开连接与重连

WebSocket 可能因客户端主动关闭、网络异常、心跳超时或服务端状态变化而断开。

客户端应区分正常断开与异常断开，并只在需要时执行自动重连。

### 5.1 断开类型

| 类型        | 说明                     | 是否重连     |
| --------- | ---------------------- | -------- |
| 客户端正常关闭   | 程序退出或用户主动停止行情          | 通常不重连    |
| 网络异常      | 网络中断、代理异常或连接丢失         | 建议重连     |
| 心跳超时      | 连续未收到有效心跳响应            | 建议重连     |
| 服务端主动关闭   | 服务端维护、连接超时或权限限制        | 根据原因决定   |
| API Key错误 | 未收到 `connect: ok`，连接关闭 | 修复密钥后再连接 |

### 5.2 正常断开

正常断开使用 WebSocket 标准关闭方法，不需要发送自定义文本指令。

JavaScript：

```
socket.close(
  1000,
  "normal shutdown"
);
```

Python：

```
ws.close(
    status=1000,
    reason="normal shutdown",
)
```

状态码 `1000` 表示客户端正常关闭连接。

### 正常关闭流程

```
停止接收新的业务任务
        ↓
停止心跳定时器
        ↓
停止重连定时器
        ↓
清理本地订阅状态
        ↓
发送 WebSocket 关闭帧
        ↓
等待 close 事件
        ↓
释放连接对象
```

程序退出时应主动关闭连接，避免服务端继续保留无效会话。

### 是否需要先取消订阅

关闭整个 WebSocket 连接时，不要求逐个取消产品或市场订阅。

关闭连接后，本次连接中的订阅会随连接失效。

如果只是停止部分行情、但需要继续保持连接，应使用“取消订阅”页面中的指令。

### 正常关闭结果

实际验证中，客户端发送：

```
socket.close(
  1000,
  "normal shutdown"
);
```

关闭事件返回：

```
code: 1000
reason: normal shutdown
```

客户端收到 `1000` 时，可以将连接识别为正常关闭。

### 5.3 服务端主动断开

服务端可能因心跳超时、连接限制或其他原因主动关闭连接。

心跳超时时可能先返回：

```
{
  "type": "close",
  "msg": "Heartbeat timeout, connection closed"
}
```

客户端收到 `type: "close"` 后应：

1. 停止心跳任务。
2. 标记当前连接不可用。
3. 等待实际的 WebSocket `close` 事件。
4. 根据关闭原因决定是否重连。

不要在旧连接尚未完全关闭时立即创建新连接。

### 5.4 网络异常断开

网络异常时，客户端通常不会收到完整的服务端关闭消息。

可能出现：

* 直接触发 `error`。
* 随后触发 `close`。
* 关闭代码为 `1006`。
* 没有关闭原因。
* 长时间收不到行情和心跳响应。

`1006` 表示连接异常中断，不是客户端可以主动发送的关闭代码。

### 常见关闭状态

| 状态     | 说明          | 建议处理        |
| ------ | ----------- | ----------- |
| `1000` | 正常关闭        | 通常不重连       |
| `1006` | 异常断开        | 检查网络后重连     |
| 未提供代码  | 客户端库未取得关闭信息 | 按异常断开处理     |
| 其他代码   | 服务端或协议关闭    | 记录代码并根据原因处理 |

### 5.5 自动重连条件

建议在以下情况自动重连：

* 网络异常断开。
* 心跳连续多次超时。
* 服务端临时关闭连接。
* 长时间没有收到任何数据和心跳响应。

以下情况不应立即自动重连：

* 用户主动停止行情。
* 应用程序正在退出。
* 客户端主动发送正常关闭。
* API Key 已确认无效。
* 重连功能被管理员关闭。
* 已达到最大重试次数。

### 手动关闭标记

客户端应设置手动关闭状态：

```
manualClose = true
```

关闭事件触发时：

```
manualClose = true
    → 不自动重连

manualClose = false
    → 根据关闭原因执行重连
```

这样可以避免程序主动退出后又被重连逻辑重新启动。

### 5.6 重连间隔

重连前至少等待5秒。

建议使用指数退避：

```
第1次重连：等待5秒
第2次重连：等待10秒
第3次重连：等待20秒
第4次及以后：等待30秒
```

可以加入少量随机延迟，避免大量客户端同时重连：

```
实际等待时间 = 基础等待时间 + 0至3秒随机值
```

成功连接后，应将重连次数清零。

### 重连流程

```
连接异常断开
        ↓
停止心跳定时器
        ↓
保存需要恢复的订阅
        ↓
确认旧连接已经关闭
        ↓
等待重连间隔
        ↓
创建新的 WebSocket 连接
        ↓
收到 connect: ok
        ↓
等待至少5秒
        ↓
恢复之前成功的订阅
        ↓
重新启动心跳
```

### 5.7 恢复订阅

WebSocket 断开后，原连接中的订阅状态全部失效。

重连成功后，客户端需要重新发送订阅指令。

只恢复之前已经确认成功的订阅：

```
activeSymbols
activeExchanges
```

不要自动恢复：

* 已取消的订阅。
* `res: false` 的订阅。
* 已删除的业务目标。
* 用户明确停止的订阅。

### 恢复产品订阅

```
/symbol/JM:BTCUSDT,JM:ETHUSDT
```

### 恢复市场订阅

```
/exchange/JM,NASDAQ
```

恢复订阅时：

* 收到 `connect: ok` 后等待至少5秒。
* 单批建议不超过50个产品。
* 超过建议数量时分批发送。
* 批次之间至少间隔5秒。
* 再次检查每一项返回的 `res`。
* 避免同时恢复重复的市场和产品订阅。

### JavaScript正常关闭示例

```
let manualClose = false;
let heartbeatTimer = null;
let reconnectTimer = null;

function disconnect() {
  manualClose = true;

  if (heartbeatTimer !== null) {
    clearInterval(heartbeatTimer);
    heartbeatTimer = null;
  }

  if (reconnectTimer !== null) {
    clearTimeout(reconnectTimer);
    reconnectTimer = null;
  }

  if (
    socket &&
    (
      socket.readyState === WebSocket.OPEN ||
      socket.readyState === WebSocket.CONNECTING
    )
  ) {
    socket.close(
      1000,
      "normal shutdown"
    );
  }
}
```

### JavaScript重连示例

```
let socket = null;
let reconnectAttempts = 0;
let reconnectTimer = null;
let manualClose = false;

const reconnectDelays = [
  5000,
  10000,
  20000,
  30000,
];

function getReconnectDelay() {
  const index = Math.min(
    reconnectAttempts,
    reconnectDelays.length - 1
  );

  const baseDelay =
    reconnectDelays[index];

  const jitter =
    Math.floor(Math.random() * 3000);

  return baseDelay + jitter;
}
```

安排重连：

```
function scheduleReconnect() {
  if (manualClose) {
    return;
  }

  if (reconnectTimer !== null) {
    return;
  }

  const delay = getReconnectDelay();

  reconnectTimer = setTimeout(() => {
    reconnectTimer = null;
    reconnectAttempts += 1;

    connect();
  }, delay);
}
```

监听关闭事件：

```
function handleClose(event) {
  stopHeartbeat();

  console.log(
    "WebSocket 已关闭：",
    event.code,
    event.reason
  );

  socket = null;

  if (!manualClose) {
    scheduleReconnect();
  }
}
```

重连成功后恢复状态：

```
function handleConnect(data) {
  if (
    data.type !== "connect" ||
    data.msg !== "ok"
  ) {
    return;
  }

  reconnectAttempts = 0;

  setTimeout(() => {
    restoreSubscriptions();
    startHeartbeat();
  }, 5000);
}
```

恢复订阅：

```
function restoreSubscriptions() {
  if (activeSymbols.size > 0) {
    socket.send(
      `/symbol/${[
        ...activeSymbols
      ].join(",")}`
    );
  }

  if (activeExchanges.size > 0) {
    setTimeout(() => {
      socket.send(
        `/exchange/${[
          ...activeExchanges
        ].join(",")}`
      );
    }, 5000);
  }
}
```

### Python正常关闭示例

```
manual_close = False
heartbeat_timer = None
reconnect_timer = None


def disconnect(ws):
    global manual_close
    global heartbeat_timer
    global reconnect_timer

    manual_close = True

    if heartbeat_timer is not None:
        heartbeat_timer.cancel()
        heartbeat_timer = None

    if reconnect_timer is not None:
        reconnect_timer.cancel()
        reconnect_timer = None

    ws.close(
        status=1000,
        reason="normal shutdown",
    )
```

### Python重连示例

```
import random
import threading

manual_close = False
reconnect_attempts = 0
reconnect_timer = None

reconnect_delays = [
    5,
    10,
    20,
    30,
]


def get_reconnect_delay():
    index = min(
        reconnect_attempts,
        len(reconnect_delays) - 1,
    )

    return (
        reconnect_delays[index]
        + random.uniform(0, 3)
    )
```

安排重连：

```
def schedule_reconnect():
    global reconnect_timer
    global reconnect_attempts

    if manual_close:
        return

    if reconnect_timer is not None:
        return

    delay = get_reconnect_delay()

    reconnect_timer = threading.Timer(
        delay,
        reconnect,
    )

    reconnect_timer.start()
    reconnect_attempts += 1
```

连接关闭处理：

```
def on_close(
    ws,
    status_code,
    reason,
):
    stop_heartbeat()

    print(
        "WebSocket 已关闭：",
        status_code,
        reason,
    )

    if not manual_close:
        schedule_reconnect()
```

重连成功后恢复订阅：

```
def handle_connect(ws, data):
    global reconnect_attempts

    if (
        data.get("type") != "connect"
        or data.get("msg") != "ok"
    ):
        return

    reconnect_attempts = 0

    timer = threading.Timer(
        5.0,
        restore_subscriptions,
        args=[ws],
    )

    timer.start()
```

### 防止重复重连

网络异常可能同时触发 `error` 和 `close`。

不要在两个事件中分别创建重连任务，否则可能同时建立多个连接。

推荐：

* `error` 只记录错误。
* `close` 统一负责触发重连。
* 使用单一 `reconnectTimer`。
* 创建重连任务前检查是否已经存在。
* 新连接成功后清除旧重连任务。

### 连接状态

建议维护明确的连接状态：

```
DISCONNECTED
CONNECTING
CONNECTED
RECONNECTING
CLOSING
```

状态转换：

```
DISCONNECTED
    ↓ connect()
CONNECTING
    ↓ connect: ok
CONNECTED
    ↓ 网络异常
RECONNECTING
    ↓ connect: ok
CONNECTED
    ↓ 主动关闭
CLOSING
    ↓ close
DISCONNECTED
```

### API Key错误

错误 API Key 可能表现为：

```
WebSocket 已打开
        ↓
未收到 connect: ok
        ↓
连接以1006关闭
```

此时不要无限自动重连。

应先：

1. 检查 API Key 是否正确。
2. 检查 API Key 是否过期。
3. 确认连接地址是否完整。
4. 修复配置后再重新连接。

### 连接数量限制

同一个 API Key 的并发连接数可能受到账号权限限制。

重连时应确保：

* 旧连接已经触发 `close`。
* 不存在其他重连任务。
* 没有其他程序占用相同连接额度。
* 不会同时创建多个 WebSocket 实例。

如果连接暂时无法建立，应继续执行退避等待，不要高频重试。

### 使用建议

* 正常退出使用 WebSocket 标准 `close()`。
* 正常关闭建议使用状态码 `1000`。
* 不需要发送自定义 `/disconnect` 指令。
* 主动关闭前停止心跳和重连定时器。
* 网络异常统一在 `close` 事件中触发重连。
* 重连前至少等待5秒。
* 使用指数退避和随机延迟。
* 重连成功并收到 `connect: ok` 后再等待5秒。
* 只恢复此前确认成功的订阅。
* 恢复订阅时继续遵循分批和间隔规则。
* API Key错误时不要无限重连。
* 防止 `error` 和 `close` 同时创建重复连接。
