blog

ทำระบบ Realtime บน Web

ถ้าต้องทำระบบ chat, dashboard, กราฟ ที่ต้องการให้ข้อมูล update เอง จะทำยังไงดี ?

ระบบ Realtime ที่ส่วนใหญ่นิยมใช้ใน web มี

  1. Short Polling
  2. Long Polling
  3. Websocket
  4. Server-Sent Event (SSE)

Short Polling

คือการที่เราตั้งเวลาให้ไปดึงข้อมูลทุก ๆ n วินาที

ข้อดีของ Short Polling

ข้อเสียของ Short Polling

ตัวอย่างการทำ Short Polling

ฝั่ง Server ไม่ต้องทำอะไรเลย (ง่ายไหม 🙈)

func getDataShortPolling(w http.ResponseWriter, r *http.Request) {
    data := getData()

    w.Write(data)
}

ฝั่ง Browser ทำได้ง่าย ๆ 2 วิธี

  1. setInterval - เขียนง่าย แต่ถ้า server ช้าจะโดนยิง

     setInterval(fetchData, 3000) // ดึงข้อมูลทุก 3 วินาที
    
  2. setTimeout - ยังเขียนง่ายอยู่

     async function startPolling () {
         await fetchData() // รอให้ดึงข้อมูลเสร็จก่อน ค่อยรออีก 3 วินาที
         setTimeout(startPolling, 3000)
     }
    
     startPolling()
    

Long Polling

คล้าย ๆ Short Polling แบบที่ 2 แต่ไม่ต้องมี setTimeout และให้ Server รอจนกว่าข้อมูลเปลี่ยนค่อยตอบ api กลับมา

ข้อดีของ Long Polling

ข้อเสียของ Long Polling

ตัวอย่างการทำ Long Polling

ฝั่ง 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

Web Socket เป็น full-duplex communication คือ client กับ server สามารถส่งข้อมูลไปกลับได้ผ่าน connection เดียว

ข้อดีของ Web Socket

ข้อเสียของ Web Socket

Server-Sent Event (SSE)

คล้าย ๆ Web Socket แต่ server push data หา client ได้ทางเดียว

ข้อดีของ Server-Sent Event

ข้อเสียของ Server-Sent Event

ตัวอย่างการทำ Server-Sent Event

หน้าตาของ 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

Note