中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

淘寶做網(wǎng)站價格百度推廣有哪些售后服務(wù)

淘寶做網(wǎng)站價格,百度推廣有哪些售后服務(wù),衡水醫(yī)院網(wǎng)站建設(shè),做紡織的都用什么網(wǎng)站目錄 為什么需要websocket 使用場景 在線教育 視頻彈幕 Web端即時通信方式 什么是web端即時通訊技術(shù)? 輪詢 長輪詢 長連接 SSE websocket 通信方式總結(jié) Websocket介紹 協(xié)議升級 連接確認 數(shù)據(jù)幀 socket和websocket 常見狀態(tài)碼 gorilla/websocket實…

目錄

為什么需要websocket

使用場景

在線教育

視頻彈幕

Web端即時通信方式

什么是web端即時通訊技術(shù)?

輪詢

長輪詢

長連接 SSE

websocket

通信方式總結(jié)

Websocket介紹

協(xié)議升級

連接確認

數(shù)據(jù)幀

socket和websocket

常見狀態(tài)碼

gorilla/websocket實戰(zhàn)和底層代碼分析

簡單使用

Upgrader

Conn

服務(wù)端示例

客戶端示例

源碼走讀

Upgrade 協(xié)議升級

ReadMessage 讀消息

WriteMessage 寫消息

advanceFrame 解析數(shù)據(jù)幀

heartbeat 心跳

總結(jié)


為什么需要websocket

????????初次接觸 websocket 的人,可能都會有這樣的疑問:我們已經(jīng)有了 http 協(xié)議,為什么還需要websocket協(xié)議?它帶來了什么好處?

????????原因是http每次請求只能由客戶發(fā)起,而websocket最大特點就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信

使用場景

在線教育

????????老師進行一對多的在線授課,在客戶端內(nèi)編寫的筆記、大綱等信息,需要實時推送至多個學生的客戶端,需要通過WebSocket協(xié)議來完成。

視頻彈幕

????????終端用戶A在自己的手機端發(fā)送了一條彈幕信息,但是您也需要在客戶A的手機端上將其他N個客戶端發(fā)送的彈幕信息一并展示。需要通過WebSocket協(xié)議將其他客戶端發(fā)送的彈幕信息從服務(wù)端全部推送至客戶A的手機端,從而使客戶A可以同時看到自己發(fā)送的彈幕和其他用戶發(fā)送的彈幕。

????????當然還有體育實況更新、視頻會議和聊天等等,這里都不一一列舉了


Web端即時通信方式

什么是web端即時通訊技術(shù)?

????????可以理解為實現(xiàn)這樣一種功能:服務(wù)器端可以即時地將數(shù)據(jù)的更新或變化反應(yīng)到客戶端,例如消息推送等功能都是通過這種技術(shù)實現(xiàn)的。

????????但是在Web中,由于瀏覽器的限制,實現(xiàn)即時通訊需要借助一些方法。這種限制出現(xiàn)的主要原因是,一般的Web通信都是瀏覽器先發(fā)送請求到服務(wù)器,服務(wù)器再進行響應(yīng)完成數(shù)據(jù)的現(xiàn)實更新。

Web端實現(xiàn)即時通訊主要有四種方式:

????????輪詢、長輪詢(comet)、長連接(SSE)、WebSocket。

????????它們大體可以分為兩類,一種是在HTTP基礎(chǔ)上實現(xiàn)的,包括短輪詢、長輪詢(comet)、長連接(SSE);另一種不是在HTTP基礎(chǔ)上實現(xiàn)是,即WebSocket。下面分別介紹一下這四種輪詢方式。

輪詢

????????基本思路就是客戶端每隔一段時間向服務(wù)器發(fā)送http請求,服務(wù)器端在收到請求后,不管是否有所需數(shù)據(jù)返回,都直接進行響應(yīng)。

????????這種方式本質(zhì)上還是客戶端不斷發(fā)送請求,才形成客戶端能實時接收服務(wù)端數(shù)數(shù)據(jù)變化的假象。

????????實現(xiàn)比較簡單,缺點是需要不斷建立http連接,浪費資源,而且在客戶端數(shù)量級很大的情況下會導致服務(wù)器壓力陡增,顯然不是好選擇!

長輪詢

????????長輪詢方式是服務(wù)器收到客戶端發(fā)來的請求后,想掛起請求,服務(wù)器端不會直接進行響應(yīng),在超時時間內(nèi)(比如20S),接收請求和處理請求進行響應(yīng)。

????????有兩種情況長輪詢會響應(yīng):

  • 達到http請求超時時間
  • 服務(wù)器正常處理請求返回響應(yīng)結(jié)果

????????長輪詢和短輪詢比起來,明顯減少了很多不必要的http請求次數(shù),但是連接掛起也會導致資源的浪費!

長連接 SSE

????????長連接是指在一個連接上可以連續(xù)發(fā)送多個數(shù)據(jù)包,在連接保持期間,如果沒有數(shù)據(jù)包發(fā)送,需要雙方發(fā)鏈路檢測包。

