ข้ามไปยังเนื้อหา

Schemas และ Versioning

แอป WebSocket จริงไม่เคยถูก deploy เพียงครั้งเดียว server ปล่อยตัวแก้ไข client อัปเดตตามตารางของตัวเอง mobile build เก่ายังคุยกับ server ใหม่ต่อไปอีกเป็น เดือน ๆ message protocol ของคุณต้องรอดผ่านความปั่นป่วนนั้น — และสองแนวคิดที่ทำให้รอดได้คือ message envelope กับ field version

แทนที่จะปล่อยให้ทุก message มีรูปร่างตามใจ จงตกลงกันเรื่องโครงสร้างภายนอกที่สม่ำเสมอ — envelope — แล้วใส่ข้อมูลเฉพาะของ message ไว้ข้างใน:

  • type — message นี้ชนิดอะไร (dispatch key จากบทเรียน framing)
  • v — version ของ protocol/schema ที่ message นี้ยึดตาม
  • payload — ข้อมูลจริง ส่วนที่แปรผันตาม type

ทุก message ในทั้งสองทิศทาง หน้าตาเหมือนกันจากภายนอก code routing อ่าน type codeความเข้ากันได้อ่าน v และมีเพียง handler เฉพาะแต่ละ type เท่านั้นที่แตะ payload message ที่ถูกต้องตามรูปแบบอ่านได้แบบ {"type":"chat","v":1,"payload":{...}}

flowchart LR
  subgraph env["Envelope — same for every message"]
    t["type: what"]
    v["v: which version"]
    p["payload: the varying data"]
  end
  t --> route["dispatch / routing"]
  v --> compat["compatibility handling"]
  p --> handler["per-type handler"]
envelope ที่สม่ำเสมอห่อหุ้ม payload ที่แปรผัน

อะไรก็มาถึง socket ได้: client ที่มีบั๊ก, build เก่า, message ที่ผิดรูปแบบหรือแม้กระทั่งประสงค์ร้าย ก่อนที่ handler จะแตะ message จง validate envelope: ต้อง parse เป็น JSON ได้ มี type ที่รู้จัก พก v และมี field ที่ type นั้นต้องการครบ นอกจากนั้นให้ปฏิเสธด้วย error ที่ชัดเจน แทนที่จะปล่อยเข้าไปทำ handler crash กลางคัน ใน production คุณจะทำสิ่งนี้ด้วย schema library (Zod, Ajv/JSON Schema หรือ Protobuf validator ที่ถูก generate) หลักการเหมือนกันในทุกระดับขนาด: ข้อมูลที่เข้ามาถือว่าไม่น่าเชื่อถือจนกว่าจะพิสูจน์ได้ว่าถูกต้องตามรูปแบบ

เมื่อ client อยู่ในมือผู้ใช้แล้ว คุณเปลี่ยนความหมายของ field เดิมไม่ได้แล้ว — client เก่ายังอ่านแบบเดิมอยู่ กฎที่ทำให้ protocol ยังมีชีวิตอยู่:

  • เพิ่ม อย่าเอามาใช้ใหม่ field ใหม่ที่เป็น optional ปลอดภัย client เก่าเพิกเฉยสิ่งที่ไม่รู้จัก client ใหม่อ่านข้อมูลเพิ่มเติม แต่การ ลบ field หรือ เปลี่ยนความหมาย จะทำให้ใครสักคนพังแน่นอน
  • bump v สำหรับการเปลี่ยนแปลงที่ breaking เมื่อการเปลี่ยนแปลงเพิ่มแบบ additive ไม่ได้จริง ๆ จงยก version ขึ้นและให้ server รองรับค่า v ทั้งสองในช่วงเวลา migration โดยแตกแขนงตาม v
  • ใส่ค่าเริ่มต้นให้สิ่งที่ขาดหาย เมื่อ field ใหม่ไม่มีอยู่ (client เก่าไม่ได้ส่งมา) ฝั่งรับใส่ค่าเริ่มต้นที่สมเหตุสมผลแทนที่จะล้มเหลว

นิสัยสามอย่างนี้ทำให้ client v1 และ v2 ใช้ server เดียวกันร่วมกันได้ไม่จำกัด

เดโม validate ทุก envelope ที่เข้ามา แล้วแสดงการพัฒนาแบบ additive ในการทำงานจริง: server เข้าใจ chat message แบบ v1 และ message แบบ v2 ที่เพิ่ม field lang แบบ optional — จัดการได้ทั้งสองอย่าง และใส่ค่าเริ่มต้นให้ lang เมื่อ client เก่าละไว้ ส่วน message ที่จงใจทำให้ผิดรูปแบบจะโดนปฏิเสธ ไม่ใช่ crash ทั้งหมดนี้รันบน echo socket ภายในหน้าที่ใช้ WebSocket API จริง

JavaScript

message v1 และ message v2 ไหลผ่าน handleChat เดียวกัน ทั้งคู่ field lang ใหม่โผล่เฉพาะตอนที่ส่งมาจริง และเติมค่าเริ่มต้นเป็น en เมื่อไม่มี — นั่นคือการพัฒนาแบบ additive ที่กำลังทำงาน ส่วน message ตัวที่สามไม่เคยไปถึง handler เพราะ validation สกัดไว้ตั้งแต่หน้าประตู สร้างนิสัยสองอย่างนี้ตั้งแต่วันแรก แล้ว protocol ของคุณจะเติบโตได้เป็นปี ๆ โดยไม่ต้องมีวัน flag day

Evolution StrategyความปลอดภัยComplexity
เพิ่ม optional field (additive)สูง — client เก่าเพิกเฉยได้ต่ำ
Bump version fieldปานกลาง — server ต้อง support หลาย versionปานกลาง
Breaking change ไม่มี migrationต่ำ — client เก่าพัง-

ไม่ Validate Message ก่อน Handle อาการ:

  • msg.payload.text โดยไม่ check ว่า payload มีอยู่ — crash เมื่อ client ส่ง malformed message
  • ทำให้ connection crash ด้วย unhandled error
  • validate envelope ก่อนเสมอ: ต้องมี type, v, payload

เปลี่ยนความหมาย Field ที่มีอยู่แล้ว อาการ:

  • "status": "ok" เปลี่ยนเป็น "status": 200 (string → number) — client เก่า parse ผิด
  • behavior เปลี่ยนโดย client เก่าไม่รู้ตัว
  • เพิ่ม field ใหม่เสมอ อย่าเปลี่ยน field เดิม — bump v เมื่อ breaking change หลีกเลี่ยงไม่ได้

💡 ตัวอย่างจากของจริง

Discord Gateway:

  • ทุก event มี op (opcode), d (data), s (sequence), t (event name)
  • envelope สม่ำเสมอ — client dispatch ตาม op ก่อนแล้วดู t

Twitch EventSub:

  • ทุก message มี metadata.message_type และ metadata.message_id
  • version field ใน subscription type — รองรับหลาย version พร้อมกัน
จุดประสงค์ของ message envelope คืออะไร?
ทำไมต้อง validate message ที่เข้ามาก่อนลงมือจัดการ?
การเปลี่ยนแปลงใดที่ทำให้ client เก่ายังทำงานกับ server ใหม่ได้?