拉模式和推模式
拉模式
1、資料更新頻率低,則大多數請求是無效的
2、線上使用者量多,則服務端的查詢負載高
3、定時輪詢拉取,實時性低
推模式
1、僅在資料更新時才需要推送
2、需要維護大量的線上長連線
3、資料更新後可以立即推送
基於webSocket推送
1、瀏覽器支援的socket程式設計,輕鬆維持服務端長連線
2、基於TCP可靠傳輸之上的協議,無需開發者關心通訊細節
3、提供了高度抽象的程式設計介面,業務開發成本較低
webSocket協議與互動
通訊流程
客戶端->upgrade->服務端
客戶端<-switching<-服務端
客戶端->message->服務端
客戶端<-message<-服務端
實現http服務端
1、webSocket是http協議upgrade而來
2、使用http標準庫快速實現空介面:/ws
webSocket握手
1、使用webSocket.Upgrader完成協議握手,得到webSocket長連線
2、操作webSocket api,讀取客戶端訊息,然後原樣傳送回去
封裝webSocket
缺乏工程化設計
1、其他程式碼模組,無法直接操作webSocket連線
2、webSocket連線非執行緒安全,併發讀/寫需要同步手段
隱藏細節,封裝api
1、封裝Connection結構,隱藏webSocket底層連線
2、封裝Connection的api,提供Send/Read/Close等執行緒安全介面
api原理(channel是執行緒安全的)
1、SendMessage將訊息投遞到out channel
2、ReadMessage從in channel讀取訊息
內部原理
1、啟動讀協程,迴圈讀取webSocket,將訊息投遞到in channel
2、啟動寫協程,迴圈讀取out channel,將訊息寫給webSocket
// server.go
package main
import (
"net/http"
"github.com/gorilla/websocket"
"./impl"
"time"
)
var (
upgrader = websocket.Upgrader{
//允許跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)
func wsHandler(w http.ResponseWriter, r *http.Request) {
var (
wsConn *websocket.Conn
err error
conn *impl.Connection
data []byte
)
//Upgrade:websocket
if wsConn, err = upgrader.Upgrade(w, r, nil); err != nil {
return
}
if conn, err = impl.InitConnection(wsConn); err != nil {
goto ERR
}
go func() {
var (
err error
)
for {
if err =conn.WriteMessage([]byte("heartbeat")); err != nil {
return
}
time.Sleep(1 * time.Second)
}
}()
for {
if data, err = conn.ReadMessage(); err != nil {
goto ERR
}
if err = conn.WriteMessage(data); err != nil {
goto ERR
}
}
ERR:
//關閉連線
conn.Close()
}
func main() {
//http:localhost:7777/ws
http.HandleFunc("/ws", wsHandler)
http.ListenAndServe("0.0.0.0:7777", nil)
}
// connection.go
package impl
import (
"github.com/gorilla/websocket"
"sync"
"github.com/influxdata/platform/kit/errors"
)
var once sync.Once
type Connection struct {
wsConn *websocket.Conn
inChan chan []byte
outChan chan []byte
closeChan chan byte
isClosed bool
mutex sync.Mutex
}
func InitConnection(wsConn *websocket.Conn) (conn *Connection, err error) {
conn = &Connection{
wsConn:wsConn,
inChan:make(chan []byte, 1000),
outChan:make(chan []byte, 1000),
closeChan:make(chan byte, 1),
}
//啟動讀協程
go conn.readLoop()
//啟動寫協程
go conn.writeLoop()
return
}
//API
func (conn *Connection) ReadMessage() (data []byte, err error) {
select {
case data = <- conn.inChan:
case <- conn.closeChan:
err = errors.New("connection is closed")
}
return
}
func (conn *Connection) WriteMessage(data []byte) (err error) {
select {
case conn.outChan <- data:
case <- conn.closeChan:
err = errors.New("connection is closed")
}
return
}
func (conn *Connection) Close() {
// 執行緒安全的close,可重入
conn.wsConn.Close()
conn.mutex.Lock()
if !conn.isClosed {
close(conn.closeChan)
conn.isClosed = true
}
conn.mutex.Unlock()
}
//內部實現
func (conn *Connection) readLoop() {
var (
data []byte
err error
)
for {
if _, data, err = conn.wsConn.ReadMessage(); err != nil {
goto ERR
}
//阻塞在這裡,等待inChan有空位置
//但是如果writeLoop連線關閉了,這邊無法得知
//conn.inChan <- data
select {
case conn.inChan <- data:
case <-conn.closeChan:
//closeChan關閉的時候,會進入此分支
goto ERR
}
}
ERR:
conn.Close()
}
func (conn *Connection) writeLoop() {
var (
data []byte
err error
)
for {
select {
case data = <- conn.outChan:
case <- conn.closeChan:
goto ERR
}
if err = conn.wsConn.WriteMessage(websocket.TextMessage, data); err != nil {
goto ERR
}
conn.outChan <- data
}
ERR:
conn.Close()
}