Close Codes & Resume
เราได้ครอบคลุมการจบของ connection โดยอุบัติเหตุไปแล้ว — drop, detection, reconnect บทเรียนสุดท้ายนี้ครอบคลุมการจบแบบ ตั้งใจ: close handshake ที่สะอาด code ตัวเลขที่อธิบายว่าทำไม connection ถึงจบ และผลตอบแทนของการทำ reconnection ให้ดี — การตามเก็บทุกอย่างที่คุณพลาดไปในระหว่างที่คุณหายไป
Close handshake
หัวข้อที่มีชื่อว่า “Close handshake”การ close WebSocket อย่างสะอาดเป็นบทสนทนาสองขั้นตอนเล็ก ๆ ไม่ใช่การกระชากสายทิ้ง:
- ฝ่ายหนึ่งส่ง close frame ที่บรรจุ code ตัวเลขและ reason string สั้น ๆ ที่เป็น optional
- อีกฝ่ายตอบด้วย close frame ของตัวเอง
- หลังจากนั้นเท่านั้น 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 code
หัวข้อที่มีชื่อว่า “การอ่าน close code”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 เพื่อให้อีกฝ่ายตัดสินใจได้อย่างนั้นพอดี
การ shutdown อย่างสะอาด
หัวข้อที่มีชื่อว่า “การ shutdown อย่างสะอาด”เมื่อแอปของคุณจบ 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 ที่เราพยายามหลีกเลี่ยงพอดี
Resume — การเล่นซ้ำสิ่งที่คุณพลาด
หัวข้อที่มีชื่อว่า “Resume — การเล่นซ้ำสิ่งที่คุณพลาด”นี่คือรางวัลสำหรับกลไก reconnect ทั้งหมด เมื่อ client drop และกลับมา แอปแบบไร้เดียงสาจะมีรู: ทุกข้อความที่ server ส่งในระหว่างช่องว่างนั้นหายไป ทางรักษาคือ resumable state ที่ key ด้วย last-seen id
รูปแบบ:
- server ประทับทุกข้อความด้วย id ที่เพิ่มขึ้นแบบ monotonic:
1, 2, 3, ... - client จำ id ของ message สุดท้ายที่ประมวลผลสำเร็จไว้ใน
lastSeenId - เมื่อ reconnect client ส่ง
lastSeenIdไปยัง server เป็นส่วนหนึ่งของ resume request - server เล่นซ้ำทุกข้อความที่มี id มากกว่า
lastSeenIdแล้วกลับมาส่งแบบสด
client กลับมาต่อที่จุดค้างไว้พอดี ไม่มีช่องว่างและไม่มีตัวซ้ำ (ฝั่ง server ต้องเก็บ buffer สั้น ๆ ของ message ล่าสุดไว้ให้เล่นซ้ำได้ ถ้าช่องว่างเก่ามากจริง ๆ อาจต้อง refresh state ทั้งก้อนแทน แต่สำหรับการ drop สั้น ๆ การเล่นซ้ำจะราบรื่น)
close-and-resume แบบสด
หัวข้อที่มีชื่อว่า “close-and-resume แบบสด”เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock server ใส่เลขให้ทุก message ที่ส่งออกไป เราปล่อยให้ client รับสองสามตัว แล้ว connection ก็ drop (close code 1006) ระหว่างที่ server ยังผลิต message ต่อไป พอ reconnect client ส่ง lastSeenId ไปให้ แล้ว server ก็เล่นซ้ำเฉพาะ message ที่พลาดไปพอดี — จากนั้นกลับมาส่งแบบสดตามปกติ
ดูลำดับ: 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? |
|---|---|---|
| 1000 | Normal — ปิดตั้งใจ | ไม่ |
| 1001 | Going Away — server กำลัง shutdown | ใช่ — สักครู่ |
| 1006 | Abnormal — ไม่มี close frame | ใช่ — ด้วย backoff |
| 4000-4999 | Application-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 ส่ง
goodbyemessage ก่อน close — client รู้ว่าเป็น intentional- reconnect พร้อม last event timestamp เพื่อ replay missed messages