跳至主要內容
入門

Ping & Pong Messages

了解 P2P 協議中的 ping/pong 消息,用於測量延遲和保持連接活躍。

6 分鐘

Ping 和 Pong 消息是比特幣 P2P 協議中用於連接保活和延遲測量的簡單機制。 節點定期發送 ping,對方必須回應 pong,這有助於檢測斷開的連接和測量網路延遲。

Ping/Pong 概述

Ping/Pong 的作用:

1. 連接保活
   - 檢測無回應的連接
   - 避免防火牆超時斷開
   - 清理死連接

2. 延遲測量
   - 測量往返時間 (RTT)
   - 用於節點評分
   - 優化連接選擇

3. 存活確認
   - 確認對方仍在運行
   - 節點可能崩潰但連接未關閉

交互流程:
┌─────────┐    ping(nonce)   ┌─────────┐
│ Node A  │ ──────────────→  │ Node B  │
│         │    pong(nonce)   │         │
│         │ ←──────────────  │         │
└─────────┘                  └─────────┘

RTT = pong_received_time - ping_sent_time

消息格式

Ping 消息格式:

┌─────────────────────────────────────────┐
│ nonce (8 bytes, uint64)                 │
└─────────────────────────────────────────┘

Pong 消息格式:

┌─────────────────────────────────────────┐
│ nonce (8 bytes, uint64)                 │
└─────────────────────────────────────────┘

nonce:
- 隨機生成的 64 位值
- pong 必須返回相同的 nonce
- 用於匹配 ping/pong 對

完整 P2P 消息 (ping):
┌──────────────────────────────────────────────┐
│ magic (4 bytes)                              │
│ command: "ping" (12 bytes, padded)           │
│ length: 8 (4 bytes)                          │
│ checksum (4 bytes)                           │
│ payload: nonce (8 bytes)                     │
└──────────────────────────────────────────────┘

總大小: 32 bytes (header + payload)

Ping 發送策略

何時發送 ping:

定時器:
PING_INTERVAL = 2 分鐘

條件:
1. 距離上次成功 ping > PING_INTERVAL
2. 沒有待處理的 ping
3. 連接處於活躍狀態

Bitcoin Core 邏輯:
void SendPing(CNode& node) {
    if (now() - node.m_ping_start > PING_INTERVAL) {
        if (!node.m_ping_nonce_sent) {
            uint64_t nonce = GetRand64();
            node.m_ping_nonce_sent = nonce;
            node.m_ping_start = now();

            connman.PushMessage(&node,
                CNetMsgMaker(node.GetCommonVersion())
                    .Make(NetMsgType::PING, nonce));
        }
    }
}

// 每 2 分鐘發送一次 ping
// 如果前一個 ping 未回應,不發送新的

Pong 處理

處理收到的 ping:

void ProcessPing(CNode& node, uint64_t nonce) {
    // 立即回應相同的 nonce
    connman.PushMessage(&node,
        CNetMsgMaker(node.GetCommonVersion())
            .Make(NetMsgType::PONG, nonce));
}

處理收到的 pong:

void ProcessPong(CNode& node, uint64_t nonce) {
    // 檢查 nonce 是否匹配
    if (nonce != node.m_ping_nonce_sent) {
        // 不匹配,可能是舊的或惡意的
        return;
    }

    // 計算 RTT
    int64_t rtt = now() - node.m_ping_start;
    node.m_ping_time = rtt;

    // 重置狀態
    node.m_ping_nonce_sent = 0;
}

nonce 不匹配的情況:
1. 延遲到達的舊 pong
2. 惡意節點發送假 pong
3. 實現錯誤

// 只接受匹配的 pong

超時處理

Ping 超時機制:

超時時間:
TIMEOUT_INTERVAL = 20 分鐘

檢測邏輯:
void CheckPingTimeout(CNode& node) {
    if (node.m_ping_nonce_sent != 0) {
        // 有待處理的 ping
        int64_t elapsed = now() - node.m_ping_start;

        if (elapsed > TIMEOUT_INTERVAL) {
            // 超時,斷開連接
            LogPrint("Ping timeout: peer=%d\n", node.GetId());
            node.fDisconnect = true;
        }
    }
}

超時後果:
1. 連接被標記為斷開
2. 資源被釋放
3. 嘗試連接其他節點

