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

Frames และโปรโตคอล

Handshake เปิดการเชื่อมต่อให้ จากจุดนั้นเป็นต้นไป ทุกไบต์ของทราฟฟิก WebSocket เคลื่อนที่ในแพ็กเก็ตขนาดเล็กที่มีโครงสร้างซึ่งเรียกว่า frame คุณจะแทบไม่ได้จัดการ frame ด้วยมือ — WebSocket API ของเบราว์เซอร์และไลบรารีฝั่ง server ทำให้คุณ — แต่การรู้ว่าเกิดอะไรขึ้นภายใต้การเรียก ws.send('hello') ของคุณ ทำให้ส่วนที่เหลือของคอร์สเข้าที่เข้าทาง

Frame คือหน่วยกะทัดรัดที่มี header เล็ก ๆ ตามด้วย payload header คือที่อยู่ของความฉลาดทั้งหมดใน protocol และแบกข้อมูลเหล่านี้ไว้ (ยังมีอย่างอื่นอีก):

  • opcode ที่บอกว่า frame นี้เป็น ชนิด ใด
  • บิต mask และ (สำหรับ client frame) masking key
  • ฟิลด์ length สำหรับ payload
  • บิต FIN ที่บ่งบอกว่า frame นี้ทำให้ข้อความสมบูรณ์หรือไม่

“ข้อความ” เชิงตรรกะหนึ่งข้อความของคุณกลายเป็น frame หนึ่งอันหรือมากกว่าบนสาย

opcode เพียงไม่กี่ตัวครอบคลุมทุกอย่างที่ WebSocket ทำ:

  • Text — สตริง UTF-8 นี่คือสิ่งที่คุณได้จาก ws.send('hello')
  • Binary — ไบต์ดิบ สำหรับสิ่งต่าง ๆ เช่น รูปภาพ ชิ้นส่วนเสียง หรือการเข้ารหัสที่มีประสิทธิภาพ
  • Ping และ Pong — คู่ heartbeat ฝ่ายหนึ่งส่ง ping อีกฝ่ายมีหน้าที่ตอบด้วย pong คู่นี้ทำให้ปลายฝั่งไหนก็ตรวจได้ว่าการเชื่อมต่อยังมีชีวิตอยู่ โดยไม่ต้องส่งข้อมูล application ออกไป (เราจะใช้สิ่งเหล่านี้อย่างหนักเมื่อไปถึงโมดูลวงจรชีวิตของการเชื่อมต่อ)
  • Close — การพูดอย่างสุภาพว่า “ฉันกำลังปิดสิ่งนี้” โดยอาจแบก status code และเหตุผลมาด้วย การปิดที่สะอาดคือทั้งสองฝ่ายแลกเปลี่ยน close frame กัน
flowchart LR
  msg["ws.send('hello')"] --> enc["encode as frame(s)"]
  enc --> op{"opcode?"}
  op -->|text| t["UTF-8 payload"]
  op -->|binary| b["raw bytes"]
  op -->|ping / pong| h["heartbeat"]
  op -->|close| x["close + status code"]
  t --> wire["bytes on the wire"]
  b --> wire
  h --> wire
  x --> wire
opcode ของ frame ไหลผ่านการเชื่อมต่อที่เปิดอยู่

มีกฎหนึ่งข้อที่ทำให้มือใหม่ประหลาดใจ: ทุก frame ที่ส่งจาก client ไป server ต้องถูก mask client จะ XOR payload กับ key สุ่มขนาด 4 ไบต์ที่แนบมาใน frame แล้ว server ก็ unmask ตอนรับ นี่ไม่ใช่การเข้ารหัส — ใครอ่านไบต์ได้ก็ย้อนกลับได้ — ตัวที่ปกป้องข้อมูลจริง ๆ คือ wss:// Masking มีอยู่ด้วยเหตุผลที่แคบกว่า: เพื่อหยุดยั้งเพจที่มุ่งร้ายไม่ให้สร้าง WebSocket payload ที่ทำให้ HTTP proxy เก่า ๆ ที่ไม่รู้จัก WebSocket ซึ่งนั่งอยู่ระหว่าง client กับ server สับสน frame จาก server ไป client ไม่ได้ ถูก mask

