แสดงบทความที่มีป้ายกำกับ Real time Web แสดงบทความทั้งหมด
แสดงบทความที่มีป้ายกำกับ Real time Web แสดงบทความทั้งหมด

วันจันทร์ที่ 20 เมษายน พ.ศ. 2563

ทำระบบ Realtime บน Web

ทำระบบ 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

  • เป็นวิธีทำระบบ realtime ที่ง่ายที่สุด
  • รันที่ไหนก็ได้ เพราะเป็นการเรียก HTTP ธรรมดา

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

  • เปลือง resource มาก เพราะถึงข้อมูลไม่เปลี่ยน ก็จะมีการส่งข้อมูลตลอดเวลา

ตัวอย่างการทำ 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

  • ประหยัด resource กว่า Short Polling
  • รันที่ไหนก็ได้ เพราะเป็นการเรียก HTTP ธรรมดา

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

  • มีโอกาสโดน timeout บ่อย
  • อาจจะมีถ้าเขียนแบบง่าย ๆ อาจจะมีช่วงที่ไม่ได้ข้อมูลล่าสุด (api ตอบ response กลับไป แล้วมี data change ก่อนที่ client จะยิง api เข้ามาอีกรอบ)

ตัวอย่างการทำ 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

  • ประหยัด resource ไม่มี overhead ของ http, เปิด connection ทิ้งไว้ สามารถส่ง data ไปกลับได้
  • มี library ครบ แทบทุกภาษา

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

  • ทำงานบน Layer 4 (TCP) ถ้ามี middleware ที่ไม่รองรับ HTTP Upgrade ก็จะไม่สามารถใช้ได้ (เช่น Cloud Run)
  • มีโอกาสโดน timeout ถ้า set middleware ไม่ดี (เช่น reverse proxy, load balancer, cdn)

Server-Sent Event (SSE)

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

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

  • รันที่ไหนก็ได้ เพราะทำงานบน HTTP ธรรมดา
  • ประหยัด resource กว่า Short/Long Polling
  • เขียนง่าย ไม่ต้องมี library

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

  • IE ใช้ไม่ได้ 🤨
  • ถ้าเขียนเองต้องมีความรู้เรื่อง HTTP ในระดับนึง

ตัวอย่างการทำ 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
				}
			}
		}
	}))
}

สรุป

  • ใช้ Server-Sent Event 🙈🙈🙈🙈🙈🙈
นอกจาก
  • ไม่อยากแก้ code server
  • คนใช้ไม่เยอะ
  • data ไม่จำเป็นต้อง realtime มาก
ให้ใช้ Short Polling

Note

  • 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 เพิ่มไม่เยอะ
ที่มา https://acoshift.me/2019/0012-realtime-web.html

Laravel

  Laravel Framework คือ PHP Framework ตัวหนึ่งที่ถูกออกแบบมาเพื่อพัฒนาเว็บแอพพลิเคชั่นต่างๆ ในรูปแบบ MVC (Model Views Controller) ซึ่งมีการแ...