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

Close Codes & Resume

เราได้ครอบคลุมการจบของ connection โดยอุบัติเหตุไปแล้ว — drop, detection, reconnect บทเรียนสุดท้ายนี้ครอบคลุมการจบแบบ ตั้งใจ: close handshake ที่สะอาด code ตัวเลขที่อธิบายว่าทำไม connection ถึงจบ และผลตอบแทนของการทำ reconnection ให้ดี — การตามเก็บทุกอย่างที่คุณพลาดไปในระหว่างที่คุณหายไป

การ close WebSocket อย่างสะอาดเป็นบทสนทนาสองขั้นตอนเล็ก ๆ ไม่ใช่การกระชากสายทิ้ง:

  1. ฝ่ายหนึ่งส่ง close frame ที่บรรจุ code ตัวเลขและ reason string สั้น ๆ ที่เป็น optional
  2. อีกฝ่ายตอบด้วย close frame ของตัวเอง
  3. หลังจากนั้นเท่านั้น TCP connection ที่อยู่เบื้องล่างจึงถูกรื้อทิ้ง

นี่คือเหตุผลที่โมดูล foundations ยืนยันว่าการ close WebSocket นั้นสมมาตร เมื่อทำงานได้ตามนั้น ทั้งสองฝ่ายเห็นพ้องว่า connection จบแล้วและไม่มีใครต้องเดา เบราว์เซอร์เปิดเผยเรื่องนี้ผ่าน event onclose โดยตัว event บรรจุ code, reason และ flag wasClean ที่บอกว่า handshake ปิดครบถ้วนหรือไม่

sequenceDiagram
  participant A as Side A
  participant B as Side B
  A->>B: close frame (code, reason)
  B-->>A: close frame (echo)
  Note over A,B: TCP connection torn down
  Note over A,B: onclose fires with wasClean = true
Close handshake ที่สะอาด

Code คือสาเหตุการตายของ connection รู้จัก code ไว้ก็จะรู้ว่าควร reconnect ยอมแพ้ หรือไปแก้บั๊ก ตัวที่สำคัญ:

  • 1000 — Normal Closure ทุกอย่างจบลงตามที่ตั้งใจ การ ws.close() อย่างตั้งใจที่แอปของคุณเรียก อย่า auto-reconnect เมื่อเจอตัวนี้ การ close นั้นตั้งใจ
  • 1001 — Going Away ฝ่ายหนึ่งกำลังจากไป — แท็บเบราว์เซอร์ที่กำลัง navigate ออกไป หรือ server ที่กำลัง shutdown เพื่อ deploy client ที่เห็น 1001 มักควร reconnect หลังจากสักครู่ server กำลังจะกลับมา
  • 1006 — Abnormal Closure ตัวที่คุณเจอในบทเรียน drop หมายความว่า connection ตาย โดยไม่มี close frame ที่เหมาะสม — และที่สำคัญ ไม่เคยมี code ใดถูกส่งผ่านสายสำหรับ 1006 เบราว์เซอร์เป็นคนสร้างขึ้นมาเองในเครื่องเพื่อบอกว่า “อันนี้จบแบบผิดปกติ” ทุกการ drop โดยอุบัติเหตุจะโผล่มาเป็น 1006 นี่คือสัญญาณให้ reconnect ด้วย backoff
  • 1011 — Internal Error server พบเงื่อนไขที่ไม่คาดคิดและกำลังจบ connection การ reconnect สมเหตุสมผล แต่ 1011 ที่เกิดต่อเนื่องหมายถึงบั๊กของ server ที่ต้องไล่ล่า
  • 4000-4999 — Application code ช่วงนี้ทั้งหมดสงวนไว้ให้ คุณ protocol จะไม่นิยามอะไรในช่วงนี้เลย คุณกำหนดความหมายเองได้ทั้งหมด: 4001 อาจเป็น “auth token expired”, 4002 “kicked by an admin”, 4008 “you are rate limited” client สามารถแยกสาขาตามตัวเหล่านี้ได้ — refresh token แล้ว reconnect เมื่อเจอ 4001 แต่หยุดและแสดงข้อความเมื่อเจอ 4002

กฎในทางปฏิบัติ: 1000 หมายถึงหยุด ส่วนใหญ่ที่เหลือหมายถึง reconnect และ 4000+ หมายถึงทำสิ่งที่คุณตัดสินใจไว้ว่า code นั้นหมายถึงอะไร ใส่ code เสมอ (และในอุดมคติคือ reason ด้วย) เมื่อ คุณ เป็นฝ่าย close เพื่อให้อีกฝ่ายตัดสินใจได้อย่างนั้นพอดี

เมื่อแอปของคุณจบ connection อย่างตั้งใจ — ผู้ใช้ logout, navigate ออกไป หรือปิดฟีเจอร์ — ให้ close อย่างเหมาะสม:

