ถ้าต้องทำระบบ chat, dashboard, กราฟ ที่ต้องการให้ข้อมูล update เอง จะทำยังไงดี ?
ระบบ Realtime ที่ส่วนใหญ่นิยมใช้ใน web มี
คือการที่เราตั้งเวลาให้ไปดึงข้อมูลทุก ๆ n วินาที
ฝั่ง Server ไม่ต้องทำอะไรเลย (ง่ายไหม 🙈)
func getDataShortPolling(w http.ResponseWriter, r *http.Request) {
data := getData()
w.Write(data)
}
ฝั่ง Browser ทำได้ง่าย ๆ 2 วิธี
setInterval - เขียนง่าย แต่ถ้า server ช้าจะโดนยิง
setInterval(fetchData, 3000) // ดึงข้อมูลทุก 3 วินาที
setTimeout - ยังเขียนง่ายอยู่
async function startPolling () {
await fetchData() // รอให้ดึงข้อมูลเสร็จก่อน ค่อยรออีก 3 วินาที
setTimeout(startPolling, 3000)
}
startPolling()
คล้าย ๆ Short Polling แบบที่ 2 แต่ไม่ต้องมี setTimeout และให้ Server รอจนกว่าข้อมูลเปลี่ยนค่อยตอบ api กลับมา
ฝั่ง Server ต้องรอให้ Data เปลี่ยนก่อน ค่อยตอบกลับไป
func getDataLongPolling(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("long") != "1" {
getDataShortPolling(w, r)
return
}
select {
case <-untilNewDataReceived():
case <-r.Context().Done():
// client close connection
return
}
data := getData()
w.Write(data)
}
ฝั่ง Browser ยังเขียนง่ายอยู่
async function startLongPolling () {
await fetchDataLongPolling()
startLongPolling()
}
fetchDataShortPolling() // ดึง data มาแสดงตอนเปิดเว็บก่อน
startLongPolling() // หลังจากนั้นค่อยใช้ long Polling รอ data ใหม่
ปัญหาของ Long Polling แบบนี้คือ ถ้า data เปลี่ยนระหว่าง request เราจะไม่ได้ data นั้น เช่น
poll poll
Client: ----|-----|-----
Server: -----|--|--|----
1 2 wait
จะเห็นว่าถ้า data เปลี่ยนจาก 1 เป็น 2 ระหว่างที่ไม่ได้ poll อยู่ client จะไม่ได้ data
วิธีแก้คือ อาจจะต้องส่ง token (เช่น timestamp) ของ data ล่าสุดไป แล้วเวลา poll ให้เอา token นั้นส่งกลับไปหา server
Web Socket เป็น full-duplex communication คือ client กับ server สามารถส่งข้อมูลไปกลับได้ผ่าน connection เดียว
คล้าย ๆ Web Socket แต่ server push data หา client ได้ทางเดียว
หน้าตาของ sse event
: บรรทัดที่ขึ้นต้นด้วย : คือ comment
: เราสามารถกำหนดชื่อ event ได้
event: add
data: 1
event: remove
data: 1
: ถ้าไม่กำหนดชื่อ event default คือ event: message
data: hello, sse
: ถ้า data มี 2 บรรทัด
data: hello,
data: sse
Server เขียนไม่ยากมาก แต่ต้องรู้ว่าจะส่งข้อมูลยังไง
func getDataWithSSESupport(w http.ResponseWriter, r *http.Request) {
// ถ้าไม่ส่ง ?sse=1 มา ถือว่ายิงมาแบบ api ธรรมดา
// จะได้ทำ api เดียว รองรับทั้ง api ธรรมดา ทั้ง sse
if r.URL.Query().Get("sse") != "1" {
getDataShortPolling(w, r)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache") // ไม่ให้ cache
w.WriteHeader(http.StatusOK)
for {
data := getData()
fmt.Fprintf(w, "data: %s\n\n", data)
w.(http.Flusher).Flush()
select {
case <-untilNewDataReceived():
case <-r.Context().Done():
return
}
}
}
Browser ยิ่งง่าย
const source = new EventSource('/data')
source.addEventListener('add', (ev) => {
//
})
source.addEventListener('remove', (ev) => {
//
})
source.addEventListener('message', (ev) => {
//
})
// หรือจะรับ ทุก event มาเลย
source.onmessage = (ev) => {
console.log(ev)
}
ลองเอาไปรันเล่น ๆ
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
w.Write([]byte(`<!doctype html>
<div id=app></div>
<script>
const $app = document.getElementById('app')
const source = new EventSource('/data')
source.addEventListener('message', (ev) => {
console.log(ev)
app.innerHTML += ev.data + "<br>"
})
</script>`))
}
if r.URL.Path == "/data" {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
for {
data := time.Now().Format(time.RFC3339)
fmt.Fprintf(w, "data: %s\n\n", data)
w.(http.Flusher).Flush()
select {
case <-time.After(time.Second):
case <-r.Context().Done():
return
}
}
}
}))
}
นอกจาก
ให้ใช้ Short Polling
Web socket ค่อนข้างมีปัญหาเวลา deploy เพราะบางที reverse proxy ไม่ support http upgrade แต่ library บางตัวจะ fallback ไปใช้ short/long Polling มีโอกาสที่ server จะโดน request ยิงรัว ๆ ยิ่งถ้าใช้ service ที่คิดตังตามจำนวน request ก็จะโดนค่าตรงนี้เยอะมาก อาจจะหลายล้าน request ต่อวัน
ถ้าจะ support IE ค่อยใช้ Long Polling ถ้าไม่ support IE ไปใช้ SSE ดีกว่า
หรือจะทำ Short Polling ไปก่อนก็ได้ แล้วคนใช้เยอะค่อยให้ api เดิม support SSE แก้ code เพิ่มไม่เยอะ