message ขนาดใหญ่ไม่จำเป็นต้องไปเป็น frame ยักษ์อันเดียว แต่ fragment ออกเป็นหลาย frame ได้: frame แรกแบก opcode โดยไม่ตั้งบิต FIN frame กลาง ๆ ส่ง payload ต่อ และ frame สุดท้ายตั้ง FIN เพื่อบอกจุดสิ้นสุด ฝั่งรับก็ประกอบกลับเป็น message เดียว วิธีนี้ทำให้ผู้ส่งเริ่ม stream ได้ก่อนรู้ความยาวทั้งหมด — มีประโยชน์กับข้อมูลขนาดใหญ่หรือข้อมูลที่ทยอยสร้างขึ้น

URL scheme สองแบบ สะท้อนเว็บเป๊ะ ๆ:

  • ws:// — การเชื่อมต่อ WebSocket ที่ไม่เข้ารหัส เหมาะสำหรับการพัฒนาในเครื่อง แต่ไม่ปลอดภัยเมื่อข้ามอินเทอร์เน็ตสาธารณะ เพราะใครก็ตามที่อยู่ตรงกลางสามารถอ่านและดัดแปลง frame ได้
  • wss:// — WebSocket บน TLS รูปแบบที่เข้ารหัส เทียบได้กับ https:// ของฝั่ง WebSocket และเป็นค่าเริ่มต้นที่ควรใช้ใน production เดโมสดทุกอันในคอร์สนี้เชื่อมต่อด้วย wss://

นี่คือส่วนที่ทำให้อุ่นใจ: API จัดการ frame, masking และการประกอบกลับให้คุณ พอคุณเขียน ws.send('hello, server') library ก็ encode เป็น masked text frame ให้ และเมื่อ message มาถึง ก็ยื่น string หรือ blob ที่ประกอบกลับเรียบร้อยแล้วให้คุณใน onmessage คุณออกแบบ message ส่วน protocol ขน frame การรู้จัก frame layer แค่ทำให้คุณเห็นภาพชัดตอนที่เราไปปรับ heartbeat จัดการข้อมูล binary และ debug การปิดในภายหลัง

Frame Typeใช้เมื่อข้อควรระวัง
Text (UTF-8)ส่ง JSON, string messageขนาด payload ไม่ควรใหญ่เกิน 64KB ต่อ frame
Binaryส่ง image, audio, Protobufต้อง parse ที่ receiver อย่างระวัง
Ping/Pongheartbeat ตรวจ connection มีชีวิตbrowser ไม่ให้ JS ส่ง Ping เอง — เป็นหน้าที่ server
Closeปิด connection อย่างนุ่มนวลควรรอ close frame ตอบกลับก่อน destroy socket

ส่ง Payload ขนาดใหญ่โดยไม่ Fragment อาการ:

  • ส่ง JSON ขนาด 10MB ใน single frame
  • receiver ต้องรอ frame ทั้งหมดก่อน process — blocking
  • ใช้ fragmentation หรือ pagination message แทน

ใช้ ws:// ใน Production อาการ:

  • ข้อมูลวิ่งแบบ plaintext ผ่าน network
  • ถูก intercept หรือ inject โดย MITM ได้
  • ใช้ wss:// เสมอใน production

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

Google Meet:

  • ใช้ Binary frame สำหรับ video/audio data (VP8, Opus codec)
  • ใช้ Text frame สำหรับ control message (mute, chat, participant list)

Figma:

  • ส่ง cursor position และ selection state เป็น Binary frame (Protobuf)
  • ลด payload size เมื่อเทียบกับ JSON text frame ได้ถึง 60%
ชุด opcode ของ frame ชุดใดที่โปรโตคอล WebSocket ใช้?
อะไรเป็นจริงเกี่ยวกับ masking?
คุณควรใช้ wss:// แทน ws:// เมื่อไหร่?
fragmentation ให้ผู้ส่งทำอะไรได้?