ทำความรู้จักกับ Real-time Communication และเหตุผลที่ต้องใช้
เวลาเราเล่นเว็บปกติ เรามักจะเห็นข้อมูลอัปเดตก็ต่อเมื่อเรากดรีเฟรชหน้าเว็บใหม่เสมอ ซึ่งกระบวนการนี้เรียกว่า HTTP (โปรโตคอลมาตรฐานที่ใช้รับส่งข้อมูลบนเว็บ) ทำงานแบบขอแล้วตอบ คือฝั่งเราถามไป ฝั่งเซิร์ฟเวอร์ถึงจะตอบกลับมา ถ้าเราไม่ถาม เซิร์ฟเวอร์ก็จะไม่ส่งอะไรมาให้เราเลย ซึ่งมันไม่เหมาะกับแอปที่ต้องการความสดใหม่ตลอดเวลา
ลองจินตนาการถึงระบบแชต ถ้าเราต้องกดรีเฟรชหน้าเว็บทุกๆ 5 วินาทีเพื่อดูว่าเพื่อนตอบกลับมาหรือยัง มันคงเป็นประสบการณ์ที่แย่มาก Real-time Communication (การสื่อสารที่ข้อมูลอัปเดตทันทีโดยไม่ต้องรอ) จึงเข้ามาแก้ปัญหานี้ เพื่อให้แอปพลิเคชันของเราสามารถส่งข้อมูลหากันได้แบบสองทางตลอดเวลาเหมือนการคุยโทรศัพท์
สำหรับมือใหม่ที่กำลังฝึก Node.js (สภาพแวดล้อมที่ช่วยให้รันภาษา JavaScript บนเซิร์ฟเวอร์ได้) การเข้าใจเรื่องการสื่อสารแบบเรียลไทม์จะช่วยเปิดประตูให้เราทำโปรเจกต์เจ๋งๆ ได้มากมาย ไม่ว่าจะเป็นระบบแจ้งเตือน (Notification) ระบบกระดานหุ้น หรือแม้แต่เกมออนไลน์ง่ายๆ บนเบราว์เซอร์ ซึ่งนี่คือทักษะสำคัญที่ตลาดงานต้องการอย่างมาก
WebSockets vs HTTP: ต่างกันอย่างไรในมุมมองโปรแกรมเมอร์
ความแตกต่างสำคัญระหว่าง HTTP และ WebSockets (ช่องทางสื่อสารที่เปิดค้างไว้เพื่อส่งข้อมูลได้ตลอดเวลา) คือเรื่องของสถานะการเชื่อมต่อ ปกติ HTTP จะเปิดการเชื่อมต่อ ส่งข้อมูลเสร็จแล้วปิดทันที ทำให้เซิร์ฟเวอร์ไม่สามารถส่งข้อมูลหาเราได้เองถ้าเราไม่ได้เป็นคนเริ่มถามก่อน
แต่ WebSockets จะใช้วิธีการ "จับมือ" กันครั้งแรกผ่าน HTTP แล้วเปลี่ยนช่องทางนั้นให้กลายเป็นท่อสื่อสารที่เปิดค้างไว้ตลอดเวลา ทำให้เซิร์ฟเวอร์สามารถยิงข้อมูล (Push) มาหาเราได้ทันทีที่เหตุการณ์เกิดขึ้น โดยที่เราไม่ต้องไปนั่งกดถามซ้ำๆ ให้เปลืองทรัพยากรเครื่องอีกต่อไป
สำหรับการพัฒนาเว็บในยุคนี้ เรามักจะใช้ไลบรารีอย่าง Socket.IO (เครื่องมือที่ช่วยให้เราใช้งาน WebSockets ได้ง่ายและเสถียรขึ้น) เพราะมันช่วยจัดการเรื่องความซับซ้อนต่างๆ เช่น การเชื่อมต่อใหม่โดยอัตโนมัติหากเน็ตหลุด หรือการรองรับเบราว์เซอร์เก่าๆ ที่ไม่รู้จัก WebSockets ทำให้ชีวิตโปรแกรมเมอร์อย่างเราง่ายขึ้นมาก
// ตัวอย่างการติดตั้ง Socket.IO ฝั่งเซิร์ฟเวอร์
const express = require('express'); // ดึงไลบรารี Express มาใช้สร้างเว็บ
const http = require('http'); // ดึงไลบรารี HTTP มาสร้างเซิร์ฟเวอร์
const { Server } = require('socket.io'); // ดึงคลาส Server จาก Socket.IO
const app = express();
const server = http.createServer(app);
const io = new Server(server); // สร้าง Socket.IO ขึ้นมาครอบเซิร์ฟเวอร์หลัก
io.on('connection', (socket) => { // เมื่อมีคนเชื่อมต่อเข้ามา
console.log('มีผู้ใช้งานเชื่อมต่อเข้ามาแล้ว'); // แสดงข้อความใน Terminal
});
server.listen(3000, () => { // เริ่มรันเซิร์ฟเวอร์ที่พอร์ต 3000
console.log('เซิร์ฟเวอร์รันอยู่ที่ http://localhost:3000');
});
บรรทัดแรกเริ่มจากการเรียกใช้งานโมดูลที่จำเป็น ส่วน io.on('connection') คือจุดสำคัญที่คอยดักฟังว่ามีใครเชื่อมต่อเข้ามาบ้าง ถ้ามีคนเข้ามา เราก็แค่สั่งให้มันแสดงข้อความในคอนโซล ผลลัพธ์ที่ได้คือเมื่อรันไฟล์นี้ใน Terminal คุณจะเห็นข้อความว่า "เซิร์ฟเวอร์รันอยู่ที่ http://localhost:3000" และเมื่อเปิดเบราว์เซอร์เข้าหน้านี้ จะเห็นข้อความ "มีผู้ใช้งานเชื่อมต่อเข้ามาแล้ว" ปรากฏขึ้นมาทันที
การสร้างระบบแชตพื้นฐานด้วย Socket.IO
หัวใจสำคัญของระบบแชตคือการรับส่งข้อความ (Event) ระหว่างฝั่งไคลเอนต์ (ฝั่งผู้ใช้งานหรือเบราว์เซอร์) และฝั่งเซิร์ฟเวอร์ โดยเราจะใช้คำสั่ง emit (การส่งสัญญาณออกไป) เพื่อส่งข้อมูล และใช้ on (การรอรับสัญญาณ) เพื่อคอยฟังข้อมูลที่ถูกส่งเข้ามา
ลองนึกภาพว่าเรามีห้องแชต ถ้ามีคนพิมพ์ข้อความเข้ามา เซิร์ฟเวอร์ต้องทำหน้าที่รับข้อความนั้นแล้ว "กระจาย" ออกไปให้ทุกคนที่อยู่ในห้องเห็นเหมือนกัน ซึ่ง Socket.IO มีฟังก์ชันที่เรียกว่า io.emit ซึ่งทำหน้าที่ส่งข้อมูลไปยังทุกเครื่องที่เชื่อมต่ออยู่พร้อมกันได้เลยแบบรวดเร็ว
มือใหม่มักพลาดเรื่องการลืมจัดการเหตุการณ์ในฝั่งไคลเอนต์ให้ตรงกับฝั่งเซิร์ฟเวอร์ เช่น ตั้งชื่อเหตุการณ์ว่า message แต่ฝั่งหนึ่งสะกดผิดเป็น msg ทำให้ข้อมูลไม่ถึงกัน ดังนั้นการตั้งชื่อเหตุการณ์ให้ชัดเจนและเป็นระบบจึงเป็นนิสัยที่ดีของโปรแกรมเมอร์มืออาชีพ
// ฝั่งเซิร์ฟเวอร์: รับข้อความแล้วกระจายต่อ
io.on('connection', (socket) => {
socket.on('chat message', (msg) => { // รอรับเหตุการณ์ชื่อ chat message
io.emit('chat message', msg); // ส่งข้อความที่ได้รับไปยังทุกคน
});
});
โค้ดชุดนี้ใช้ socket.on เพื่อรอรับข้อมูลที่ส่งมาจากผู้ใช้คนใดคนหนึ่ง เมื่อเซิร์ฟเวอร์ได้รับแล้ว ก็จะใช้ io.emit เพื่อส่งข้อความนั้นกลับไปยังเบราว์เซอร์ของทุกคนที่เชื่อมต่ออยู่ ผลลัพธ์คือถ้าคุณเปิดเบราว์เซอร์สองหน้าต่างพร้อมกัน เมื่อพิมพ์แชตในหน้าต่างแรก ข้อความจะไปปรากฏในหน้าต่างที่สองทันทีโดยไม่ต้องรีเฟรชหน้าเว็บ
การส่งข้อมูลแจ้งเตือน (Notification) แบบเจาะจงบุคคล
บางครั้งเราไม่ได้อยากส่งข้อมูลให้ทุกคน แต่ต้องการส่งให้แค่บางคน เช่น ระบบแจ้งเตือนว่า "มีคนกดไลก์รูปของคุณ" ซึ่งเราต้องระบุให้ชัดเจนว่าส่งให้ใคร โดย Socket.IO จะสร้าง socket.id (รหัสประจำตัวเฉพาะของการเชื่อมต่อแต่ละครั้ง) มาให้เราใช้งานโดยอัตโนมัติ
เราสามารถเก็บรหัสเหล่านี้ไว้ในฐานข้อมูลหรือตัวแปรในหน่วยความจำ เพื่อระบุว่าผู้ใช้งานคนไหนใช้รหัสอะไร แล้วเมื่อถึงเวลาแจ้งเตือน เราก็แค่ส่งข้อมูลไปยังรหัสเฉพาะตัวนั้นๆ โดยใช้คำสั่ง socket.to(id).emit ซึ่งจะส่งไปหาแค่เครื่องที่ระบุไว้เท่านั้น
การเข้าใจเรื่องการระบุตัวตนนี้สำคัญมากสำหรับการทำแอปจริง เพราะถ้าเราส่งข้อมูลผิดพลาดไปหาคนอื่นนอกจากจะน่ารำคาญแล้ว ยังเป็นเรื่องของความเป็นส่วนตัวด้วย ดังนั้นการทดสอบระบบด้วยการเปิดหลายๆ เบราว์เซอร์เพื่อจำลองหลายผู้ใช้งานจึงเป็นสิ่งที่ขาดไม่ได้
// ส่งข้อความหาคนๆ เดียวผ่าน socket.id
io.on('connection', (socket) => {
console.log('User ID:', socket.id); // พิมพ์ ID ออกมาดูใน Terminal
// สมมติว่าอยากส่งข้อความหาคนนี้คนเดียว
socket.emit('private message', 'สวัสดี นี่คือข้อความส่วนตัวถึงคุณ');
});
ในโค้ดนี้เราดึงค่า socket.id ออกมาเพื่อตรวจสอบว่าใครเชื่อมต่อเข้ามาบ้าง และใช้ socket.emit ภายในบล็อกการเชื่อมต่อเพื่อส่งข้อความไปหาคนนั้นโดยตรง ผลลัพธ์ที่ได้คือในหน้าเว็บของผู้ใช้คนนั้นจะได้รับข้อความต้อนรับส่วนตัวทันทีที่เชื่อมต่อสำเร็จ ในขณะที่คนอื่นที่เชื่อมต่อเข้ามาในเวลาเดียวกันก็จะได้ ID ที่แตกต่างกันไป
ข้อควรระวังและแนวปฏิบัติที่ดีในการใช้ Real-time
สิ่งที่มือใหม่ต้องระวังคือการส่งข้อมูลมากเกินไปจนเซิร์ฟเวอร์รับไม่ไหว (Overload) การเปิดการเชื่อมต่อค้างไว้หลายร้อยหลายพันคนพร้อมกันกินทรัพยากรเครื่องมากกว่า HTTP ปกติมาก ดังนั้นเราต้องวางแผนให้ดีว่าข้อมูลไหนจำเป็นต้องเป็นเรียลไทม์ และข้อมูลไหนแค่ดึงจากฐานข้อมูลปกติก็พอ
อีกเรื่องที่สำคัญคือความปลอดภัย การสื่อสารผ่าน WebSockets ก็สามารถถูกดักฟังหรือปลอมแปลงข้อมูลได้ ถ้าเราทำระบบแชตที่มีการล็อกอิน ต้องตรวจสอบสิทธิ์ทุกครั้งก่อนอนุญาตให้เชื่อมต่อ หรือตรวจสอบก่อนจะส่งข้อมูลสำคัญผ่านซ็อกเก็ต เพื่อป้องกันการโจมตีจากผู้ไม่หวังดี
สุดท้ายคือเรื่อง Scalability (ความสามารถในการรองรับผู้ใช้งานที่เพิ่มขึ้นเรื่อยๆ) เมื่อแอปของคุณเติบโตขึ้น เซิร์ฟเวอร์ตัวเดียวอาจไม่พอ คุณอาจต้องใช้เทคนิคอย่าง Redis (ฐานข้อมูลที่เก็บข้อมูลในหน่วยความจำเพื่อความเร็ว) เข้ามาช่วยจัดการซิงค์ข้อมูลระหว่างเซิร์ฟเวอร์หลายตัว เพื่อให้ระบบแชตยังทำงานได้ต่อเนื่องแม้คนจะเยอะขึ้น
สรุป: เริ่มต้นสร้างโปรเจกต์เรียลไทม์ด้วยตัวเอง
การเรียนรู้ Socket.IO ไม่ได้เป็นเรื่องยากอย่างที่คิด หากเราเข้าใจพื้นฐานว่ามันคือการสร้างช่องทางสื่อสารที่เปิดค้างไว้เพื่อส่งข้อมูลแบบทันทีทันใด การเริ่มต้นฝึกจากโปรเจกต์เล็กๆ เช่น ระบบแชตกลุ่ม หรือระบบแจ้งเตือนสถานะออนไลน์ของเพื่อน จะช่วยให้คุณเห็นภาพชัดเจนที่สุด
คำแนะนำสำหรับคนที่กำลังฝึกคือ ให้ลองสร้างระบบแชตที่สามารถระบุชื่อผู้ส่งได้ หรือลองทำระบบนับจำนวนคนออนไลน์แบบเรียลไทม์ สิ่งเหล่านี้จะทำให้คุณเข้าใจการใช้งาน emit และ on ได้ลึกซึ้งขึ้น และเป็นพื้นฐานที่ดีในการขยับไปทำแอปพลิเคชันที่ซับซ้อนกว่านี้ในอนาคต
จงจำไว้ว่าการเขียนโปรแกรมที่ดีเริ่มจากการลงมือทำจริง อย่าอ่านแค่ทฤษฎี ให้ลองติดตั้ง socket.io ในโปรเจกต์ Node.js ของคุณแล้วลองเล่นดู หากติดบั๊ก (ข้อผิดพลาดในโค้ด) ก็ให้ใช้ console.log ตรวจสอบค่าในแต่ละขั้นตอน นี่คือวิธีที่โปรแกรมเมอร์ทุกคนใช้เพื่อเก่งขึ้นในทุกๆ วัน