跳轉到內容

WebSocket 心跳機制

概述

HabitTrade API 的 WebSocket 連接支援服務端心跳機制,確保連接的穩定性和可靠性。系統會定期向所有活躍連接發送心跳消息,幫助客戶端檢測連接狀態。

心跳消息格式

服務端每 30 秒會自動向所有 WebSocket 連接發送心跳消息:

json
{
  "type": "heartbeat",
  "data": {
    "timestamp": 1703123456789
  }
}

字段說明

  • type: 消息類型,心跳消息固定為 "heartbeat"
  • data.timestamp: 心跳發送時的 Unix 時間戳(毫秒級)

適用端點

心跳機制適用於以下 WebSocket 端點:

交易端點

  • /ws/trade/* - 交易 WebSocket 連接

行情端點

  • /ws/market/* - 所有市場行情 WebSocket 連接

客戶端處理建議

1. 心跳監控

建議客戶端監控心跳消息來檢測連接健康狀態:

Python
import json
import time

def on_message(ws, message):
    data = json.loads(message)
    if data.get('type') == 'heartbeat':
        print(f"收到心跳: {data['data']['timestamp']}")
        global last_heartbeat_time
        last_heartbeat_time = time.time() * 1000
    # 處理其他業務消息...
Java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public void onMessage(String message) {
    ObjectMapper mapper = new ObjectMapper();
    JsonNode data = mapper.readTree(message);
    if ("heartbeat".equals(data.get("type").asText())) {
        System.out.println("收到心跳: " + data.get("data").get("timestamp").asLong());
        lastHeartbeatTime = System.currentTimeMillis();
    }
    // 處理其他業務消息...
}
C#
using Newtonsoft.Json.Linq;

private void OnMessage(string message)
{
    var data = JObject.Parse(message);
    if (data["type"]?.ToString() == "heartbeat")
    {
        Console.WriteLine($"收到心跳: {data["data"]["timestamp"]}");
        lastHeartbeatTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    }
    // 處理其他業務消息...
}
go
import (
    "encoding/json"
    "fmt"
    "time"
)

type HeartbeatMessage struct {
    Type string `json:"type"`
    Data struct {
        Timestamp int64 `json:"timestamp"`
    } `json:"data"`
}

func onMessage(message []byte) {
    var msg HeartbeatMessage
    json.Unmarshal(message, &msg)
    if msg.Type == "heartbeat" {
        fmt.Printf("收到心跳: %d\n", msg.Data.Timestamp)
        lastHeartbeatTime = time.Now().UnixMilli()
    }
    // 處理其他業務消息...
}
javascript
websocket.onmessage = function(event) {
    const message = JSON.parse(event.data);
    
    if (message.type === 'heartbeat') {
        console.log('收到心跳:', new Date(message.data.timestamp));
        // 更新最後心跳時間
        lastHeartbeatTime = Date.now();
    }
    // 處理其他業務消息...
};

2. 連接超時檢測

可以實現客戶端超時檢測機制:

Python
import threading
import time

def check_heartbeat_timeout():
    while True:
        current_time = time.time() * 1000
        if current_time - last_heartbeat_time > 60000:
            print("心跳超時,準備重新連接")
            reconnect()
        time.sleep(10)  # 每 10 秒檢查一次

# 啟動心跳檢查線程
heartbeat_thread = threading.Thread(target=check_heartbeat_timeout)
heartbeat_thread.daemon = True
heartbeat_thread.start()
Java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    long now = System.currentTimeMillis();
    if (now - lastHeartbeatTime > 60000) {
        System.out.println("心跳超時,準備重新連接");
        reconnect();
    }
}, 10, 10, TimeUnit.SECONDS); // 每 10 秒檢查一次
C#
using System;
using System.Threading;

private Timer heartbeatTimer;

private void StartHeartbeatCheck()
{
    heartbeatTimer = new Timer(CheckHeartbeatTimeout, null, 
        TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
}

private void CheckHeartbeatTimeout(object state)
{
    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    if (now - lastHeartbeatTime > 60000)
    {
        Console.WriteLine("心跳超時,準備重新連接");
        Reconnect();
    }
}
go
import "time"

func startHeartbeatCheck() {
    ticker := time.NewTicker(10 * time.Second)
    go func() {
        for range ticker.C {
            now := time.Now().UnixMilli()
            if now - lastHeartbeatTime > 60000 {
                fmt.Println("心跳超時,準備重新連接")
                reconnect()
            }
        }
    }()
}
javascript
// 檢查心跳超時(建議設置為 60 秒)
setInterval(() => {
    const now = Date.now();
    if (now - lastHeartbeatTime > 60000) {
        console.log('心跳超時,準備重新連接');
        // 重新建立連接
        reconnect();
    }
}, 10000); // 每 10 秒檢查一次

3. 忽略心跳消息

如果不需要心跳功能,可以安全地忽略這些消息:

Python
import json

def on_message(ws, message):
    data = json.loads(message)
    # 忽略心跳消息
    if data.get('type') == 'heartbeat':
        return
    # 處理業務消息
    handle_business_message(data)
Java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public void onMessage(String message) {
    ObjectMapper mapper = new ObjectMapper();
    JsonNode data = mapper.readTree(message);
    // 忽略心跳消息
    if ("heartbeat".equals(data.get("type").asText())) {
        return;
    }
    // 處理業務消息
    handleBusinessMessage(data);
}
C#
using Newtonsoft.Json.Linq;

private void OnMessage(string message)
{
    var data = JObject.Parse(message);
    // 忽略心跳消息
    if (data["type"]?.ToString() == "heartbeat")
    {
        return;
    }
    // 處理業務消息
    HandleBusinessMessage(data);
}
go
import "encoding/json"

func onMessage(message []byte) {
    var data map[string]interface{}
    json.Unmarshal(message, &data)
    // 忽略心跳消息
    if data["type"] == "heartbeat" {
        return
    }
    // 處理業務消息
    handleBusinessMessage(data)
}
javascript
websocket.onmessage = function(event) {
    const message = JSON.parse(event.data);
    
    // 忽略心跳消息
    if (message.type === 'heartbeat') {
        return;
    }
    
    // 處理業務消息
    handleBusinessMessage(message);
};

客戶端斷連處理

正確的斷連流程

客戶端主動斷開連接時,需要發送關閉幀以確保連接正確關閉:

Python
def close_connection():
    if ws and not ws.closed:
        ws.close(code=1000, reason='Client closing connection')
        print("已發送關閉幀")
Java
public void closeConnection() {
    if (webSocket != null && webSocket.isOpen()) {
        webSocket.close(1000, "Client closing connection");
        System.out.println("已發送關閉幀");
    }
}
C#
public async Task CloseConnection()
{
    if (webSocket != null && webSocket.State == WebSocketState.Open)
    {
        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, 
            "Client closing connection", CancellationToken.None);
        Console.WriteLine("已發送關閉幀");
    }
}
Go
func closeConnection() {
    if conn != nil {
        err := conn.WriteMessage(websocket.CloseMessage, 
            websocket.FormatCloseMessage(websocket.CloseNormalClosure, 
            "Client closing connection"))
        if err == nil {
            fmt.Println("已發送關閉幀")
        }
        conn.Close()
    }
}
javascript
// 正確的斷連方式
function closeConnection() {
    if (websocket && websocket.readyState === WebSocket.OPEN) {
        // 發送關閉幀
        websocket.close(1000, 'Client closing connection');
        console.log('已發送關閉幀');
    }
}

斷連過程說明

完整的 WebSocket 斷連過程包括以下步驟:

  1. 客戶端發起關閉: 調用 websocket.close() 發送關閉幀
  2. 服務端確認: 服務端收到關閉幀後發送確認
  3. 連接關閉: 雙方完成握手,TCP 連接正式關閉
  4. 資源清理: 服務端自動清理相關連接資源

監聽斷連事件

Python
def on_close(ws, close_status_code, close_msg):
    print(f"連接已關閉: {close_status_code}, {close_msg}")
    # 清理客戶端資源
    cleanup()
Java
public void onClose(int code, String reason, boolean remote) {
    System.out.println("連接已關閉: " + code + ", " + reason);
    // 清理客戶端資源
    cleanup();
}
C#
private void OnClose(WebSocketCloseStatus closeStatus, string statusDescription)
{
    Console.WriteLine($"連接已關閉: {closeStatus}, {statusDescription}");
    // 清理客戶端資源
    Cleanup();
}
go
func onClose(code int, text string) {
    fmt.Printf("連接已關閉: %d, %s\n", code, text)
    // 清理客戶端資源
    cleanup()
}
javascript
websocket.onclose = function(event) {
    console.log('連接已關閉:', event.code, event.reason);
    // 清理客戶端資源
    cleanup();
};

最佳實踐

  1. 監控心跳: 建議客戶端監控心跳消息以檢測連接狀態
  2. 設置超時: 建議設置 60 秒的心跳超時時間
  3. 自動重連: 心跳超時時實現自動重連機制
  4. 日誌記錄: 記錄心跳狀態有助於問題排查

故障排除

心跳消息丟失

如果長時間未收到心跳消息:

  1. 檢查網絡連接狀態
  2. 確認 WebSocket 連接是否正常
  3. 檢查是否有防火牆或代理干擾
  4. 考慮重新建立連接

連接頻繁斷開

如果連接經常斷開:

  1. 檢查客戶端網絡穩定性
  2. 確認是否正確處理心跳消息
  3. 檢查客戶端是否有資源限制
  4. 聯繫技術支援獲取協助