????????SSE是HTML5新增的功能,全稱為Server-Sent Events,它可以允許服務(wù)器推送數(shù)據(jù)到客戶端。

????????SSE在本質(zhì)上就與之前的長輪詢、輪詢不同,雖然都是基于http協(xié)議的,但是輪詢需要客戶端先發(fā)送請求,服務(wù)端才能響應(yīng)。而SSE最大的特點就是不需要持續(xù)客戶端發(fā)送請求,可以實現(xiàn)只要服務(wù)器端數(shù)據(jù)有更新,就可以馬上發(fā)送到客戶端。

????????長鏈接流程:連接->傳輸數(shù)據(jù)->保持連接 -> 傳輸數(shù)據(jù)-> ....->直到一方關(guān)閉連接,客戶端關(guān)閉連接

????????SSE的優(yōu)勢在于,它不需要建立或保持大量的客戶端發(fā)往服務(wù)器端的請求,節(jié)約了很多資源,提升應(yīng)用性能,但是可以關(guān)閉一些長時間不讀寫操作的連接,這樣可以避免一些惡意連接導致server端壓力。

websocket

????????WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議,它實現(xiàn)了客戶端與服務(wù)器全雙工(full-duplex)通信(同一時間里,雙方都可以主動向?qū)Ψ桨l(fā)送數(shù)據(jù))。

????????在WebSocket中,客戶端和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸。

通信方式總結(jié)

??????????兼容性角度:短輪詢>長輪詢>長連接SSE>WebSocket

??????????性能方面:WebSocket>長連接SSE>長輪詢>短輪詢


Websocket介紹

????????我們已經(jīng)知道了WebSocket 是一種網(wǎng)絡(luò)傳輸協(xié)議,可在單個 TCP 連接上進行全雙工通信,位于 OSI 模型的應(yīng)用層。

????????而通過WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù),只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接。

協(xié)議升級

????????出于兼容性的考慮,websocket 的握手使用 HTTP 來實現(xiàn),客戶端的握手消息就是一個「普通的,帶有 Upgrade 頭的,HTTP Request 消息」。

📢 想建立websoket連接,就需要在http請求上帶一些特殊的header頭才行!

????????我們看下WebSocket協(xié)議客戶端請求和服務(wù)端響應(yīng)示例,關(guān)于http這里就不多介紹了(這里自行回想下Http請求的request和reposone部分)

????????header頭的意思是,瀏覽器想升級http協(xié)議,并且想升級成websocket協(xié)議

客戶端請求:

//以下是WebSocket請求頭中的一些字段:Upgrade: websocket   // 1
Connection: Upgrade  // 2
Sec-WebSocket-Key: xx==  // 3
Origin: http:			// 4
Sec-WebSocket-Protocol: chat, superchat  // 5
Sec-WebSocket-Version: 13  // 6

上述字段說明如下:

  1. Upgrade:字段必須設(shè)置 websocket,表示希望升級到 WebSocket 協(xié)議
  2. Connection:須設(shè)置 Upgrade,表示客戶端希望連接升級
  3. Sec-WebSocket-Key:是隨機的字符串,服務(wù)器端會用這些數(shù)據(jù)來構(gòu)造出一個 SHA-1 的信息摘要
  4. Origin:字段是可選的,只包含了協(xié)議和主機名稱
  5. Sec-WebSocket-Extensions:用于協(xié)商本次連接要使用的 WebSocket 擴展
  6. Sec-WebSocket-Version:表示支持的 WebSocket 版本,RFC6455 要求使用的版本是 13

服務(wù)端響應(yīng)

HTTP/1.1 101 Web Socket Protocol Handshake  // 1
Connection: Upgrade  // 2
Upgrade: websocket  // 3
Sec-WebSocket-Accept: 2mQFj9iUA/Nz8E6OA4c2/MboVUk=  //4

上述字段說明如下:

  1. 101 響應(yīng)碼確認升級到 WebSocket 協(xié)議
  2. Connection:值為 “Upgrade” 來指示這是一個升級請求
  3. Upgrade:表示升級為 WebSocket 協(xié)議
  4. Sec-WebSocket-Accept:簽名的鍵值驗證協(xié)議支持

????????🚩 1:ws 協(xié)議默認使用 80 端口,wss 協(xié)議默認使用 443 端口,和 http 一樣

????????🚩 2:WebSocket 沒有使用 TCP 的“IP 地址 + 端口號”,開頭的協(xié)議名不是“http”,引入的是兩個新的名字:“ws”和“wss”,分別表示明文和加密的 WebSocket 協(xié)議

連接確認

????????發(fā)建立連接是前提,但是只有當請求頭參數(shù)Sec-WebSocket-Key字段的值經(jīng)過固定算法加密后的數(shù)據(jù)和響應(yīng)頭里的Sec-WebSocket-Accept的值保持一致,該連接才會被認可建立。

????????如下圖從瀏覽器截圖的兩個關(guān)鍵參數(shù):

