Schemas และ Versioning
แอป WebSocket จริงไม่เคยถูก deploy เพียงครั้งเดียว server ปล่อยตัวแก้ไข client อัปเดตตามตารางของตัวเอง mobile build เก่ายังคุยกับ server ใหม่ต่อไปอีกเป็น เดือน ๆ message protocol ของคุณต้องรอดผ่านความปั่นป่วนนั้น — และสองแนวคิดที่ทำให้รอดได้คือ message envelope กับ field version
envelope
หัวข้อที่มีชื่อว่า “envelope”แทนที่จะปล่อยให้ทุก 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"] validate สิ่งที่เข้ามา — อย่าเชื่อ wire เด็ดขาด
หัวข้อที่มีชื่อว่า “validate สิ่งที่เข้ามา — อย่าเชื่อ wire เด็ดขาด”อะไรก็มาถึง 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 แล้วค่อยพัฒนา
หัวข้อที่มีชื่อว่า “เดโม: validate แล้วค่อยพัฒนา”เดโม validate ทุก envelope ที่เข้ามา แล้วแสดงการพัฒนาแบบ additive ในการทำงานจริง: server เข้าใจ chat message แบบ v1 และ message แบบ v2 ที่เพิ่ม field lang แบบ optional — จัดการได้ทั้งสองอย่าง และใส่ค่าเริ่มต้นให้ lang เมื่อ client เก่าละไว้ ส่วน message ที่จงใจทำให้ผิดรูปแบบจะโดนปฏิเสธ ไม่ใช่ crash ทั้งหมดนี้รันบน echo socket ภายในหน้าที่ใช้ WebSocket API จริง
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ก่อนแล้วดูtTwitch EventSub:
- ทุก message มี
metadata.message_typeและmetadata.message_id- version field ใน subscription type — รองรับหลาย version พร้อมกัน