實戰範例 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 操作中常見的資料錯亂問題。