????????服務(wù)端返回的響應(yīng)頭字段 Sec-WebSocket-Accept 是根據(jù)客戶端請求 Header 中的Sec-WebSocket-Key計算出來。那么時如何進行參數(shù)加密驗證和比對確認的呢,如下圖。

具體流程如下:

  • 客戶端握手中的 Sec-WebSocket-Key 頭字段的值是16字節(jié)隨機數(shù),并經(jīng)過base64編碼
  • 服務(wù)端需將該值和固定的 GUID 字符串( 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)拼接后使用 SHA-1 進行哈希,并采用 base64 編碼后
  • 服務(wù)端將編碼后的值作為響應(yīng)作為的Sec-WebSocket-Accept 值返回。
  • 客戶端也必須按照服務(wù)端生成 Sec-WebSocket-Accept 的方式一樣生成字符串,與服務(wù)端回傳的進行對比
  • 相同就是協(xié)議升級成功,不同就是失敗

????????在協(xié)議升級完成后websokcet就建立完成了,接下來就是客戶端和服務(wù)端使用websocket進行數(shù)據(jù)傳輸通信了!

數(shù)據(jù)幀

????????一旦升級成功 WebSocket 連接建立后,后續(xù)數(shù)據(jù)都以幀序列的形式傳輸

📄協(xié)議規(guī)定了數(shù)據(jù)幀的格式,服務(wù)端要想給客戶端推送數(shù)據(jù),必須將要推送的數(shù)據(jù)組裝成一個數(shù)據(jù)幀,這樣客戶端才能接收到正確的數(shù)據(jù);同樣,服務(wù)端接收到客戶端發(fā)送的數(shù)據(jù)時,必須按照幀的格式來解包,才能真確獲取客戶端發(fā)來的數(shù)據(jù)

????????我們來看下對幀的格式定義吧!

看看數(shù)據(jù)幀字段代表的含義吧:

  1. FIN 1個bit位,用來標記當前數(shù)據(jù)幀是不是最后一個數(shù)據(jù)幀
  2. RSV1, RSV2, RSV3 這三個,各占用一個bit位用做擴展用途,沒有這個需求的話設(shè)置位0
  3. Opcode 的值定義的是數(shù)據(jù)幀的數(shù)據(jù)類型。值為1 表示當前數(shù)據(jù)幀內(nèi)容是文本;值為2 表示當前數(shù)據(jù)幀內(nèi)容是二進制;值為8表示請求關(guān)閉連接
  4. MASK 表示數(shù)據(jù)有沒有使用掩碼

服務(wù)端發(fā)送給客戶端的數(shù)據(jù)幀不能使用掩碼,客戶端發(fā)送給服務(wù)端的數(shù)據(jù)幀必須使用掩碼

  1. Payload len 數(shù)據(jù)的長度,Payload data的長度,占7bits,7+16bits,7+64bits
  2. Masking-key 數(shù)據(jù)掩碼 (設(shè)置位0,則該部分可以省略,如果設(shè)置位1,則用來解碼客戶端發(fā)送給服務(wù)端的數(shù)據(jù)幀)
  3. Payload data 幀真正要發(fā)送的數(shù)據(jù),可以是任意長度

????????上面我們說到Payload len三種長度(最開始的7bit的值)來標記數(shù)據(jù)長度,這里具體看下是哪三種:

🚩 情況1:值設(shè)置在0-125

????????那么這個有效載荷長度(Payload len)就是對應(yīng)的數(shù)據(jù)的值

🚩 情況2:值設(shè)置為126

????????如果設(shè)置為 126,可表示payload的長度范圍在 126~65535 之間,那么接下來的 2 個字節(jié)(擴展用16bit Payload長度)會包含Payload真實數(shù)據(jù)長度

🚩 情況3:值設(shè)置為127

????????可表示payload的長度范圍在 >=65535 ,那么接下來的 8 個字節(jié)(擴展用16bit + 32bit + 16bit Payload長度)會包含Payload真實數(shù)據(jù)長度,這種情況能表示的數(shù)據(jù)就很大了,完全夠用

socket和websocket

????????這兩者名字上差距不大,雖然都有帶個socket,但是完全是兩個不同的東西, 大家千萬別被名字給帶的傻傻分不清楚了!

????????我們來看下之間的區(qū)別

????????socket:是在應(yīng)用層和傳輸層之間的一個中間軟件抽象層,是一組接口,它把TCP/IP層復雜的操作抽象為幾個簡單的接口供應(yīng)用層調(diào)用以實現(xiàn)進程在網(wǎng)絡(luò)中通信。

????????websocket:是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議,和http協(xié)議一樣屬于應(yīng)用層協(xié)議。

????????下圖中分別表示了socket和websocket在網(wǎng)絡(luò)中的位置

常見狀態(tài)碼

