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

การ Framing Messages

มีของขวัญละเอียดอ่อนชิ้นหนึ่งที่ WebSocket protocol มอบให้คุณ และมีกับดักละเอียดอ่อนอยู่ข้าง ๆ กันด้วย ของขวัญคือ message มาถึงครบทั้งก้อน ส่วนกับดักคือการเชื่อว่าครบทั้งก้อนแปลว่า มีความหมาย บทเรียนนี้ว่าด้วยช่องว่างระหว่างสองสิ่งนั้น

ถ้าคุณเคยเขียนcodeกับ TCP socket ดิบ ๆ คุณจะรู้ถึงความเจ็บปวด: TCP คือ byte stream คุณอาจเรียก send('{"a":1}') หนึ่งครั้งที่ฝั่งหนึ่ง แต่อีกฝั่งอาจได้รับ '{"a' แล้วตามด้วย ':1}' หรือได้ทั้งสองก้อนในครั้งเดียวติดกับ message ถัดไป ไม่มีขอบเขตใด ๆ คุณต้องประดิษฐ์ framing ขึ้นมาเอง — length prefixes, delimiters หรืออะไรสักอย่าง — เพื่อรู้ว่า message หนึ่งจบตรงไหนและอันถัดไปเริ่มตรงไหน

WebSocket แก้ปัญหานั้นให้คุณแล้ว protocol ทำ framing ให้สตรีมที่ระดับ wire ดังนั้น onmessage จึงทำงาน หนึ่งครั้งต่อหนึ่ง message ที่คุณส่ง โดย payload มาครบทั้งก้อน หนึ่ง send ที่ฝั่งหนึ่งกลายเป็น onmessage หนึ่งครั้งพอดีที่อีกฝั่ง

flowchart TB
  subgraph tcp["Raw TCP — a byte stream"]
    direction LR
    t1["send A · send B"] --> t2["recv: A+par<br/>recv: t of B"]
    t2 --> t3["YOU must reframe"]
  end
  subgraph wsf["WebSocket — discrete messages"]
    direction LR
    w1["send A · send B"] --> w2["onmessage: A<br/>onmessage: B"]
    w2 --> w3["boundaries already correct"]
  end
TCP ดิบ ๆ ส่งมอบสตรีม ส่วน WebSocket ส่งมอบ message ที่แยกเป็นชิ้น ๆ

ดังนั้นปัญหา framing ระดับล่าง — message จบตรงไหน — หายไปแล้ว วิเศษมาก แต่นั่นบอกเพียงว่า bytes ถูกจัดกลุ่มอย่างถูกต้อง ไม่ได้บอกเลยว่า bytes ที่ถูกจัดกลุ่มเหล่านั้น มีความหมายอะไร

นี่คือกับดัก เพราะ message มาถึงครบทั้งก้อน จึงมีแรงดึงดูดให้แค่ send('user joined: bob') แล้วถือว่าเสร็จ วิธีนี้ได้ผล — สำหรับ message เพียงชนิดเดียวเท่านั้น ทันทีที่คุณมีชนิดที่สอง (“user left”, “new message”, “typing…”) ฝั่งรับของคุณก็ถูกลดทอนลงเหลือเพียงการดมกลิ่น string อย่างเปราะบาง: string นี้ขึ้นต้นด้วย “user joined” ไหม? มี colon ไหม? นั่นคือ parser ที่คุณกำลังเขียนด้วยมือโดยไม่ตั้งใจ และเขียนได้แย่ด้วย

วินัยที่ขยายตัวได้นั้นเรียบง่ายและคุ้มค่าที่จะจดจำ:

  • หนึ่ง JSON object ต่อหนึ่ง WebSocket message อย่าอัด message เชิงตรรกะสองอันเข้าไปใน send เดียว และอย่าแยก message เชิงตรรกะหนึ่งอันออกเป็นหลาย ๆ ครั้ง
  • ทุก message มี field type ฝั่งรับแตกแขนงตาม type ไม่ใช่ตามรูปร่างของ string
  • payload อยู่ใน field ที่มีชื่อ ดังนั้นการเพิ่มข้อมูลภายหลังจึงไม่เคยทำลายการ parsing ที่มีอยู่เดิม

นี่คือ application-level framing ของคุณ: WebSocket ทำ framing ให้ bytes ส่วน field type ของคุณทำ framing ให้ ความหมาย

เดโมส่ง message ชนิด ต่าง ๆ สามชนิดผ่าน connection เดียว แต่ละอันเป็น JSON object หนึ่งอันที่มี type และฝั่งรับ dispatch ตาม type นั้น — ไม่มีการดมกลิ่น string ที่ใดเลย เดโมรันในเบราว์เซอร์ของคุณบน echo socket ภายในหน้าที่ใช้ WebSocket API จริง

JavaScript

สาม send กลายเป็น onmessage สามครั้ง — WebSocket รักษาขอบเขตให้สมบูรณ์แบบ และเพราะแต่ละ message ประกาศ type ของตน ฝั่งรับจึงจัดการทั้งสามชนิดด้วย if/else ที่สะอาดแทนที่จะเดาจาก text เพิ่ม message ชนิดที่สี่ในวันพรุ่งนี้ แล้วมีเพียงแขนงใหม่หนึ่งแขนงเท่านั้นที่เปลี่ยนแปลง

Message Designข้อดีข้อเสีย
One JSON object per message + type fieldrouting ชัดเจน, extensibleverbose กว่า binary
String-based (no structure)ง่าย debugfragile, ไม่ scale เมื่อ message types เพิ่ม
Binary framing (Protobuf/MessagePack)compact, เร็วต้องมี schema, ไม่ human-readable

ยัด Message หลายอันใน send() เดียว อาการ:

  • ws.send('joined:bob\nchat:hello') — ยัด 2 event ในครั้งเดียว
  • receiver parse ผิด — บางครั้งทำงาน บางครั้งไม่
  • ส่งหนึ่ง JSON object ต่อ send() เสมอ — WebSocket เก็บ boundary ให้แล้ว

Dispatch ตาม String Shape แทน type Field อาการ:

  • if (data.startsWith('user joined')) — เปราะบาง แก้ยาก
  • message format เปลี่ยนนิดเดียว → ทุก handler พัง
  • ใส่ type field ทุก message แล้ว dispatch ตามนั้น

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

Slack RTM API:

  • ทุก message มี type field: "type": "message", "type": "user_typing", "type": "presence_change"
  • client dispatch ตาม type — ชัดเจน extensible

Discord Gateway:

  • ใช้ op (opcode) แทน type เพื่อแยก application message จาก protocol control
  • client แตกแขนงตาม opcode ก่อน แล้วจึงดู payload
การส่งมอบ message ของ WebSocket ต่างจาก TCP ดิบ ๆ อย่างไร?
ในเมื่อ message มาถึงครบทั้งก้อนอยู่แล้ว ทำไมยังต้องออกแบบ application protocol?
วินัยการ framing ที่แนะนำในบทเรียนนี้คืออะไร?