function shutdown(ws: WebSocket) {
// 1000 = normal. The peer learns this was intentional and will not reconnect.
ws.close(1000, 'client navigating away');
}

เหตุผลที่การ close อย่างสะอาดสำคัญคือด้านกลับของทุกอย่างในโมดูลนี้: connection ที่จบด้วย code 1000 บอกอีกฝ่ายว่า “อย่า reconnect นี่ตั้งใจให้เกิดขึ้น” ถ้าข้ามขั้นนี้ไป คุณก็ทิ้งให้ server ถือ connection ผีที่ต้องคอย heartbeat จนกว่าจะรู้ว่าตาย — ก็คือความยุ่งเหยิงแบบ half-open ที่เราพยายามหลีกเลี่ยงพอดี

นี่คือรางวัลสำหรับกลไก reconnect ทั้งหมด เมื่อ client drop และกลับมา แอปแบบไร้เดียงสาจะมีรู: ทุกข้อความที่ server ส่งในระหว่างช่องว่างนั้นหายไป ทางรักษาคือ resumable state ที่ key ด้วย last-seen id

รูปแบบ:

  1. server ประทับทุกข้อความด้วย id ที่เพิ่มขึ้นแบบ monotonic: 1, 2, 3, ...
  2. client จำ id ของ message สุดท้ายที่ประมวลผลสำเร็จไว้ใน lastSeenId
  3. เมื่อ reconnect client ส่ง lastSeenId ไปยัง server เป็นส่วนหนึ่งของ resume request
  4. server เล่นซ้ำทุกข้อความที่มี id มากกว่า lastSeenId แล้วกลับมาส่งแบบสด

client กลับมาต่อที่จุดค้างไว้พอดี ไม่มีช่องว่างและไม่มีตัวซ้ำ (ฝั่ง server ต้องเก็บ buffer สั้น ๆ ของ message ล่าสุดไว้ให้เล่นซ้ำได้ ถ้าช่องว่างเก่ามากจริง ๆ อาจต้อง refresh state ทั้งก้อนแทน แต่สำหรับการ drop สั้น ๆ การเล่นซ้ำจะราบรื่น)

เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock server ใส่เลขให้ทุก message ที่ส่งออกไป เราปล่อยให้ client รับสองสามตัว แล้ว connection ก็ drop (close code 1006) ระหว่างที่ server ยังผลิต message ต่อไป พอ reconnect client ส่ง lastSeenId ไปให้ แล้ว server ก็เล่นซ้ำเฉพาะ message ที่พลาดไปพอดี — จากนั้นกลับมาส่งแบบสดตามปกติ

JavaScript

ดูลำดับ: client รับ message 1 และ 2 แบบสด แล้ว connection ก็ drop (code 1006) ตอนที่ message 3 ยังคิวอยู่บน server พอ client reconnect ก็ส่ง lastSeenId=2 ไป แล้ว server เล่นซ้ำ เฉพาะ message 3 — ไม่มีช่องว่าง ไม่มีตัวซ้ำของ 1 หรือ 2 การตามเก็บที่ราบรื่นแบบนี้คือสิ่งที่แอป real-time ที่สร้างมาดีทำอยู่เบื้องหลัง และเป็นไปได้ก็เพราะ reconnection กับ last-seen id ทำงานร่วมกัน

Close Codeความหมายควร Reconnect?
1000Normal — ปิดตั้งใจไม่
1001Going Away — server กำลัง shutdownใช่ — สักครู่
1006Abnormal — ไม่มี close frameใช่ — ด้วย backoff
4000-4999Application-definedขึ้นกับความหมายของ code

Reconnect เมื่อได้รับ Code 1000 อาการ:

  • reconnect logic ไม่แยก 1000 จาก 1006
  • user logout → app reconnect วนซ้ำ
  • check code === 1000 → หยุด reconnect, code === 1006 → reconnect ด้วย backoff

ไม่ Track Last-Seen ID — ข้อมูลหาย อาการ:

  • reconnect แล้วไม่ request missed message
  • user เห็นช่องว่างในข้อมูล real-time
  • ส่ง lastSeenId ตอน reconnect ให้ server replay missed messages

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

Discord:

  • code 4xxx เช่น 4000 (Unknown), 4007 (Invalid seq) — app แยกว่าควร resume หรือ reconnect fresh

Slack RTM:

  • server ส่ง goodbye message ก่อน close — client รู้ว่าเป็น intentional
  • reconnect พร้อม last event timestamp เพื่อ replay missed messages
Close code 1006 (Abnormal Closure) บ่งบอกอะไร?
หลังจาก close code ใดที่ client โดยทั่วไป NOT ควร auto-reconnect?
last-seen id ทำให้ client ที่กำลัง reconnect resume ได้โดยไม่มีช่องว่างหรือตัวซ้ำได้อย่างไร?