????????下面顯示了從服務(wù)器到客戶端的通信的 WebSocket 狀態(tài)碼和錯誤提示,WebSocket 狀態(tài)碼遵循 RFC 正常關(guān)閉連接標準

  • 1000 CLOSE_NORMAL 連接正常關(guān)閉
  • 1001 CLOSE_GOING_AWAY 終端離開 例如:服務(wù)器錯誤,或者瀏覽器已經(jīng)離開此頁面
  • 1002 CLOSE_PROTOCOL_ERROR 因為協(xié)議錯誤而中斷連接
  • 1003 CLOSE_UNSUPPORTED 端點因為受到不能接受的數(shù)據(jù)類型而中斷連接
  • 1004 保留
  • 1005 CLOSE_NO_STATUS 保留, 用于提示應(yīng)用未收到連接關(guān)閉的狀態(tài)碼
  • 1006 CLOSE_ABNORMAL 期望收到狀態(tài)碼時連接非正常關(guān)閉 (也就是說, 沒有發(fā)送關(guān)閉幀)
  • 1007 Unsupported Data 收到的數(shù)據(jù)幀類型不一致而導致連接關(guān)閉
  • 1008 Policy Violation 收到不符合約定的數(shù)據(jù)而斷開連接
  • 1009 CLOSE_TOO_LARGE 收到的消息數(shù)據(jù)太大而關(guān)閉連接
  • 1010 Missing Extension 客戶端因為服務(wù)器未協(xié)商擴展而關(guān)閉
  • 1011 Internal Error 服務(wù)器因為遭遇異常而關(guān)閉連接
  • 1012 Service Restart 服務(wù)器由于重啟而斷開連接
  • 1013 Try Again Later 服務(wù)器由于臨時原因斷開連接, 如服務(wù)器過載因此斷開一部分客戶端連接
  • 1015 TLS握手失敗關(guān)閉連接

gorilla/websocket實戰(zhàn)和底層代碼分析

????????相信很多使用Golang的小伙伴都知道Gorilla這個工具包,長久以來gorilla/websocket 都是比官方包更好的websocket包。

????????gorilla/websocket 框架開源地址為: https://github.com/gorilla/websocket

簡單使用

????????安裝Gorilla Websocket Go軟件包,只需要使用即可go get

go get github.com/gorilla/websocket

????????在正式使用之前我們先簡單了解下兩個數(shù)據(jù)結(jié)構(gòu) Upgrader 和 Conn

Upgrader

????????Upgrader指定用于將 HTTP 連接升級到 WebSocket 連接

type Upgrader struct {HandshakeTimeout time.DurationReadBufferSize, WriteBufferSize intWriteBufferPool BufferPoolSubprotocols []stringError func(w http.ResponseWriter, r *http.Request, status int, reason error)CheckOrigin func(r *http.Request) boolEnableCompression bool
}
  • HandshakeTimeout: 握手完成的持續(xù)時間
  • ReadBufferSize和WriteBufferSize:以字節(jié)為單位指定I/O緩沖區(qū)大小。如果緩沖區(qū)大小為零,則使用HTTP服務(wù)器分配的緩沖區(qū)
  • CheckOrigin : 函數(shù)應(yīng)仔細驗證請求來源 防止跨站點請求偽造

這里一般會設(shè)置下CheckOrigin來解決跨域問題

Conn

????????Conn類型表示W(wǎng)ebSocket連接,這個結(jié)構(gòu)體的組成包括兩部分,寫入字段(Write fields)和 讀取字段(Read fields)

type Conn struct {conn        net.ConnisServer    bool...// Write fieldswriteBuf      []byte        writePool     BufferPoolwriteBufSize  intwriter        io.WriteCloser isWriting     bool           ...// Read fieldsreadRemaining int64readFinal     bool  readLength    int64 messageReader *messageReader ...
}
  • isServer : 字段來區(qū)分我們是否用Conn作為客戶端還是服務(wù)端,也就是說說gorilla/websocket中同時編寫客戶端程序和服務(wù)器程序,但是一般是Web應(yīng)用程序使用單獨的前端作為客戶端程序。

????????部分字段說明如下圖:


服務(wù)端示例

????????出于說明的目的,我們將在Go中同時編寫客戶端程序和服務(wù)端程序(其實因為本人不會前端)。

????????當然我們在開發(fā)程序的時候基本都是單獨的前端,通常使用(Javascript,vue等)實現(xiàn)websocket客戶端,這里為了讓大家有比較直觀的感受,用【gorilla/websocket】分別寫了服務(wù)端和客戶端示例。

var upGrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return true},
}func main() {http.HandleFunc("/ws", wsUpGrader)err := http.ListenAndServe("localhost:8080", nil)if err != nil {log.Println("server start err", err)}
}func wsUpGrader(w http.ResponseWriter, r *http.Request) {//轉(zhuǎn)換為升級為websocketconn, err := upGrader.Upgrade(w, r, nil)if err != nil {log.Println(err)return}//釋放連接defer conn.Close()for {//接收消息messageType, message, err := conn.ReadMessage()if err != nil {log.Println(err)return}log.Println("server receive messageType", messageType, "message", string(message))//發(fā)送消息err = conn.WriteMessage(messageType, []byte("pong"))if err != nil {log.Println(err)return}}
}

