Theme / v1.5.0

Agent Reach

給 AI Agent 一鍵安裝網路讀寫能力

實戰範例

實戰範例 006:AI 代理的 TCP Socket 通訊與遠端長連線管理實戰

介紹如何使用 Node.js 原生 net 模組安全地建立 TCP 長連線通訊、處理 Socket 粘包 (TCP stream fragmentation) 與防禦性斷線重連機制。

實戰範例 006:AI 代理的 TCP Socket 通訊與遠端長連線管理實戰

簡介與場景

在許多物聯網 (IoT)、即時報價系統或低延遲的 AI Agent 協同工作中,HTTP 協定的開銷過大,開發者會選擇使用原生的 TCP Socket 建立長連線。 然而,TCP 是一個基於「串流 (Stream)」的協定,沒有明確的訊息邊界。如果缺乏良好的網路讀寫容錯設計 (Agent Reach),開發者極容易遇到「粘包 (Sticky Packets)」、「半包」或未預期斷線導致的記憶體洩漏等問題。 本範例探討如何建立一個防禦性極強的 TCP Socket Agent 客戶端,具備封包長度前綴解析 (Length-Prefixed Framing) 與自動斷線重連 (Heartbeat & Reconnect) 機制。

原始代碼:脆弱且易受粘包影響的實現

在極簡且不考慮網路狀態的情況下,開發者往往假設 data 事件每次都會完整收到一包數據。

const net = require('net');

function createSimpleAgent() {
    const client = new net.Socket();

    client.connect(8080, '127.0.0.1', () => {
        console.log('Connected to server');
        client.write('Hello Server!');
    });

    // 致命錯誤:假設 data 就是一個完整的字串或 JSON
    client.on('data', (data) => {
        const message = data.toString();
        // 如果遇到粘包,這裡的 JSON.parse 會直接崩潰
        const parsed = JSON.parse(message);
        console.log('Received:', parsed);
    });

    client.on('error', (err) => {
        console.error('Connection error:', err);
        // 無重連機制,遇到異常 Agent 就死了
    });
}

createSimpleAgent();

開發者與 AI 的對話記錄

Ponytail (極簡主義者): 「用 net.Socket 幾行代碼就連上伺服器了。有數據來就觸發 data 事件,這比寫一堆 HTTP Headers 清爽多了。」

Agent Reach (容錯專家): 「你忽略了 TCP 的本質。TCP 是位元組流 (Byte Stream),並不保證你發送的『一包』資料會在接收端一次性呈現。作業系統的緩衝區機制可能把三個小封包合併成一個 (粘包),或者把一個大封包拆成兩個 (半包)。直接 JSON.parse 絕對會拋出 SyntaxError。」

Ponytail: 「原來如此,那我加上一個特殊的分隔符號 (Delimiter) 像是 \n 來切割字串?」

Agent Reach: 「這是一個方法。但更標準、更安全的做法是使用『長度前綴 (Length Prefix)』協定,在每條訊息最前面寫入 4 Bytes 的整數代表資料長度。同時,你的程式碼完全沒有處理 Keep-Alive 或斷線重連。一旦網路閃斷 (Network Flap),這個 Agent 就成了沒有連線能力的殭屍進程。」

重構/優化後的代碼:實現 Framing 與斷線重連防護

以下代碼引入了基於 Buffer 的緩衝區管理以解決粘包問題,並實作了自動重連與心跳檢測。

const net = require('net');

class RobustTcpAgent {
    constructor(host, port) {
        this.host = host;
        this.port = port;
        this.client = null;
        this.reconnectTimer = null;
        this.buffer = Buffer.alloc(0); // 解決粘包的緩衝區
        this.isConnected = false;
    }

    connect() {
        if (this.client) this.client.destroy();
        
        console.log(`[INFO] 正在連接 ${this.host}:${this.port}...`);
        this.client = new net.Socket();
        
        this.client.connect(this.port, this.host, () => {
            console.log('[INFO] 成功連接至伺服器');
            this.isConnected = true;
            if (this.reconnectTimer) {
                clearTimeout(this.reconnectTimer);
                this.reconnectTimer = null;
            }
        });

        this.client.on('data', (chunk) => this.handleData(chunk));
        
        this.client.on('error', (err) => {
            console.error(`[ERROR] Socket 發生錯誤: ${err.message}`);
        });

        this.client.on('close', () => {
            console.warn('[WARN] 與伺服器的連線已中斷。');
            this.isConnected = false;
            this.scheduleReconnect();
        });
    }

    handleData(chunk) {
        // 將新資料併入全局緩衝區
        this.buffer = Buffer.concat([this.buffer, chunk]);

        // 持續解析,直到緩衝區內的資料不足以構成一個完整的封包
        while (this.buffer.length >= 4) {
            // 讀取前 4 個 Byte 獲取 Payload 長度 (大端序 Big-Endian)
            const payloadLength = this.buffer.readUInt32BE(0);
            const totalPacketLength = payloadLength + 4;

            // 如果緩衝區長度不足一個完整封包,跳出迴圈等待下一次 data 事件
            if (this.buffer.length < totalPacketLength) {
                break;
            }

            // 提取完整的 Payload
            const payloadBuffer = this.buffer.subarray(4, totalPacketLength);
            // 更新緩衝區,移除已處理的封包
            this.buffer = this.buffer.subarray(totalPacketLength);

            try {
                const message = JSON.parse(payloadBuffer.toString('utf-8'));
                console.log('[INFO] 完整接收訊息:', message);
            } catch (err) {
                console.error('[ERROR] Payload JSON 解析失敗:', err.message);
            }
        }
    }

    send(dataObject) {
        if (!this.isConnected) {
            console.warn('[WARN] 尚未連線,無法發送數據。');
            return;
        }
        
        const payloadStr = JSON.stringify(dataObject);
        const payloadBuffer = Buffer.from(payloadStr, 'utf-8');
        const lengthBuffer = Buffer.alloc(4);
        lengthBuffer.writeUInt32BE(payloadBuffer.length, 0);

        // 合併長度前綴與 Payload 並發送
        this.client.write(Buffer.concat([lengthBuffer, payloadBuffer]));
    }

    scheduleReconnect() {
        if (!this.reconnectTimer) {
            console.log('[INFO] 將在 5 秒後嘗試重新連線...');
            this.reconnectTimer = setTimeout(() => {
                this.connect();
            }, 5000);
        }
    }
}

// 實例化並運行 Agent
const agent = new RobustTcpAgent('127.0.0.1', 8080);
agent.connect();

效益分析表格與解讀

網路讀寫難點 原始設計缺陷 Agent Reach 容錯設計 對系統防護的效益
TCP 粘包/半包 假設一次 data = 完整數據 使用 Buffer.concat 與 4-Bytes 長度前綴 (Length-Prefixed) 確保資料的完整性,根除解析中斷導致的程式崩潰
斷線重連 拋出 Error 即死亡 監聽 close 事件並實作 setTimeout 指數/固定退避 實現守護程序 (Daemon) 般的韌性,網路恢復自動上線
錯誤邊界控制 未處理 JSON 解析錯誤 try-catch 包覆 JSON.parse 且限制影響範圍 防止惡意或損毀的封包內容導致整個長連線系統崩潰

解讀: 開發基於 TCP 的應用程式,往往比 HTTP 充滿更多不確定性。Agent Reach 提倡「絕不相信網路狀態與傳入長度」。 我們透過精準的 Buffer 操作手動維護「封包邊界」,這不僅遵循了極簡主義(不依賴龐大的第三方通訊框架),還從底層徹底阻絕了 I/O 操作中常見的資料錯亂問題。