為什麼需要超時?
- 對方可能已崩潰
- 網路可能已斷開
- TCP 可能未正確關閉

延遲統計

使用 ping 測量延遲:

節點信息:
bitcoin-cli getpeerinfo
[
  {
    "id": 1,
    "pingtime": 0.123,      // 最後一次 ping RTT(秒)
    "minping": 0.098,       // 最小 ping 時間
    "pingwait": 0.0,        // 等待 pong 的時間
    ...
  }
]

延遲的用途:

1. 節點評分
   - 低延遲節點更受青睞
   - 區塊中繼優先使用快速節點

2. 連接選擇
   - 斷開慢速連接
   - 保持高質量連接

3. 調試網路問題
   - 識別網路瓶頸
   - 檢測路由問題

統計:
class NodeStats {
    double m_ping_time;       // 最後一次 RTT
    double m_min_ping;        // 歷史最小 RTT
    std::deque<double> m_ping_samples;  // 歷史樣本
};

// pingtime 的單位是秒

歷史演進

Ping/Pong 的演進:

早期版本 (BIP-31 之前):
- ping 消息沒有 payload
- 無法可靠測量 RTT
- 只能確認連接存活

BIP-31 改進:
- 添加 nonce 欄位
- pong 消息回傳 nonce
- 可以精確測量 RTT
- 可以匹配請求和回應

協議版本:
- 版本 < 60001: ping 無 payload
- 版本 >= 60001: ping 有 8 字節 nonce

現代節點:
- 總是使用帶 nonce 的 ping
- 舊格式已很少見

// 所有現代節點都支持 nonce

調試與監控

# 監控 ping/pong

# 查看所有節點的 ping 時間
bitcoin-cli getpeerinfo | jq '.[] | {
  id: .id,
  addr: .addr,
  pingtime: .pingtime,
  minping: .minping,
  pingwait: .pingwait
}'

# 找出最快的節點
bitcoin-cli getpeerinfo | jq 'sort_by(.minping) | .[0:5] | .[] | {
  addr: .addr,
  minping: .minping
}'

# 找出有問題的連接 (ping 超過 1 秒)
bitcoin-cli getpeerinfo | jq '.[] | select(.pingtime > 1) | {
  id: .id,
  addr: .addr,
  pingtime: .pingtime
}'

# 日誌設置
# bitcoin.conf
debug=net

# 日誌輸出:
# "Sending ping to peer=5"
# "Received pong from peer=5 (123ms)"
# "Ping timeout on peer=3"

實現細節

Bitcoin Core 中的實現:

// 發送 ping
void PeerManager::MaybeSendPing(CNode& node) {
    // 檢查是否該發送 ping
    bool pingSend = false;
    if (node.m_ping_nonce_sent == 0 &&
        node.m_ping_start + PING_INTERVAL < GetTime<std::chrono::seconds>()) {
        pingSend = true;
    }

    if (pingSend) {
        uint64_t nonce = GetRand(std::numeric_limits<uint64_t>::max());
        node.m_ping_nonce_sent = nonce;
        node.m_ping_start = GetTime<std::chrono::microseconds>();

        if (node.GetCommonVersion() > BIP0031_VERSION) {
            connman.PushMessage(&node,
                CNetMsgMaker(node.GetCommonVersion())
                    .Make(NetMsgType::PING, nonce));
        } else {
            // 舊版本無 nonce
            connman.PushMessage(&node,
                CNetMsgMaker(node.GetCommonVersion())
                    .Make(NetMsgType::PING));
        }
    }
}

// 處理 pong
void PeerManager::ProcessPong(CNode& node, uint64_t nonce) {
    if (node.m_ping_nonce_sent != 0 && nonce == node.m_ping_nonce_sent) {
        auto ping_time = GetTime<std::chrono::microseconds>() - node.m_ping_start;
        node.m_ping_time = ping_time.count() / 1e6;
        node.m_min_ping = std::min(node.m_min_ping, node.m_ping_time);
        node.m_ping_nonce_sent = 0;
    }
}

相關概念

  • Version Handshake:版本握手
  • Message Types:P2P 消息類型
  • Peer Management:節點管理
  • P2P Protocol:點對點協議
  • Node Security:節點安全
已複製連結
已複製到剪貼簿