RPC over WebSocket
Pub/sub เกี่ยวกับ การ broadcast ความสนใจ แต่บางครั้ง client ต้องการสิ่งตรงข้าม: คำตอบที่แม่นยำของคำถามเฉพาะข้อหนึ่ง — “ประวัติของห้องนี้เป็นอย่างไร?”, “username นี้ถูกใช้ไปแล้วหรือยัง?” บน HTTP นั่นก็แค่ request คู่กับ response แต่บน WebSocket ไม่มีอะไรแบบนั้นมาให้ในตัว: คุณ send message ออกไป แล้ว onmessage ก็ทำงานแยกต่างหากสำหรับ ทุก message ที่เข้ามา ไม่มีอะไรผูก reply กลับไปยังคำถามต้นทาง RPC over WebSocket คือ pattern เล็ก ๆ ที่สร้าง request/response ขึ้นมาใหม่บน stream message ทางเดียว
ปัญหา: message ไม่มี reply
หัวข้อที่มีชื่อว่า “ปัญหา: message ไม่มี reply”WebSocket คือ stream ของ frame ที่เป็นอิสระต่อกันในแต่ละทิศทาง เมื่อคุณเรียก ws.send แล้วต่อมา message มาถึง คุณไม่มีวิธีในระดับภาษาที่จะรู้ว่า message นั้น ตอบ การ send นี้ ถ้าคุณยิงสาม request แล้วสาม reply กลับมา — ซึ่งอาจสลับลำดับกัน — ทั้งหมดจะลงมาที่ handler onmessage เดียวกันเป็นกองที่แยกแยะไม่ออก คุณต้องเย็บแต่ละ reply เข้ากับ request ต้นทางเอง
ทางแก้: correlation id
หัวข้อที่มีชื่อว่า “ทางแก้: correlation id”เคล็ดลับคือติด correlation id ที่ไม่ซ้ำกันให้ทุก request และกำหนดให้ server สะท้อน id เดียวกันนั้นกลับมาบน reply ฝั่ง client เก็บตารางของ request ที่ กำลังรอ โดยใช้ id เป็น key เมื่อ message มาถึง ก็อ่าน id ออกมา ค้นหา promise ที่กำลังรออยู่ แล้ว resolve ทิ้ง ขั้นตอนเป็นแบบนี้:
sequenceDiagram
participant App as App code
participant RPC as RPC client
participant WS as WebSocket
participant Srv as Server
App->>RPC: call('getUser', {id: 7})
RPC->>RPC: id = 'r1'; store pending['r1'] = resolve
RPC->>WS: send {id:'r1', method:'getUser', params:{id:7}}
WS->>Srv: {id:'r1', ...}
Srv-->>WS: {id:'r1', result:{name:'Sam'}}
WS-->>RPC: onmessage {id:'r1', result:...}
RPC->>RPC: lookup pending['r1'] -> resolve(result)
RPC-->>App: promise resolves with {name:'Sam'} เพราะแต่ละ request พก id ของตัวเอง reply จึงมาถึงในลำดับใดก็ได้ — id ไม่ใช่ลำดับการมาถึง ที่ตัดสินว่า promise ใด resolve นั่นคือสิ่งที่ทำให้การเชื่อมต่อเดียวปลอดภัยที่จะ multiplex ข้ามการเรียกพร้อมกันหลายครั้ง
สามชิ้นส่วนที่คุณต้องมีเสมอ
หัวข้อที่มีชื่อว่า “สามชิ้นส่วนที่คุณต้องมีเสมอ”RPC client บน WebSocket ที่ใช้งานได้จริงโดยพื้นฐานคือสามสิ่ง:
- pending map.
Map<id, { resolve, reject, timer }>— หนึ่งรายการต่อหนึ่งการเรียกที่กำลังบินอยู่ ถูกลบออกเมื่อ reply ลงมาหรือเมื่อการเรียก timeout - id generator. แหล่งกำเนิด string ที่ไม่ซ้ำกันอันใดก็ได้ — counter,
crypto.randomUUID()ขอแค่ไม่ซ้ำกันในบรรดาการเรียกที่ กำลังรออยู่ในขณะนั้น - timeout. จุดอ่อนร้ายแรงของ RPC แบบไร้เดียงสาคือ reply ที่ไม่มีวันมา (server crash, message หลุด) ถ้าไม่มี timeout promise จะค้างตลอดกาลและ pending map จะรั่ว ทุกการเรียกจะได้ timer ที่ reject และเคลียร์ตัวเองหากไม่มี reply มาถึงในเวลาที่กำหนด
ดูของจริงทำงานสด ๆ
หัวข้อที่มีชื่อว่า “ดูของจริงทำงานสด ๆ”demo ต่อสาย mock สไตล์ WebSocket จริงเข้ากับ RPC client ที่สมบูรณ์ เราเรียกสำเร็จสองครั้ง (add และ echo) ซึ่ง reply มาถึง สลับลำดับกัน และเรียกหนึ่งครั้งไปยัง method ที่ server เพิกเฉย — เพื่อพิสูจน์ว่า timeout ทำงานและ reject แทนที่จะค้าง ลองรันแล้วดูว่า id จับคู่ reply เข้ากับการเรียกได้ไม่ว่าลำดับจะเป็นอย่างไร และ reply ที่หายไปจะ reject ตามกำหนด
สังเกตลำดับใน log: add และ echo ถูก ส่ง เป็นอันแรกและอันที่สอง แต่ echo resolve ก่อนเพราะ reply กลับมาเร็วกว่า — และนั่นไม่เป็นไร เพราะแต่ละ promise ถูก key ด้วย id ไม่ใช่ด้วยลำดับ ส่วน noReply ไม่เคยได้คำตอบ พอครบ timeout promise จึง reject ด้วย error ที่ชัดเจน แล้วรายการ pending ก็ถูกลบทิ้ง การเคลียร์ตรงนี้แหละคือความแตกต่างระหว่าง client ที่ทนทานกับ memory leak ที่ค่อย ๆ รั่ว
ออกแบบ wire format
หัวข้อที่มีชื่อว่า “ออกแบบ wire format”รักษา envelope ให้น้อยที่สุดและสมมาตร request พก id, method และ params ส่วน reply พก id เดียวกันและไม่ result ก็ error ถ้าคุณต้องการมาตรฐานแทนการประดิษฐ์เอง JSON-RPC 2.0 นิยามรูปร่างนี้ไว้พอดี — id, method, params, result/error — และเป็นค่าตั้งต้นที่สมเหตุสมผลในการลอกมาใช้ ไม่ว่าทางไหนกฎก็ตายตัว: reply ต้องสะท้อน id ของ request ไม่งั้น client จะ route กลับไม่ถูก
| RPC Pattern | ทำงานอย่างไร | เหมาะกับ |
|---|---|---|
| Request-Response | ส่ง request พร้อม id, รอ response ที่ match id | Operation ที่ต้องการผลลัพธ์ |
| Notification | ส่งข้อความโดยไม่รอ response | Event, log, metric |
| Server Push | server ส่งโดยไม่มี request | Real-time update |
| Bidirectional Stream | ทั้งสองฝั่งส่งพร้อมกัน | Audio/video, game state |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ไม่มี Request Timeout สำหรับ RPC อาการ:
- ส่ง RPC request แล้วรอ response ไม่จำกัดเวลา
- server ไม่ตอบ (crash, bug) → client hang ตลอดไป
- ตั้ง timeout ต่อ request: resolve error เมื่อ response ไม่มาใน N วินาที
ใช้ WebSocket RPC เมื่อ HTTP REST เพียงพอ อาการ:
- operation ที่ทำครั้งเดียว (login, create post) ผ่าน WebSocket RPC
- overhead จาก persistent connection โดยไม่จำเป็น
- ใช้ HTTP สำหรับ stateless operation, WebSocket สำหรับ stateful real-time
💡 ตัวอย่างจากของจริง
tRPC over WebSocket:
- type-safe RPC บน WebSocket — subscription สำหรับ real-time, query/mutation สำหรับ HTTP
- Discord, Linear ใช้ pattern คล้ายกันสำหรับ real-time feature
JSON-RPC 2.0 over WebSocket:
- protocol มาตรฐานสำหรับ RPC บน WebSocket
- request:
\{ "jsonrpc": "2.0", "method": "sendMessage", "params": \{...\}, "id": 1 \}