????????我們知道websocket協(xié)議是基于http協(xié)議進行upgrade升級的, 這里使用 net/http提供原始的http連接。

????????http.HandleFunc接受兩個參數(shù):第一個參數(shù)是字符串表示的 url 路徑,第二個參數(shù)是該 url 實際的處理對象

????????http.ListenAndServe 監(jiān)聽在某個端口,啟動服務(wù),準備接受客戶端的請求

HandleFunc的作用:通過類型轉(zhuǎn)換讓我們可以將普通的函數(shù)作為HTTP處理器使用

服務(wù)端代碼流程:

  • Gorilla在使用websocket之前是先將http裝為websocket,用的是初始化的upGrader結(jié)構(gòu)體變量調(diào)用Upgrade方法進行請求協(xié)議升級
  • 升級后返回 *Conn(此時isServer = true),后續(xù)使用它來處理websocket連接
  • 服務(wù)端消息讀寫分別用 ReadMessage()、WriteMessage()

客戶端示例

import ("fmt""github.com/gorilla/websocket""log""time"
)func main() {//服務(wù)器地址 websocket 統(tǒng)一使用 ws://url := "ws://localhost:8080/ws" //使用默認撥號器,向服務(wù)器發(fā)送連接請求ws, _, err := websocket.DefaultDialer.Dial(url, nil)if err != nil {log.Fatal(err)}//關(guān)閉連接defer ws.Close()//發(fā)送消息go func() {for {err := ws.WriteMessage(websocket.BinaryMessage, []byte("ping"))if err != nil {log.Fatal(err)}//休眠兩秒time.Sleep(time.Second * 2)}}()//接收消息for {_, data, err := ws.ReadMessage()if err != nil {log.Fatal(err)}fmt.Println("client receive message: ", string(data))}
}

????????客戶端的實現(xiàn)看起來也是簡單,先使用默認撥號器,向服務(wù)器地址發(fā)送連接請求,撥號成功時也返回一個*Conn,開啟一個協(xié)程每隔兩秒向服務(wù)端發(fā)送消息,同樣都是使用ReadMessage和W riteMessage讀寫消息。

????????示例代碼運行結(jié)果如下:


源碼走讀

????????看完上面基本的客戶端和服務(wù)端案例之后,我們對整個消息發(fā)送和接收的使用已經(jīng)熟悉了,實際開發(fā)中要做的就是如何結(jié)合業(yè)務(wù)去定義消息類型和發(fā)送場景了,我們接著走讀下底層的實現(xiàn)邏輯!

Upgrade 協(xié)議升級

????????Upgrade顧名思義【升級】,在進行協(xié)議升級之前是需要對協(xié)議進行校驗的,之前我們知道待升級的http請求是有固定請求頭的,這里列舉幾個:

?? Upgrade進行校驗的目的是看該請求是否符合協(xié)議升級的規(guī)定

Upgrade的部分校驗代碼如下,return處進行了省略

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {if !tokenListContainsValue(r.Header, "Connection", "upgrade") {return ...}if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {return ...}//必須是get請求方法if r.Method != http.MethodGet {return ...}if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {return ...}if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {return ...}...c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)...
}

????????tokenListContainsValue的目的是校驗請求的Header中是否有upgrade需要的特定參數(shù),比如我們上圖列舉的一些。

????????newConn就是初始化部分Conn結(jié)構(gòu)體的,方法中的第二個參數(shù)為true代表這是服務(wù)端

computeAcceptKey 計算接受密鑰:

????????這個函數(shù)重點說下,在上一期中在websocket【連接確認】這一章節(jié)中知道,websocket協(xié)議升級時,需要滿足如下條件:

??只有當請求頭參數(shù)Sec-WebSocket-Key字段的值經(jīng)過固定算法加密后的數(shù)據(jù)和響應(yīng)頭里的Sec-WebSocket-Accept的值保持一致,該連接才會被認可建立。

var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")func computeAcceptKey(challengeKey string) string {h := sha1.New() h.Write([]byte(challengeKey))h.Write(keyGUID)return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

????????上面 computeAcceptKey 函數(shù)的實現(xiàn),驗證了之前說的關(guān)于 Sec-WebSocket-Accept的生成

????????服務(wù)端需將Sec-WebSocket-Key和固定的 GUID 字符串( 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)拼接后使用 SHA-1 進行哈希,并采用 base64 編碼后返回


ReadMessage 讀消息

????????ReadMessage方法內(nèi)部使用NextReader獲取讀取器并從該讀取器讀取到緩沖區(qū),如果是一條消息由多個數(shù)據(jù)幀,則會拼接成完整的消息,返回給業(yè)務(wù)層。

func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {var r io.ReadermessageType, r, err = c.NextReader()if err != nil {return messageType, nil, err}//ReadAll從r讀取,直到出現(xiàn)錯誤或EOF,并返回讀取的數(shù)據(jù)p, err = io.ReadAll(r)return messageType, p, err
}

