Protocol
ตัวนึง ***** (ขอย้ำว่ามันคือ Protocol)หมายเหตุ
ในเมื่อมันก็ทำ Realtime ได้เหมือนกัน
เป็นเทคนิคนึงของ JavaScript ที่นิยมนำมาใช้ในการดึงข้อมูลจาก Web Server แล้วนำมาอัพเดทหน้าจอ โดยไม่ทำให้หน้าจอเกิดการ Refresh ซึ่งเบื้องหลังของ Ajax นั้นเป็น Http (Hypertext Transfer Protocol) ธรรมดา ๆ
จากภาพ
ข้อจำกัดของ Http (Version 1.1)
คือ Web Server ไม่สามารถ Push ข้อมูล (ส่งข้อมูลกลับ) มายัง Web Browser (Client) ได้เอง โดยไม่ต้องทำการร้องขอ (ยกเว้น Http 2)
หมายเหตุ
ถ้าเราต้องการใช้ Ajax ทำ Realtime Web Application เราจะต้องเขียน JavaScript ให้ไปดึงข้อมูลจาก Web Server มาอัพเดทข้อมูลที่ฝั่งหน้าจอเป็นระยะ ๆ ทุก ๆ x วินาที เราเรียกเทคนิคนี้ว่า การทำ Polling
ซึ่งมีจุดด้อยและข้อที่ต้องพึงระวัง เช่น
เค้าก็เลยได้ทำการ Upgrade กรรมวิธี Polling ให้มันดียิ่งขึ้น เรียกว่า การทำ Long Polling
แต่ขอไม่กล่าวถึงน่ะ!
จากภาพ
หมายเหตุ
การเชื่อมต่อไปยัง WebSocket จะใช้ URI Scheme เป็น ws
และ wss
จะคล้าย ๆ กับ http
และ https
ws
เป็นการเชื่อมต่อแบบ Non-securewss
เป็นการเชื่อมต่อแบบ Secure คือ เป็น WebSocket ที่ทำงานอยู่บน TLS (Transport Layer Security)หมายเหตุ
ตัวอย่างการเชื่อมต่อ
ws://localhost/chat
หรือ
wss://mydomain.com/chat
สามารถมี Query string ต่อท้าย path ได้ เช่น
wss://mydomain.com/chat?groupId=1234
เมื่อทำการ Connect แล้ว
WebSocket จะส่ง Request แรก เป็น Http ไปยัง WebSocket Server
Http ใช้ในการเริ่มต้น หรือ Handshake เพื่อ Upgrade Protocol จาก Http ไปเป็น WebSocket จากนั้นจึงเริ่มกระบวนการสื่อสารด้วย WebSocket อย่างเต็มรูปแบบ จนกว่าจะยกเลิกการเชื่อมต่อ (Close Connection)
จากภาพนี้
ปรับมาเป็นภาพนี้
การ Handshake มี 2 ขั้นตอน คือ
ถ้าการ Handshake ถูกต้อง สมบูรณ์ จึงค่อยเริ่มแลกเปลี่ยนข้อมูลกัน
เป็น Http Request ที่มีหน้าตาประมาณนี้
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
...
...
ข้อกำหนด
GET
แล้วตามด้วย Path ที่เปิด WebSocket ฝั่ง Server ไว้Upgrade: websocket
เป็น Header ที่ใช้บอกว่า ต้องการให้ Upgrade Protocol จาก Http ไปเป็น WebSocketConnection: Upgrade
เป็นการ Upgrade Connection ไปเป็นแบบ keep-alive
คือ จะไม่ทำการยกเลิกหรือปิด Connection จนกว่าจะมีสัญญาณ (Signal) ให้ยกเลิกSec-WebSocket-Key : dGhlIHNhbXBsZSBub25jZQ==
เป็นค่า Random แบบใช้ครั้งเดียว (One-Time random) ขนาด 16 Bytes แล้ว encode เป็น Base64 ฝั่ง Client เป็นคน Generate ขึ้นมา เพื่อเอามาใช้สำหรับเปิด HandshakeSec-WebSocket-Version: 13
เป็น Version ปัจจุบันของ WebSocket (ซึ่งเป็น version 13)หมายเหตุ
เป็น Http Response ที่มีหน้าตาประมาณนี้
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
...
...
โดย Server จะ Response Status เป็น Http 101 Switching Protocols
เพื่อให้ Client ทำการ Upgrade Connection และ Protocol ไปเป็น WebSocket
ถ้าการ Handshake สำเร็จ Web Browser (Client) จะแสดงผลเป็นแบบนี้
สังเกตว่าใน Column Time Status
จะเป็น Pending (รอข้อมูล ไม่ตัดการเชื่อมต่อ)
การส่ง Handshake Request มาที่ Server
Server จะอ่านข้อมูลและทำการ check Headers ต่าง ๆ โดย Headers สำคัญ ๆ ที่จะนำมา check คือ Header ที่ขึ้นต้นด้วย Sec-WebSocket-*
เช่น
Sec-WebSocket-Version
ต้องมีค่าเท่ากับ 13Sec-WebSocket-Key
เพื่อนำไป Build Handshake Response ต่อไปSec-WebSocket-*
อื่น ๆ ถ้ามีแล้วตอบกลับด้วย Sec-WebSocket-Accept
Sec-WebSocket-Accept
ได้มาจาก
var secWebSocketKey = ...;
var concatKey = secWebSocketKey + RFC6455_CONSTANT;
var hashed = sha1.hash(concatKey);
var secWebSocketAccept = base64.encode(hashed);
อธิบาย
Sec-WebSocket-Key
มาต่อกับ RFC6455_CONSTANT
SHA1
algorithmSec-WebSocket-Accept
RFC6455_CONSTANT
เป็นค่าเฉพาะที่ RFC6455 กำหนดขึ้นมา มีค่าเท่ากับ 258EAFA5-E914-47DA-95CA-C5AB0DC85B1
ให้ดูที่ RFC6455 หัวข้อ 1.3. Opening Handshake
หลังจากที่ทำการ Handshake กันเสร็จเรียบร้อยแล้ว ก็จะเริ่มกระบวนการแปลกเปลี่ยนข้อมูลกัน ซึ่งข้อมูลที่ส่งไปมาระหว่างกัน เราจะเรียกมันว่า Frame
ในบางครั้งการส่งข้อมูล 1 ชุด (ที่มีขนาดค่อนข้างใหญ่) อาจจะไม่ได้จบที่ Frame เดียว แต่จะเป็นการแบ่งข้อมูลที่มีอยู่ออกเป็นหลาย ๆ Frame แล้วส่งต่อเนื่องกันไปเรื่อย ๆ แบบนี้
โครงสร้างของ Frame ข้อมูล ถูกกำหนดไว้ใน RFC6455 หัวข้อ 5.2. Base Framing Protocol มีโครงสร้างดังนี้
Frame ข้อมูล เป็นการนำ Byte ข้อมูล (8 bits) มาเรียงต่อ ๆ กัน ซึ่งมีทั้ง
คำอธิบาย
โครงสร้าง Frame ข้อมูล
FIN
: (1 bit) เป็น bit ที่เอาไว้บอกว่า Frame ข้อมูลชุดนี้ เป็น Frame สุดท้ายหรือไม่RSV1, RSV2, RSV3
: (อย่างละ 1 bit) เป็น bit ที่เอาไว้อธิบายเกี่ยวกับ ExtensionsOpcode
(4 bits) เอาไว้ระบุประเภท (Type) ของ Frame ข้อมูล ดังนี้ Mask
: (1 bit) เป็น bit ที่เอาไว้บอกว่า จะให้ทำ Masking หรือ encode Payload Data หรือไม่Payload Length
: (7 bits, 7 + 16 bits, หรือ 7 + 64 bits) เป็นส่วนที่เอาไว้บอกว่า Payload Data มีขนาดทั้งหมดกี่ Bytes ซึ่งถ้า Payload Data มีขนาด Masking-key
: 0 หรือ 4 Bytes ถ้า bit Mask
ถูก set เป็น 1 จะมีการเก็บ Masking key (Random key) เพิ่มอีก 4 Bytes ซึ่งจะเอาไว้สำหรับ encode Payload DataPayload Data
คือ Data จริง ๆ ที่ส่งหากัน ซึ่งอาจจะถูก encode ไว้ด้วย Masking key ถ้า bit Mask
ถูก set เป็น 1หมายเหตุ
Frame ข้อมูลจะถูกแบ่งออกเป็น 2 ประเภทใหญ่ ๆ คือ
ถ้า bit Mask
ถูก set เป็น 1 จะมีการทำ Masking (Encode/Decode) Payload Data เกิดขึ้น โดยใช้การดำเนินการ XOR (ระดับ bit ข้อมูล) ดังนี้
Masking (Pseudocode)
function masking(data, key){
var output = [];
for (var i = 0; i < data.length; i++) {
//XOR
var masked = data[i] ^ key[i % 4];
output.push(masked);
}
return output;
}
การ Encode Payload Data
var frame = ...;
if (isMask) {
var key = randomMaskingKey();
frame.push(key);
frame.push(masking(payloadData, key));
} else {
frame.push(payloadData);
}
การ Decode Payload Data จาก Frame
var frame = ...;
var payloadData;
if (isMask) {
var key = getMaskingKey(frame);
payloadData = masking(frame.remaining(), key);
} else {
payloadData = frame.remaining();
}
หมายเหตุ
การทำ Masking นี้ เราจะเรียกมันว่า XOR Cipher
ซึ่งจะช่วยป้องกันเกี่ยวกับ
สามารถอ่านรายละเอียดเพิ่มเติมได้จาก
การยกเลิกหรือปิดการเชื่อมต่อ จะเกิดขึ้นจากหลาย ๆ กรณี เช่น
ถ้าเป็น Case ที่สามารถ Handle ได้
เวลา Close Connection จะมีการส่ง Close Connection Frame ที่มี Status code ไปบอกอีกฝ่ายด้วย เช่น
สามารถอ่านเพิ่มเติมได้จาก RFC6455 หัวข้อ
นอกจากที่อธิบายมาทั้งหมด ยังมีเรื่องอื่น ๆ ที่ต้องทำความเข้าใจเพิ่มเติมอีก เช่น
แต่ขอไม่พูดถึงน่ะ!
โดยธรรมชาติของ WebSocket เป็น Cross Origin by default
ใน Spec ไม่ได้มีการพูดถึง CORS (Cross-Origin Resource Sharing)
ถ้าจะป้องกันการใช้งาน WebSocket จาก Site อื่น ๆ ใน Spec แนะนำว่าให้ทำการ check Http header Origin
ในตอนที่ทำการ Hanshake ถ้าไม่มีสิทธิ์เข้าถึงก็ให้ตอบ Http 403 Forbidden
กลับไป
อ่านเพิ่มเติมได้ที่ RFC6455 หัวข้อ
ใน Spec ไม่ได้ Focus เรื่อง Client Authentication
แต่ก็มีแนะนำว่า สามารถทำ Authen ได้ตอนทำ Handshake ด้วยวิธีการที่ใช้กับ Http ทั่ว ๆ ไป เช่น การใช้ Cookie เป็นต้น
อ้างอิง RFC6455 หัวข้อ
เพิ่มเติม
บทความเกี่ยวกับ WebSocket Security
จากการเรียนรู้การทำงานของ WebSocket อย่าง (ค่อนข้าง) ละเอียด ผมได้พยายามลองเขียน WebSocket Server ง่าย ๆ เป็นตัวอย่างภาษา Java ไว้ใน GitHub นี้
ถ้าใครสนใจ ก็ลองเข้าไปอ่าน Code ดูได้น่ะ
ใน JavaScript จะมี class WebSocket
ให้เราใช้งาน เพื่อเชื่อมต่อไปยัง WebSocket Server ปลายทาง ดังนี้
const input = ...; // <input/>
const output = ...; // <div/>
const sendButton = ...; // <button/>
const closeButton = ...; // <button/>
//URL WebSocket Server ที่ต้องการเชื่อมต่อ
const socket = new WebSocket("wss://mydomain.com/chat");
//ถ้าเชื่อมต่อไปยัง WebSocket Server สำเร็จ จะมี event open เกิดขึ้น
socket.addEventListener("open", function (event) {
console.log("On open => ", event);
});
//ถ้ามีการปิดการเชื่อมต่อ (Close Connection) จะมี event close เกิดขึ้น
socket.addEventListener("close", function (event) {
console.log("On close => ", event);
});
//ถ้า Error จะมี event error เกิดขึ้น
socket.addEventListener("error", function (event) {
console.log("On error => ", event);
});
//ถ้า WebSocket Server ส่งข้อมูลกลับมา จะมี event message เกิดขึ้น
socket.addEventListener("message", function (event) {
console.log("Received data from server => ", event);
//เอาไปต่อท้ายข้อมูลเดิม
output.innerHTML = output.innerHTML + "<br/>" + event.data;
});
//ถ้าต้องการส่งข้อมูลไปยัง WebSocket Server จะ call socket.send(...)
sendButton.addEventListener("click", function () {
console.log("Send data to server => ", input.value);
socket.send(input.value);
input.value = "";
});
//ถ้าต้องการปิดการเชื่อมต่อ จะ call socket.close(...)
closeButton.addEventListener("click", function () {
console.log("Close connection");
//จริง ๆ สามารถส่ง Status code + Reason เพิ่มไปได้น่ะ
//แต่อันนี้เป็นการ Close Connection แบบปกติ
socket.close();
});
Code เต็ม ๆ สามารถดูได้จาก index.html
ถ้าต้องการเรียนรู้ JavaScript WebSocket แบบละเอียด สามารถอ่านเพิ่มเติมได้จาก
Browser ที่รองรับ
ต้องเป็น Browser version ใหม่ ๆ แต่ในปัจจุบันก็รองรับแทบจะทุก Browser แล้ว
อ้างอิง https://caniuse.com/websockets
Proxy / Middleware ที่รองรับ
แล้วข้อมูลสูญหาย
อาจจะเป็นเพราะ Connection ถูกตัด เพราะ Timeout เนื่องจาก Connect นานเกินไป
ซึ่งอาจเป็นปัญหาที่ตัว Proxy หรือ Middleware ที่ใช้อยู่
การ Reconnect แล้วได้ข้อมูลส่วนที่หายไปกลับมา ต้อง Handle เพิ่มเติมเอง
เนื่องจาก WebSocket เป็น Protocol ที่ทำงานแบบ Single TCP Connection คือ
ถ้า Client เชื่อมต่ออยู่ที่ WebSocket Server เครื่องไหน จะต้องเชื่อมต่ออยู่กับเครื่องนั้นตลอด จนกว่าจะยกเลิกการเชื่อมต่อไป ไม่สามารถเชื่อมต่อไปที่เครื่องใดเครื่องหนึ่ง แล้ววนไปมา (กระจาย Load แบบ Round Robin) ได้เหมือนกับการใช้ Http
ทางแก้ไข คือ
ตรง Load Balancer ให้กำหนดการทำงานสำหรับ WebSocket เป็นแบบ Sticky Session
จากข้อจำกัดด้านบน (Single TCP Connection) ทำให้การ Scale ทำได้ยาก จึงต้องมีการใช้เทคนิคและเทคโนโลยีอื่น ๆ เข้ามาช่วย ซึ่งสามารถอ่านได้ในหัวข้อ การ Scale WebSocket
แนะนำให้ลองอ่านบทความเหล่านี้ดูครับ
มีคนเขียนบทความทดสอบ Performance ของ Http กับ WebSocket ไว้
โดยส่วนตัวคิดว่า เราเอาเรื่องนี้มาอ้างเพื่อใช้งาน WebSocket สำหรับทุก ๆ Case ไม่ได้ เพราะทั้ง Http และ WebSocket ถูกสร้างขึ้นมาเพื่อใช้งานในวัตถุประสงค์ที่แตกต่างกัน
บาง Case ยังไงก็ยังต้องเป็น Http อยู่
ลองอ่านบทความกันดูน่ะ