
node.js กับ socket.io เพื่อทำ web socket
บทความนี้ ยังคงเป็นอีก 1 ใน series ของ node.js ครับ ติดตามอ่านเนื่อหาเก่าๆ ได้ที่ http://meewebfree.com/cat/nodejs ใครที่ยังไม่แน่ใจว่ารู้เรื่อง node.js ดีมั้ย ใช้งานอย่างไร แนะนำให้อ่านบทความเก่าๆก่อน เพราะคราวนี้ ผมข้ามเรื่องพื้นฐานที่เกี่ยวกับ node.js ไปหมดแล้ว โดยถือว่าทุกท่านรู้แล้ว
คราวนี้ เป็นการเปิด web socket เพื่อให้หน้าเว็บ ที่กำลังใช้งาน รับส่งข้อมูลกัน กับ server แบบ real time เลย ซึ่งเนื้อหาคราวนี้ จะซับซ้อนขึ้น เพราะว่า ต้องมองจากทั้งสองฝั่งนะครับ คือทั้ง server และ client ซึ่ง source code ทั้งหมดของเนื้อหาวันนี้ download ได้ที่นี่
server และ client
ก่อนอื่น ก็ขอนิยาม เพื่อให้เราเข้าใจตรงกันก่อนนะครับ เนื่องจาก web socket มีการ รับ และ ส่งข้อมูลได้ทั้งสองทาง รวมทั้ง source code ที่จะเขียนวันนี้ มันก็ต้องเขียนทั้งฝั่ง server และ client ด้วย ถ้าพูดถึง server ก็จะหมายถึง ฝั่งที่ประมวลผล node.js หรือ ฝั่งที่เป็นเครื่อง server ทำหน้าที่เก็บข้อมูลต่างๆ ใน database ของเว็บ ส่วน client ก็คือ คนที่เป็น user ทั่วไป ที่เปิดหน้าเว็บใช้งาน ด้วย web browser ซึ่งไอ web browser เช่น firefox, google chrome, iPhone iPad(ใช้ safari) นี่แหล่ะครับ เค้าเรียกมันว่าเป็น client ดังนั้น หน้าเว็บที่เป็น client ก็จะเป็น html, javascript, css ธรรมดานี่แหล่ะครับ
web socket เอาไว้ใช้เพื่ออะไร
ก่อนที่จะบอก ประโยชน์ของมัน ขอย้อนกลับมาในยุคปัจจุบันกันก่อน ให้เราลองจินตนาการนะครับ หากว่า ผมมีตัวเลข แสดงอุณหภูมิ แสดงอยู่ที่หน้าเว็บ และต้อง update มันทุกวินาที เพราะว่าค่าจะเปลี่ยนอยู่ตลอดเวลา แบบนี้ เราจะเขียนโค้ดได้อย่างไรครับ
แน่นอน ก็ต้องเขียน javascript function นึงขึ้นมา แล้วตั้ง interval ให้มันไป ajax request ไปที่ server ทุกๆ 1 วินาที เอาผลลัพท์ที่ได้ กลับมาแสดงที่หน้าเว็บ ถูกต้องมั้ยครับ ซึ่งแบบนี้ เค้าจะเรียกว่า "pull" คือฝั่ง client ไปดึงข้อมูลออกมาจาก server นั่นเอง
ทีนี้ การใช้วิธีนี้ มันก่อให้เกิดปัญหาตามมาอย่างมากครับ เพราะว่า การ request แต่ละครั้ง มันจะเกิด ข้อมูล header ขึ้น เป็นจำนวนมาก เช่น ถ้าเราเอา ตัวเลขอุณหภูมิ ซึ่งข้อมูลมันแค่ ตัวหนังสือ 5 bytes แต่เราอาจจะต้องเสีย header ต่อครั้งไปถึง 100 bytes เลยนะครับ ซึ่งเรื่องนี้ ขอให้ไปอ่านเพิ่มเติม เรื่องของ http header request example แล้วจะเข้าใจ นอกเหนือจากเสียปริมาณ bandwidth แล้ว ยังก่อให้เกิด request ที่ไม่จำเป็นจำนวนมากอีกด้วย ซึ่งถ้าคนมา online ที่หน้าเว็บหลักร้อย หรือ พันคน ก็อาจจะทำให้ apache ตอบสนอง request ไม่ทันแล้วล่มไปเลยก็ได้ครับ (ผมเคยเจอ project นึงลูกค้าเล่าให้ฟังว่า คนเก่าเค้าเขียนเอาใว้ ทุกวินาที จะมีการ ajax request 5 ครั้ง เปิดพร้อมกับ 80 คน ล่มไปเลย)
ดังนั้น ทางออกที่สวยหรูกว่า ก็คือการเปิด socket คุยกันซะเลย เหมือนการยกหูโทรหา แล้วค้างสายเอาไว้ ไม่วางนั่นแหล่ะครับ วางก็ต่อเมื่อปิดหน้าเว็บไป ทีนี้ หลังจากที่เราเปิด socket เราก็ได้ประโยชน์เพิ่มมา เพราะว่า เหมือนคนคุยโทรศัพท์กัน ทั้งสองฝ่าย สามารถเป็นฝ่ายเริ่มต้นพูดก่อนได้ ไม่จำเป็นว่าฝ่าย client จะต้องเป็นคนเริ่มต้นพูดก่อน หลังจากที่โทรหากันแล้ว ฝั่ง server อาจจะพูดไปที่ client อย่างเดียวเลยก็ได้ แบบนี้เค้าจะเรียกว่าการ "push"
ซึ่งเมื่อรวมๆกันแล้ว นี่ก็คือ ประโยชน์ของ web socket นั่นเองครับ คือการยกหูโทรหากัน และหลังจากที่เชื่อมต่อกันแล้ว ต่างฝ่ายต่างเริ่มพูดก่อนได้เลย
อย่าลืม event driven
การที่มีฝ่ายหนึ่งฝ่ายใด เริ่มพูดก่อน นั่นล่ะครับ ทำให้เกิด event ขึ้นแล้ว แต่ว่า event ดังกล่าว มันจะต้องเกิดจากการตกลงกันเอาไว้ล่วงหน้าก่อนแล้ว .ในตัวอย่างนี้ ผมจะยกตัวอย่างว่า client (ลูกค้า) โทรไปสั่ง(web socket) pizza โดยมีพนักงาน(server) คอยบริการเรา
เราโทรสั่ง pizza แล้วระบุว่า เอาหน้า ฮาวายเอี้ยน โดยการ "ระบุหน้า pizza" เค้าจะเรียกว่า "event" แล้วหน้า "ฮาวายเอี้ยน" มันก็คือ "data" ที่ส่งจาก client ไปหา server นั่นเอง (แยกกันให้ดีนะครับ)
แต่กลับกัน หากพนักงาน(server) เป็นคนพูดก่อนว่า เราขอนำเสนอ pizza หน้า ฮาวายเอี้ยน ดังนั้น "นำเสนอ" ก็คือ "event" และ ฮาวายเอี้ยน มันก็คือ "data" ซึ่งจะถูกส่งกลับไปกระตุ้นการทำงานที่ฝั่ง client (ทำให้หิว หรือ สั่งเลย หรือ ขอหน้าอื่นเพิ่ม)
สรุป การกระตุ้นการทำงานในการทำของ web socket ก็คือ event ที่ถูกเขียนรอเอาไว้ล่วงหน้านั่นเอง
เริ่มต้นเขียน node.js เปิด socket ด้วย socket.io
แน่นอน ว่าเราต้องติดตั้ง module เพิ่ม เพื่อใช้งาน socket.io ครับ การติดตั้ง ก็อ้างอิงหน้า module เลย นั่นคือ https://npmjs.org/package/socket.io ซึ่งติดตั้งด้วยคำสั่งดังนี้
npm install socket.io
หลังจากที่ติดตั้งแล้ว ก็เริ่มเขียน ฝั่ง node.js ด้วยโค้ดดังนี้
var io = require('socket.io').listen(8080);
io.sockets.on('connection', function(socket) {
io.sockets.emit('menu', {pizza: 'Hawaiian pizza'});
socket.on('order', function(pizza) {
console.log('You want to order menu ', pizza);
});
socket.on('disconnect', function() {
io.sockets.emit('end call');
});
});
อธิบายโค้ด
- บรรทัดแรก ทำหลายอย่างเลย (รวบให้เหลือสั้นในบรรทัดเดียว) ประกอบด้วย การ include module socket.io เข้ามา และ ทำการเปิด port 8080 เอาไว้ แล้วเอา connection ตัวนี้ใส่ในตัวแปรชื่อ io
- บรรทัด 3,13 ทำหน้าที่รอคอย ว่าเมื่อใด จะมี connection เข้ามา ก็จะได้เริ่มทำงานทันที (อันนี้ แค่มี connect เข้ามา เริ่มทำงานทันทีนะครับ แต่ทำอะไร ต้องอ่านข้อต่อไป)
- บรรทัด 4 server สั่ง emit event ที่ชื่อว่า menu โดยข้อมูลที่ส่งไปด้วยก็คือ pizza ที่มีหน้า Hawaiian นั่นเอง (เหมือน พนักงาน พูดทันที วันนี้เรามี pizza หน้า Hawaiian นะคะ)
- บรรทัด 6-7 server รอคอยรับ(event) "order" พร้อมทั้ง ข้อมูลที่จะส่งกลับมา ว่าจะเป็นหน้าอะไร หาก order มาเมื่อไร ให้แสดงข้อความที่หน้าจอทันทีว่า You want to order menu pizza โดยหน้าจอที่ว่า คือ หน้าจอฝั่ง server เพราะ client emit มาให้ server
- บรรทัด 10-12 หากลูกค้าวางสาย ให้แสดงข้อความว่า end call
เราก็สั่งรันเอาไว้ก่อนได้เลย แต่เราจะยังไม่เห็นข้อมูลอะไรที่หน้าจอ เพราะยังไม่มีลูกค้าโทรมาสั่งครับ ระหว่างนี้ เราไปสร้างลูกค้าขึ้นมา ด้วยการเขียนโค้ดหน้าเว็บ(ฝั่ง client) ขึ้นมาดังนี้
อธิบายโค้ด
- บรรทัด 4 คือการ load javascript ของ socket.io เข้ามาที่หน้าเว็บ เพื่อให้หน้าเว็บนี้ สามารถเปิด web socket ได้ โดย path ที่เห็นนั้นเป็น path ของ server นะครับผมใช้ server ip 192.168.1.59 ส่วน port คือ 8080 (ตามที่เราเขียนโค้ดในฝั่ง server ต้องใช้ port ให้ตรงกัน ไม่อย่างนั้นคือการกดโทรผิดเบอร์)
- บรรทัด 6 สั่งให้เปิดการเชื่อมต่อไปที่ ip 192.168.1.59 ด้วย port 8080 อันนี้คือการกดโทรสั่ง pizza แล้วครับ โดยเอาผลการโทร เก็บในตัวแปร socket
- บรรทัด 8 เมื่อโทรติดแล้ว มีคนรับสาย ก็เตรียมทำงานบรรทัดต่อไปเลย
- บรรทัด 9 หาก พนักงาน แจ้งเมนูมา "event menu" ให้เราทำบรรทัดต่อไป อันนี้ มันจะผูก event กับ ฝั่ง server นะครับ ให้ไปดูที่ server emit "menu" มา ก็จะมาทำงานที่ตรงนี้แหล่ะ เหมือนพนักงาน แจ้งว่า วันนี้ มี pizza หน้า อะไรก็ว่าไป
- บรรทัด 10 print ที่ส่วน debug ว่า Today have menu (ด้วยค่าที่ส่งมาจาก server)
- บรรทัด 11 order หน้า pizza กลับไปด้วย pizza หน้า Hawaiian pizza อันนี้ก็ผูกกับฝั่ง server ไว้ก่อนแล้วเช่นกัน คือที่ฝั่ง server ก็ต้องรอ event นี้ครับ สลับกัน
สำหรับการ ดูข้อความใน console.log สำหรับคนที่ใช้ firefox และมี firebug ก็ดูได้ใน tab ที่ชื่อ console ของ firebug นะครับ หน้าตาดังนี้
สำหรับ google chrome กด f12 จะอยู่ใน tab console เช่นกันครับ
สำหรับฝั่ง server จะได้ข้อความแบบนี้ครับ
ลองค่อยๆลำดับดูนะครับ ถ้า server emit ออกมา client ก็ต้องรอรับ event นั้น แต่ถ้า client emit ไป server ก็ต้องรอรับ event นั้นครับ กลับไปกลับมานั่นเอง ตรงนี้ คือจุดที่สับสนที่สุดของ web socket แล้ว ผ่านตรงนี้ไป ไม่มีอะไรอีกแล้วครับ
ลองไล่ดูหลายๆรอบครับหลายคนจะสงสัยว่า ในเมื่อ node.js รันขึ้นมาแค่ instant เดียว แล้ว ถ้ามีสายซ้อนโทรเข้ามา node.js จะทำงาน คำตอบคือ รับได้ สบายมากครับรับได้หลายสายพร้อมกันเลยทีเดียว ขึ้นอยู่กับการตั้งค่าที่ server แล้วครับ
อาจจะมีข้อสงสัยว่า ถ้าฝั่งหนึ่ง emit event ที่อีกฝั่งหนึ่งไม่มีล่ะ เช่นลูกค้าเกิด สั่ง steak ขึ้นมา server จะเป็นยังไง คำตอบคือ ก็ไม่เป็นยังไง ไม่ error ไม่อะไรเพราะ server ไม่รู้จักนั่นเอง
อาจจะสงสัยต่อว่า ถ้าพนักงานคุยกับลูกค้าแล้วสั่ง Hawaiian แล้วกำลังทำ pizza หน้า Hawaiian อยู่ แล้วเกิดลูกค้าอีกคนโทรมาสั่งหน้า Seafood ล่ะ หน้า Hawaiian จะเสร็จมั้ย หรือลูกค้ารายแรกจะกลายเป็น Seafood ไปด้วย ตรงนี้แหล่ะครับ ขึ้นอยู่กับคนที่เขียนโปรแกรมแล้ว ว่า จะเขียนให้มันเป็นแบบไหน เพราะว่ามันก็เป็นไปได้ทั้งสองแบบเลยนั่นแหล่ะ ถ้าอยากให้เป็นคนละหน้า ก็เขียนให้เป็นแบบ Asynchronous ซะ ด้วยการส่ง order ต่อไปที่พ่อครัว พ่อครัวก็ทำไป ถ้ามีรายการใหม่มาก็เข้าคิวเอาไว้ รอพ่อครัวทำเสร็จก็ไปส่งทีละคนเท่านั้นเอง
จริงๆ concept หลักๆของ web socket มันก็จบแต่เพียงเท่านี้ครับ เพราะว่านี่มันคือแก่นแท้ของมันแล้ว ถ้าเข้าใจได้ การประยุกต์ต่อจากนี้ก็คือ ความสามารถด้าน javascript แล้วนั่นเอง ในการจะไป update ปรับปรุงหน้าเว็บ หรือ เขียนให้เกิด action อะไรยังไงกับ user ต่อๆไปครับ
ดังนั้นโปรแกรม จะเป็นไปได้หรือไม่ ขึ้นอยู่กับการออกแบบที่ดี และถูกต้องเลยครับ เพราะว่าถ้าออกแบบผิด ผลลัพท์ ก็ออกมาผิดด้วย การจะลดความเสี่ยงตรงนี้ได้ก็คือ ทำเยอะๆ ลองยกตัวอย่าง case เยอะๆ แล้วลองเขียนเยอะๆ รวมทั้งเวลาเขียนงานจริง พยายามทำตัว demo แล้วรันให้มั่นใจก่อน ค่อยเขียนให้มันรันยาวๆ บางทีผมเขียนเทส รันด้วย data ที่เยอะมากๆๆ หรือ รันนานมากๆ เพื่อดูการทำงานของมันด้วยนะครับ (print ออกมาที่หน้าจอ แล้วก็นั่งดูมันทำงานไปเรื่อยๆ อย่างตอนที่เขียนบทความนี่ก็รันเทสงานชิ้นนึงอยู่) เพื่อให้มั่นใจว่า ผลลัพท์ที่ได้ ถูกต้อง ไม่ผิดพลาดและไม่ตายกลางทาง