????????該方法,返回三個參數(shù),分別是消息類型、內(nèi)容、error

messageType是int型,值可能是 BinaryMessage(二進制消息) TextMessage(文本消息)

NextReader:該方法得到一個消息類型 messageType,io.Reader,err

func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {...for c.readErr == nil {//解析數(shù)據(jù)幀方法advanceFrame// frameType : 幀類型frameType, err := c.advanceFrame()if err != nil {c.readErr = hideTempErr(err)break}//數(shù)據(jù)類型是 文本或二進制類型if frameType == TextMessage || frameType == BinaryMessage {c.messageReader = &messageReader{c}c.reader = c.messageReaderif c.readDecompress {c.reader = c.newDecompressionReader(c.reader)}return frameType, c.reader, nil}}...
}

????????c.advanceFrame() 是核心代碼,主要是實現(xiàn)解析這條消息,這里在最后章節(jié)會講。

????????這里有個 c.messageReader (當前的低級讀取器),賦值給c.reader,為什么要這樣呢?

????????c.messageReader 是更低級讀取器,而 c.reader 的作用是當前讀取器返回到應(yīng)用程序。簡單就是messageReader 是實現(xiàn)了 c.reader 接口的結(jié)構(gòu)體, 從而也實現(xiàn)了 io.Reader接口

????????圖上加一個 bufio.Read方法:Read讀取數(shù)據(jù)寫入p。本方法返回寫入p的字節(jié)數(shù)。本方法一次調(diào)用最多會調(diào)用下層Reader接口一次Read方法,因此返回值n可能小于len(p)。讀取到達結(jié)尾時,返回值n將為0而err將為io.EOF

messageReader的 Read方法: 我們看下Read的具體實現(xiàn),Read方法主要是讀取數(shù)據(jù)幀內(nèi)容,直到出現(xiàn)并返回io.EOF或者其他錯誤為止,而實際調(diào)用它的正是 io.ReadAll。

func (r *messageReader) Read(b []byte) (int, error) {...for c.readErr == nil {//當前幀中剩余的字節(jié)if c.readRemaining > 0 {if int64(len(b)) > c.readRemaining {b = b[:c.readRemaining]}//讀取到切片b中n, err := c.br.Read(b)c.readErr = hideTempErr(err)//當Conn是服務(wù)端if c.isServer {c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])}//readRemaining字節(jié)數(shù)轉(zhuǎn)int64rem := c.readRemainingrem -= int64(n)//跟蹤連接上剩余的字節(jié)數(shù)if err := c.setReadRemaining(rem); err != nil {return 0, err}if c.readRemaining > 0 && c.readErr == io.EOF {c.readErr = errUnexpectedEOF}//返回讀后字節(jié)數(shù)return n, c.readErr}//標記是否最后一個數(shù)據(jù)幀if c.readFinal {// messageRader 置為nilc.messageReader = nilreturn 0, io.EOF}//獲取數(shù)據(jù)幀類型frameType, err := c.advanceFrame()switch {case err != nil:c.readErr = hideTempErr(err)case frameType == TextMessage || frameType == BinaryMessage:c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")}}err := c.readErrif err == io.EOF && c.messageReader == r {err = errUnexpectedEOF}return 0, err
}

io.ReadAll :ReadAll從r讀取,這里是實現(xiàn)如果一條消息由多個數(shù)據(jù)幀,會一直讀直到最后一幀的關(guān)鍵。

func ReadAll(r Reader) ([]byte, error) {b := make([]byte, 0, 512)for {if len(b) == cap(b) {// 給[]byte添加更多容量b = append(b, 0)[:len(b)]}n, err := r.Read(b[len(b):cap(b)])b = b[:len(b)+n]if err != nil {if err == EOF {err = nil}return b, err}}
}

????????可以看出在for 循環(huán)中一直讀取,直至讀取到最后一幀,直到返回io.EOF或網(wǎng)絡(luò)原因錯誤為止,否則一直進行阻塞讀,這些 error 可以從上面講到的messageReader的 Read方法可以看出來。

????????總結(jié)下,整個流程如下:


WriteMessage 寫消息

????????既然讀消息是對數(shù)據(jù)幀進行解析,那么寫消息就自然會聯(lián)想到將數(shù)據(jù)按照數(shù)據(jù)幀的規(guī)范組裝寫入到一個writebuf中,然后寫入到網(wǎng)絡(luò)中。

????????我們繼續(xù)看WriteMessage是如何實現(xiàn)的

func (c *Conn) WriteMessage(messageType int, data []byte) error {...//w 是一個io.WriteCloserw, err := c.NextWriter(messageType)if err != nil {return err}//將data寫入writeBuf中if _, err = w.Write(data); err != nil {return err}return w.Close()
}

????????WriteMessage方法接收一個消息類型和數(shù)據(jù),主要邏輯是先調(diào)用Conn的NextWriter方法得到一個io.WriteCloser,然后寫消息到這個Conn的writeBuf,寫完消息后close它。

NextWriter實現(xiàn)如下:

func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {var mw messageWriterif err := c.beginMessage(&mw, messageType); err != nil {return nil, err}c.writer = &mw...return c.writer, nil
}

????????注意看這里有個messageWriter賦值給了Conn的writer,也就是說messageWriter實現(xiàn)了io.WriterCloser接口。

????????這里的實現(xiàn)跟讀消息中的NextReader方法中的messageReader很像,也是通過實現(xiàn)io.Reader接口,然后賦值給了Conn的Reader,這里可以做個小聯(lián)動,找到讀寫消息實際的實現(xiàn)者 messageReader、messageWriter。

messageWriter的Write實現(xiàn):

????????前置知識:如果沒有設(shè)置Conn中writeBufferSize, 默認情況下會設(shè)置為 4096個字節(jié),另外加上14字節(jié)的數(shù)據(jù)幀頭部大小【這些在newConn中初始化的時候有代碼說明】

func (w *messageWriter) Write(p []byte) (int, error) {...//如果字節(jié)長度大于初始化的writeBuf空間大小if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {//寫入方法err := w.flushFrame(false, p)...}//字節(jié)長度不大于初始化的writeBuf空間大小nn := len(p)for len(p) > 0 {//內(nèi)部也是調(diào)用的flushFramen, err := w.ncopy(len(p))...}return nn, nil
}

????????messageWriter中的Write方法主要的目的是將數(shù)據(jù)寫入到writeBuf中,它主要存儲結(jié)構(gòu)化的數(shù)據(jù)幀內(nèi)容,所謂結(jié)構(gòu)化就是按照數(shù)據(jù)幀的格式,用Go實現(xiàn)寫入的。

????????總結(jié)下,整個流程如下:

????????而flushFrame方法將緩沖數(shù)據(jù)和額外數(shù)據(jù)作為幀寫入網(wǎng)絡(luò),這個final參數(shù)表示這是消息中的最后一幀。

????????至于flushFrame內(nèi)部是如何實現(xiàn)寫入網(wǎng)絡(luò)中的,你可以看看 net.Conn 是怎么Write的,因為最終就是調(diào)這個寫入網(wǎng)絡(luò)的,這里就不再深究了,有興趣的可以自己挖一挖!


advanceFrame 解析數(shù)據(jù)幀

????????解析數(shù)據(jù)幀放在最后,前面的代碼走讀主要是為了方便能把整體流程搞清楚,而數(shù)據(jù)幀的解析,是更加需要對websocket基礎(chǔ)有了解,特別是數(shù)據(jù)幀的組成,因為解析就是按照協(xié)定用Go代碼實現(xiàn)的一種方式而已。

根據(jù)上圖回顧下數(shù)據(jù)幀各部分代表的意思:

FIN :1個bit位,用來標記當前數(shù)據(jù)幀是不是最后一個數(shù)據(jù)幀

RSV1, RSV2, RSV3 :這三個各占用一個bit位用做擴展用途,沒有這個需求的話設(shè)置位0 Opcode :該值定義的是數(shù)據(jù)幀的數(shù)據(jù)類型 1 表示文本 2 表示二進制

MASK: 表示數(shù)據(jù)有沒有使用掩碼

Payload length :數(shù)據(jù)的長度,Payload data的長度,占7bits,7+16bits,7+64bits Masking-key :數(shù)據(jù)掩碼 (設(shè)置位0,則該部分可以省略,如果設(shè)置位1,則用來解碼客戶端發(fā)送給服務(wù)端的數(shù)據(jù)幀)

Payload data : 幀真正要發(fā)送的數(shù)據(jù),可以是任意長度

advanceFrame 解析方法

????????實現(xiàn)代碼會比較長,如果直接貼代碼,會看不下去,該方法返回數(shù)據(jù)類型和error, 這里我們只會截取其中一部分

func (c *Conn) advanceFrame() (int, error) {...//讀取前兩個字節(jié)p, err := c.read(2)if err != nil {return noFrame, err}//數(shù)據(jù)幀類型frameType := int(p[0] & 0xf)// FIN 標記位final := p[0]&finalBit != 0//三個擴展用rsv1 := p[0]&rsv1Bit != 0rsv2 := p[0]&rsv2Bit != 0rsv3 := p[0]&rsv3Bit != 0//mask :是否使用掩碼mask := p[1]&maskBit != 0...switch c.readRemaining {case 126:p, err := c.read(2)if err != nil {return noFrame, err}if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {return noFrame, err}case 127:p, err := c.read(8)if err != nil {return noFrame, err}if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {return noFrame, err}}..
}

整個流程分為了 7 個部分:

  1. 跳過前一幀的剩余部分,畢竟這是之前幀的數(shù)據(jù)
  2. 讀取并解析幀頭的前兩個字節(jié)(從上面圖中可以看出只讀取到 Payload len)
  3. 根據(jù)讀取和解析幀長度(根據(jù) Payload length的值來獲取Payload data的長度)
  4. 處理數(shù)據(jù)幀的mask掩碼
  5. 如果是文本和二進制消息,強制執(zhí)行讀取限制并返回 (結(jié)束)
  6. 讀取控制幀有效載荷 即 play data,設(shè)置setReadRemaining以安全地更新此值并防止溢出
  7. 過程控制幀有效載荷,如果是ping/pong/close消息類型,返回 -1 (noFrame) (結(jié)束)

????????advanceFrame方法的主要目的就是解析數(shù)據(jù)幀,獲取數(shù)據(jù)幀的消息類型,而對于數(shù)據(jù)幀的解析都是按照上圖幀格式來的!


heartbeat 心跳

????????WebSocket 為了確保客戶端、服務(wù)端之間的 TCP 通道連接沒有斷開,使用心跳機制來判斷連接狀態(tài)。如果超時時間內(nèi)沒有收到應(yīng)答則認為連接斷開,關(guān)閉連接,釋放資源。流程如下

  • 發(fā)送方 -> 接收方:ping
  • 接收方 -> 發(fā)送方:pong

ping、pong 消息:它們對應(yīng)的是 WebSocket 的兩個控制幀,opcode分別是0x9、0xA,對應(yīng)的消息類型分別是PingMessage, PongMessage,前提是應(yīng)用程序需要先讀取連接中的消息才能處理從對等方發(fā)送的 close、ping 和 pong 消息。


總結(jié)

????????本文主要了解 什么是Websocket以及gorilla/websocket 框架的使用和部分底層實現(xiàn)原理代碼走讀。

????????不過流行的開源 Go 語言 Web 工具包 Gorilla 宣布已正式歸檔,目前已進入只讀模式?!八l(fā)出的信號是,這些庫在未來將不會有任何發(fā)展。也就是說 gorilla/websocket 這個被廣泛使用的 websocket 庫也會停止更新了,真是個令人悲傷的消息!

????????正如作者所說的那樣:“沒有一個項目需要永遠存在。這可能不會讓每個人都開心,但生活就是這樣?!?/p>

http://www.risenshineclean.com/news/22856.html

相關(guān)文章:

  • 翻譯公司網(wǎng)站建設(shè)多少錢百度賬號注冊中心
  • 建立網(wǎng)站外鏈常用的渠道有哪些優(yōu)化是什么梗
  • 在哪個網(wǎng)站做任務(wù)賺錢小程序開發(fā)公司前十名
  • 響水做網(wǎng)站的源碼交易平臺
  • 有哪些網(wǎng)站可以做家教網(wǎng)絡(luò)推廣seo怎么做
  • 合川網(wǎng)站優(yōu)化茶葉網(wǎng)絡(luò)推廣方案
  • 國內(nèi)互聯(lián)網(wǎng)公司排名2021seo推廣軟件哪個好
  • 專門做干果批發(fā)的網(wǎng)站seo快速排名關(guān)鍵詞
  • wordpress wjj搜索引擎優(yōu)化的基本方法
  • goood設(shè)計網(wǎng)站俄羅斯搜索引擎yandex
  • 下列關(guān)于網(wǎng)站開發(fā)網(wǎng)頁上傳中國500強最新排名
  • wordpress移動端導航鞍山seo公司
  • 安徽建站公司短視頻關(guān)鍵詞優(yōu)化
  • web網(wǎng)站開發(fā)技術(shù)考試題型武漢seo哪家好
  • 動態(tài)網(wǎng)站建設(shè)技術(shù)做網(wǎng)頁設(shè)計的軟件
  • 建設(shè)工程信息在什么網(wǎng)站發(fā)布互動營銷策略
  • 國外怎么做直播網(wǎng)站網(wǎng)站很卡如何優(yōu)化
  • 做網(wǎng)站和seo流程接推廣app任務(wù)的平臺
  • 企業(yè)網(wǎng)站html源代碼永久免費二級域名申請
  • 二次網(wǎng)站開發(fā)電商seo是什么
  • 輕定制網(wǎng)站建設(shè)seo在中國
  • 河北網(wǎng)站建設(shè)與推廣站長域名查詢工具
  • 網(wǎng)站設(shè)計開發(fā)人員招聘40個免費靠譜網(wǎng)站
  • 食品包裝設(shè)計價格seo崗位工資
  • 做網(wǎng)站用windows和 linux廣州網(wǎng)絡(luò)推廣公司有哪些
  • 企業(yè)網(wǎng)站建設(shè)論文糕點烘焙專業(yè)培訓學校
  • 網(wǎng)站域名免費申請網(wǎng)站排名優(yōu)化培訓哪家好
  • 做 愛 網(wǎng)站小視頻下載查詢網(wǎng)官網(wǎng)
  • 中石化第五建設(shè)有限公司官方網(wǎng)站濰坊關(guān)鍵詞優(yōu)化平臺
  • wordpress商家展示主題企業(yè)網(wǎng)站的優(yōu)化建議