從http到websocket

閱讀本文之前,你最好已經做過一些websocket的簡單應用

從http到websocket

  • HTTP101
  • HTTP 輪詢、長輪詢和流化
  • 其他技術
    • 1. 服務器發送事件
    • 2. SPDY
    • 3. web實時通信
  • 互聯網簡史
  • web和http
  • Websocket協議
    • 1. 簡介
    • 2. 初始握手
    • 3. 計算響應健值
    • 4. 消息格式
    • 5. WebSocket關閉握手
  • 實現

HTTP101

  • 在HTTP/1.0中,每個服務器請求需要一個單獨的鏈接,這種方法至少可以說沒有太好的伸縮性。在HTTP的下一個修訂版本,也就是HTTP/1.1中增加了可重用連接。由于可重用連接的推出,瀏覽器可以初始化一個到web服務器的連接,以讀取HTML頁面,然后重用該連接讀取圖片、腳本等資源。HTTP/1.1通過減少客戶端到服務器的連接數量,降低了請求的延遲
  • HTTP是無狀態的,也就是說,它將每個請求當成唯一和獨立的。無狀態協議具有一些優勢,例如,服務器不需要保存有關會話的信息,從而不需要存儲數據。但是這也意味著在每次HTTP請求和響應中都會發送關于請求的冗余信息
  • 從根本上講,HTTP還是半雙工的協議,也就是說,在同一時刻,流量只能單向流動:客戶端向服務器發送請求,然后服務器響應請求。之后出現了輪詢、長輪詢和HTTP流化(streaming)等技術

HTTP 輪詢、長輪詢和流化

  • 很多提供實時web應用程序的嘗試多半是圍繞輪詢(polling)技術進行的,這是一種定時的同步調用,客戶端向服務器發送請求查看是否有可用的新信息。請求以固定的時間間隔發出,不管是否有信息,客戶端都會得到響應:如果有可用信息,服務器發送這些信息,否則服務器返回一個拒絕響應,客戶端關閉連接,這種技術的問題在于我們無法事先預知信息交付的精確間隔,從而導致打開或者關閉很多不必要的連接
  • 長輪詢(long polling)是另一種流行的通信方法,客戶端向服務器請求信息,并且在設定的時間段內打開一個連接。服務器如果沒有任何信息,會保持請求打開,直到有客戶端可用的信息,或者直到指定的超時時間用完為止。這時,客戶端重新向服務器請求信息。長輪詢也稱作Comet或者反向AJAXComet延長HTTP響應的完成,直到服務器有需要發送給客戶端的內容,這種技術常常稱作“掛起GET”或“擱置POST”。但是當信息量很大的時候,長輪詢相對于傳統輪詢并沒有明顯的性能優勢,因為客戶端必須頻繁的重連到服務器以讀取新信息,造成網絡的表現和快速輪詢相同。長輪詢的另一個問題是缺乏標準實現
  • 在流化技術中,客戶端發送一個請求,服務器發送并維護一個持續更新和保持打開(可以是無限或者規定的時間段)的開放響應。每當服務器有需要交付給客戶端的信息時,它就更新響應。似乎這是一種能夠適應不可預測的信息交付的極佳方案,但是服務器從不發出完成HTTP響應的請求,從而使連接一直保持打開。在這種情況下,代理和防火墻可能緩存響應,導致信息交付的延遲增加。因此,許多流化的嘗試對于存在防火墻和代理的網絡時不友好的
  • 上述幾種方法還有一些問題,例如冗余的HTTP首標數據和延遲、客戶端必須等待請求返回才能發出后續的請求,這會顯著增加延遲

其他技術

1. 服務器發送事件

  • 如果你的服務主要向其客戶端廣播或者推送消息,而不需要任何交互,可能使用服務器發送事件(Server-Sent Events SSE)提供的EventSource API是個好的選擇。SSEHTML5規范的一部分,加強了某些Comet技術,可以將SSE當作一種HTTP輪詢、長輪詢和流化的公用可互操作語法使用。利用SSE,你可以得到自動重連、事件ID等功能,但是這種方式只支持文本數據

2. SPDY

  • SPDY(音同"Speedy")是Google開發的一種網絡協議,本質上擴充了HTTP協議,通過壓縮HTTP首標和多路復用等手段改進HTTP請求性能,也就是說,它相當于對于HTTP進行的增量改進,改正了許多HTTP的非本質問題,增加了多路復用、工作管道(working pipling)和其他有用的改進。而websocketHTTP之間的不同是架構性的,不是增量的,可以將SPDY擴充的HTTP連接升級為Websocket,從而在兩個領域獲得利益

3. web實時通信

  • 這是一種瀏覽器之間的點對點技術,不借助服務器傳輸數據,目前尚未完善

互聯網簡史

  • 一開始,互聯網主機之間采用TCP/IP通信。在這種情況下,任意一臺主機都可以建立新的連接,一旦TCP連接建立,兩臺主機都可以在任何時候發送數據
  • 你想在網絡協議中實現的其他功能必須在傳輸協議基礎上構建,這些更高的層次被稱為應用層協議。例如,在web之前主線的用于聊天的IRC和用于遠程終端訪問的telnet就是兩個重要的應用層協議,他們顯然需要異步的雙向通信,客戶端必須在另一個用戶發送聊天消息或者遠程應用程序打印一行輸出時接收到提示通知。由于這些協議一般在TCP之上運行,異步雙向通信總是可用
  • TCP/IP還是http和websocket協議的基礎,我們先簡單介紹一下http協議

web和http

  • 1991年,萬維網(World Wide Web)項目第一次公布。Web是使用統一資源定位符 (URL)鏈接的超文本文檔系統。當時,URL是一個重大的發明。URL的U是universal(統一)的縮寫,說明了當時的一個革命性想法 ,所有超文本文檔的相互連接。Web上的HTML文檔通過URL相互連接。更有意義的事Web洗可以經過裁剪,用于讀取資源。HTTP是一個用于文檔傳輸的簡單同步請求 — \text{---} 響應式協議
  • 最早的web應用程序使用表單和全頁刷新。每當用戶提交信息,瀏覽器將提交一個表單并讀取新頁面。每當有需要顯示的更新信息,用戶或者瀏覽器必須刷新整個頁面,使用HTTP讀取整個資源
  • 利用JavaScriptXMLHttpRequest API,人們開發出了一組稱為AJAX的技術,這項技術能夠使應用程序在每次交互期間不會有不連貫的過渡。AJAX使應用程序只讀取感興趣去的資源數據,并在沒有導航的情況下更新頁面。AJAX使用的網絡協議仍然是HTTP;盡管名為XMLHttpRequest,數據也只是有時使用XML格式,而不是始終使用該格式
  • 本質上,HTTP用其內置的文本支持、URLHTTPS使Web成為可能,然而,在某種程度上,HTTP的流行也造成了互聯網的退化。因為HTTP不需要可尋址的客戶端,Web世界的尋址變成不對稱的。瀏覽器能夠通過URL尋找服務器資源,但是服務器端應用程序卻無法主動的向客戶端發送資源。客戶端只能發起請求,而服務器只能響應未決的請求,在這個非對稱的世界中,要求全雙工通信的協議無法正常工作
  • 解決這一局限性的方法之一是由客戶端發出HTTP請求,以防服務器有需要共享的更新。使用HTTP請求顛倒通知流程的這一過程用一個傘形術語Comet來表示。正如前面所說,Comet本質是一組利用輪詢、長輪詢和流化開發HTTP潛力的技術。這些技術實際上模擬了TCP的一些功能。因為同步的HTTP和這些異步應用程序之間不匹配,Comet復雜、不標準且低效

Websocket協議

1. 簡介

  • Websocket是定義服務器和客戶端如何通過Web通信的一種網絡協議。在萬維網以及其基礎技術HTML、HTTP等推出之前,互聯網和現在完全不同。一方面,它比現在小的多;另一方面,它實際上是一個對等網絡。當時互聯網主機之間通信的兩個流行協議現在仍然盛行:互聯網協議(Internet Protocol, IP)和傳輸控制協議(Transmission Control Protocol, TCP)
    ,前者負責在互聯網的兩臺主機之間傳送數據封包,后者可以看做跨越互聯網,在兩個端點之間可靠地雙向傳輸字節流的一個管道。兩者結合起來的TCP/IP在歷史上是無數網絡應用程序使用的和核心傳輸層協議,這種情況仍在持續
  • WebSocket為Web應用程序保留了我們所喜歡的HTTP特性(URL、HTTP安全性、更簡單的基于數據模型的消息和內置的文本支持),同時提供了其他網絡架構和通信模式。和TCP一樣,WebSocket是異步的,可以用作高級協議的傳輸層。WebSocket是消息協議、聊天、服務器通知、管道和多路復用協議、自定義協議、緊湊二進制協議和用于互聯網服務器互操作的其他標準協議的很好基礎
  • WebSocket為Web應用程序提供了TCP風格的網絡能力。尋址仍然是單向的,服務器可以異步發送客戶端數據,但是只在WebSocket連接打開時才能做到。在客戶端和服務器之間WebSocket連接始終打開。WebSocket服務器也可以作為WebSocket客戶端
特性TCPHTTPWebSocket
尋址IP地址和端口URLURL
并發傳輸全雙工半雙工全雙工
內容字節流MIME信息文本和二進制數據
消息定界
連接定向
  • TCP只能傳送字節流,所以消息邊界只能由更高層的協議來表現。對于TCP來說,它唯一可以保證的是到達接收端的單個字節將會按順序到達。和TCP不同,WebSocket傳輸一序列單獨的消息,在WebSocket中,和HTTP一樣,多字節的消息作為整體,按照順序到達。因為WebSocket協議內置了消息邊界,所以它能夠發送和接收單獨的消息并避免常見的碎片錯誤
  • IP處于互聯網層,而TCP處于IP之上的傳輸層。WebSocket的層次在TCP/IP之上,因為你可以在WebSocket上構建應用級協議,所以它也被看作是傳輸層協議

2. 初始握手

  • 每個WebSocket連接都始于一個HTTP請求,該請求與其他請求很相似,但是包含一個特殊的首標 — \text{---} Upgrade,這個首標表示客戶端將把連接升級到不同的協議。在這種情況下,這種特殊的協議就是WebSocket
  • 從客戶端發往服務器升級為WebSocket的HTTP請求稱為WebSocket的初始握手,在成功升級之后,連接的語法切換為用于表示WebSocket消息的數據幀格式。除非服務器響應 101代碼、Upgrade首標和Sec-WebSocket-Accept首標,否則WebSocket連接不能成功。Sec-WebSocket-Accept響應首標的值從Sec-WebSocket-Key請求首標繼承而來,包含一個特殊的響應健值,必須與客戶端的預期精確匹配

3. 計算響應健值

  • 為了成功地完成握手,WebSocket服務器必須響應一個計算出來的健值。這個響應說明服務器理解WebSocket協議。這個響應說明服務器理解WebSocket協議。。沒有精確的響應,就可能哄騙一些輕信的HTTP服務器意外的升級一個連接
  • 響應函數從客戶端發送的Sec-WebSocket-Key首標中取得鍵值,并在Sec-WebSocket- Accept首標中返回根據客戶端預期計算的鍵值
首標描述
Sec-WebSocket-Key只能在HTTP請求中出現一次,用于從客戶端到服務器的WebSocket初始握手,避免跨協議攻擊
Sec-WebSocket-Accept只能在HTTP請求中出現一次,用于從客戶端到服務器的WebSocket初始握手,確認服務器理解WebSocket協議
Sec-WebSocket-Extensions可能在HTTP請求中出現多次,但是在HTTP響應中只能出現一次。用于從客戶端到服務器的WebSocket初始握手,然后用于從服務器到客戶端的響應。這個首標幫助客戶端和服務器商定一組連接期間使用的協議級擴展
Sec-WebSocket-Protocol用于從客戶端到服務器的WebSocket初始握手,然后用于從服務器到客戶端的響應。這個首標通告客戶端應用程序可使用的協議。服務器使用相同的首標,在這些協議中最多選擇一個
Sec-WebSocket-Version用于從客戶端到服務器的WebSocket初始握手,表示版本兼容性。RFC 6455的版本總是13。服務器如果不支持客戶端請求的協議版本,則用這個首標響應。在那種情況下,服務器發送的首標中列出了它支持的版本。這只發生在RFC 6455之前的客戶端中

4. 消息格式

  • 當WebSocket連接打開時,客戶端和服務器可以在任何時候相互發送消息。這些消息在網絡上用于標記消息之間邊界并包括簡潔的類型消息的二進制語法表示。更準確地說,這些二進制首標標記另一個單位 — \text{---} 幀(frame)之間的邊界。幀是可以合并組成消息的部分數據。你可能在WebSocket的相關討論中將“幀”和“消息”互換使用,這是因為很少有一個消息使用超過一個幀的(至少目前如此)。而且在協議幀的早期草案中,幀就是消息,消息在線路上的表示被稱作“組幀”(framing)
  • WebSocket API沒有向應用程序暴露幀級別的信息。盡管API按照消息工作,但是可以在協議級別上處理子消息數據單元。雖然消息一般只有一個幀,但是它可以由任意數量的幀組成。服務器可以使用不同數量的幀,在全體數據可用之前開始交付數據
  • 下面是一個WebSocket幀頭
    在這里插入圖片描述
      0                   1                   2                   30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len |    Extended payload length    ||I|S|S|S|  (4)  |A|     (7)     |             (16/64)           ||N|V|V|V|       |S|             |   (if payload len==126/127)   || |1|2|3|       |K|             |                               |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +|     Extended payload length continued, if payload len == 127  |+ - - - - - - - - - - - - - - - +-------------------------------+|                               |Masking-key, if MASK set to 1  |+-------------------------------+-------------------------------+| Masking-key (continued)       |          Payload Data         |+-------------------------------- - - - - - - - - - - - - - - - +:                     Payload Data continued ...                :+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +|                     Payload Data continued ...                |+---------------------------------------------------------------+

參考https://datatracker.ietf.org/doc/html/rfc6455

下面詳細介紹

  1. 操作碼(opcode)
  • 每條Websocket消息都有一個指定消息載荷類型的操作碼。操作碼由幀頭的第一個字節中最后4 bit組成,如下表所示
操作碼消息載荷類型描述
1文本消息的數據類型為文本
2二進制消息的數據類型為二進制
8關閉客戶端或者服務器向對方發送關閉握手
9ping客戶端或者服務器向對方發送ping
10pong客戶端或者服務器向對方發送pong
  • 4 bit的操作碼有16種可能取值,WebSocket協議只定義了5種操作碼,剩余的操作碼保留用于未來的擴展
  1. 長度
  • WebSocket協議使用可變位數來變碼幀長度,這樣,小的消息就能使用緊湊的編碼,協議仍然可以攜帶中型甚至非常大的消息。對于小于126字節的消息,長度用幀頭前兩個字節之一來表示。對于126~216字節的消息,使用額外的兩個字節表示長度。對于大于216字節的消息,長度為8字節。該長度編碼保存于幀頭第二個字節的最后7位。該字段種126和127兩個值被當作特殊的信號,表示需要后面的字節才能完成長度編碼
  1. 編碼文本
  • WebSocket文本消息用8位UCS轉換格式(UTF-8)編碼。UTF-8是用于Unicode的變長編碼,向后兼容7位的ASCII,也是WebSocket文本消息允許的唯一編碼。堅持使用UTF-8編碼避免了大量的“普通文本”格式以及協議中的不同編碼對互操作性的妨害
  1. 屏蔽(或者叫掩碼)
  • 從瀏覽器向服務器發送的WebSocket幀內容進行了“屏蔽”,以混淆其內容。屏蔽的目的不是阻止竊聽,而是為了不常見的安全原因,以及改進和現有的HTTP代理的兼容性。
  • 幀頭的第二個字節的第一位表示該幀是否進行了屏蔽,WebSocket協議要求客戶端屏蔽發送的所有幀。如果有屏蔽,所用的掩碼將占據幀頭擴展長度部分后的4個字節
  • WebSocket服務器接收的每個載荷在處理之前首先被解除屏蔽。解除屏蔽之后,服務器得到原始消息內容:二進制消息可以直接交付;文本消息將進行UTF-8編碼,并通過服務器API輸出字符串
  1. 多幀消息
  • 幀格式中的fin位考慮了多幀消息或者部分可用消息的流化,這些消息可能不連續或者不完整。要發送一條不完整的消息,你可以發送一個fin位設置為0的幀。最后一個幀的fin位設置為1,表示消息以這一幀的載荷作為結束

5. WebSocket關閉握手

  • WebSocket連接總是以初始握手開始,因為這是初始化互聯網和其他可靠網上對話的唯一手段,連接可以在任何時候關閉,所以不可能總是以關閉握手結束。有時候,底層的TCP套接字可能突然關閉。關閉握手優雅地關閉連接,使應用程序能夠知道有意中斷和意外終止連接之間的差異
  • 當WebSocket關閉時,終止連接的端點可以發送一個數字代碼,以及一個表示選擇關閉套接字原因的字符串。代碼和原因編碼為具有關閉操作碼的一個幀的載荷。數字代碼 用一個16位無符號整數表示,原因則是一個UTF-8編碼的短字符串。RFC 6455定義了多種特殊的關閉代碼。代碼1000~1015規定用于WebSocket連接層。這戲的代碼表示網絡中或者協議中的某些故障,下表是關閉代碼
代碼描述何時使用
1000正常關閉當你的會話成功完成時發送這個代碼
1001離開因應用程序離開且不希望后續的連接嘗試而關閉連接時,發送這一代碼。服務器可能關閉,或者客戶端應用程序可能關閉
1002協議錯誤當因協議錯誤而關閉連接時發送這一代碼
1003不可接受的數據類型當應用程序接收到一條無法處理的意外類型消息時發送這一代碼
1004保留不要發送這一代碼。根據RFC 6455,這個狀態碼保留,可能在未來定義
1005保留不要發送這一代碼。WebSocket API用這個代碼表示沒有接收到任何代碼
1006保留不要發送這一代碼。WebSocket API用這個代碼表示連接異常關閉
1007無效數據在接收一個格式與消息類型不匹配的消息之后發送這一代碼。如果文本消息包含錯誤格式的UTF-8數據,連接應該用這個代碼關閉
1008違反消息政策當應用程序由于其他代碼所不包含的原因終止連接,或者不希望泄露消息無法處理的原因時,發送這一代碼
1009消息過大當接收的消息過大,應用程序無法處理時發送這一代碼(幀的載荷長度最多為64字節,即使你有一個大服務器,有些消息也仍然太大)
1010需要擴展當應用程序需要一個或職責多個服務器無法協商的特殊擴展時,從客戶端(瀏覽器)發送這一代碼
1011意外情況當應用程序由于不可預見的原因,無法繼續處理連接時,發送這一代碼
1015TLS失敗(保留)不要發送這個代碼。WebSocket API用這個代碼表示TLS在WebSocket握手之前失敗

實現

  • 我們分析一下golang的websocket包https://github.com/gorilla/websocket來看此協議是如何實現的,重點關注conn.go文件,我們先看一下Conn結構的定義
type Conn struct {conn        net.Conn // 底層網絡連接isServer    bool // 如果這個連接作為服務器端的連接則為true,如果是客戶端則為falsesubprotocol string // 代表WebSocket連接中協商的子協議// Write fields (寫操作相關字段)mu            chan struct{} // used as mutex to protect write to conn(用作互斥鎖保護對連接的寫操作)writeBuf      []byte        // frame is constructed in this buffer.(字節切片,用于構造要寫入的幀)writePool     BufferPool // 提供和管理寫緩沖區的池writeBufSize  int // 寫緩沖區的大小writeDeadline time.Time // 寫操作的截止時間writer        io.WriteCloser // the current writer returned to the application(當前返回給應用程序的寫入器)isWriting     bool           // for best-effort concurrent write detection(用于盡最大努力檢測并發寫操作)writeErrMu sync.Mutex // 用于保護寫操作錯誤的互斥鎖writeErr   error // 保存寫操作中發生的錯誤enableWriteCompression bool // 指示是否啟用寫操作壓縮compressionLevel       int // 寫操作壓縮的級別newCompressionWriter   func(io.WriteCloser, int) io.WriteCloser // 用于創建新的壓縮寫入器// Read fields(讀操作相關字段)reader  io.ReadCloser // the current reader returned to the application (當前 返回給應用程序的讀取器)readErr error // 保存讀操作中發生的錯誤br      *bufio.Reader // 帶緩沖的讀取器// bytes remaining in current frame. // set setReadRemaining to safely update this value and prevent overflowreadRemaining int64 // 當前幀剩余的字節數readFinal     bool  // true the current message has more frames.(指示當前消息是否有更多幀)readLength    int64 // Message size. // 消息大小readLimit     int64 // Maximum message size. // 消息的最大大小readMaskPos   int // 掩碼在消息中的位置readMaskKey   [4]byte // 用于WebSocket消息掩碼handlePong    func(string) error // 處理Pong幀的回調函數handlePing    func(string) error // 處理Ping幀的回調函數handleClose   func(int, string) error // 處理關閉幀的回調函數readErrCount  int // 記錄讀取錯誤的次數messageReader *messageReader // the current low-level reader(當前的底層消息讀取器)readDecompress         bool // whether last read frame had RSV1 set(指示最后讀取的幀是否設置了RSV1(用于壓縮))newDecompressionReader func(io.Reader) io.ReadCloser // 用于創建新的解壓縮讀取器
}
  • 之后我們來看一個關鍵的函數ReadMessage
// ReadMessage is a helper method for getting a reader using NextReader and
// reading from that reader to a buffer.
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}p, err = io.ReadAll(r)return messageType, p, err
}
  • ReadMessage函數是我們經常會用到的函數,它用來接收WebSocket消息,一般接收到來自瀏覽器端的每條消息,我們會從p數組中獲取,可以看到這個函數的核心功能是NextReader()方法實現的,下面是這個方法
// NextReader returns the next data message received from the peer. The
// returned messageType is either TextMessage or BinaryMessage.
//
// There can be at most one open reader on a connection. NextReader discards
// the previous message if the application has not already consumed it.
//
// Applications must break out of the application's read loop when this method
// returns a non-nil error value. Errors returned from this method are
// permanent. Once this method returns a non-nil error, all subsequent calls to
// this method return the same error.
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {// Close previous reader, only relevant for decompression.if c.reader != nil {_ = c.reader.Close()c.reader = nil}c.messageReader = nilc.readLength = 0for c.readErr == nil {frameType, err := c.advanceFrame()if err != nil {c.readErr = errbreak}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}}// Applications that do handle the error returned from this method spin in// tight loop on connection failure. To help application developers detect// this error, panic on repeated reads to the failed connection.c.readErrCount++if c.readErrCount >= 1000 {panic("repeated read on failed websocket connection")}return noFrame, nil, c.readErr
}
  • 核心是advanceFrame函數,這個函數內部實現了WebSocket協議的內容,此處可以對照幀格式來閱讀
      0                   1                   2                   30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len |    Extended payload length    ||I|S|S|S|  (4)  |A|     (7)     |             (16/64)           ||N|V|V|V|       |S|             |   (if payload len==126/127)   || |1|2|3|       |K|             |                               |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +|     Extended payload length continued, if payload len == 127  |+ - - - - - - - - - - - - - - - +-------------------------------+|                               |Masking-key, if MASK set to 1  |+-------------------------------+-------------------------------+| Masking-key (continued)       |          Payload Data         |+-------------------------------- - - - - - - - - - - - - - - - +:                     Payload Data continued ...                :+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +|                     Payload Data continued ...                |+---------------------------------------------------------------+
  • 前兩行展示了幀格式的布局和結構,0到3表示列的索引,好像沒什么實際含義,只是標記0的位置,第二行有32個數字,表示32個比特位,一共4個字節
  • FIN表示這是消息的最后一個幀
  • RSV1,RSV2,RSV3用于擴展協議
  • OpCode指示數據幀的類型,例如文本、二進制、連接關閉等
  • Mask指示是否有掩碼
  • Payload length表示負載數據的長度,也就是實際傳輸的數據長度
  • Extended payload length表示擴展的負載長度,如果Payload len小于等于125個字節,則使用一個字節來表示長度;如果長度在126個字節到65535個字節之間,則使用兩個字節來表示長度;如果長度超過65535個字節,則使用8個字節來表示長度。因此,負載數據的長度最大可以達到 2 64 ? 1 2^{64}-1 264?1,所以一般不會有超過一個幀的數據
  • Masking-Key是發送方隨機生成的4字節掩碼,它會對負載數據進行加密,接收方使用這個掩碼對負載數據進行加密,以獲取原始數據內容。它能夠防止一些網絡攻擊,如中間人攻擊,每次隨機生成的Masking-Key使得中間人無法進行持續解密
  • 接下來的部分就都是傳輸的數據了
// Read methodsfunc (c *Conn) advanceFrame() (int, error) {// 1. Skip remainder of previous frame.if c.readRemaining > 0 {if _, err := io.CopyN(io.Discard, c.br, c.readRemaining); err != nil {return noFrame, err}}// 2. Read and parse first two bytes of frame header.// To aid debugging, collect and report all errors in the first two bytes// of the header.var errors []stringp, err := c.read(2)if err != nil {return noFrame, err}frameType := int(p[0] & 0xf)final := p[0]&finalBit != 0rsv1 := p[0]&rsv1Bit != 0rsv2 := p[0]&rsv2Bit != 0rsv3 := p[0]&rsv3Bit != 0mask := p[1]&maskBit != 0if err := c.setReadRemaining(int64(p[1] & 0x7f)); err != nil {return noFrame, err}c.readDecompress = falseif rsv1 {if c.newDecompressionReader != nil {c.readDecompress = true} else {errors = append(errors, "RSV1 set")}}if rsv2 {errors = append(errors, "RSV2 set")}if rsv3 {errors = append(errors, "RSV3 set")}switch frameType {case CloseMessage, PingMessage, PongMessage:if c.readRemaining > maxControlFramePayloadSize {errors = append(errors, "len > 125 for control")}if !final {errors = append(errors, "FIN not set on control")}case TextMessage, BinaryMessage:if !c.readFinal {errors = append(errors, "data before FIN")}c.readFinal = finalcase continuationFrame:if c.readFinal {errors = append(errors, "continuation after FIN")}c.readFinal = finaldefault:errors = append(errors, "bad opcode "+strconv.Itoa(frameType))}if mask != c.isServer {errors = append(errors, "bad MASK")}if len(errors) > 0 {return noFrame, c.handleProtocolError(strings.Join(errors, ", "))}// 3. Read and parse frame length as per// https://tools.ietf.org/html/rfc6455#section-5.2//// The length of the "Payload data", in bytes: if 0-125, that is the payload// length.// - If 126, the following 2 bytes interpreted as a 16-bit unsigned// integer are the payload length.// - If 127, the following 8 bytes interpreted as// a 64-bit unsigned integer (the most significant bit MUST be 0) are the// payload length. Multibyte length quantities are expressed in network byte// order.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}}// 4. Handle frame masking.if mask {c.readMaskPos = 0p, err := c.read(len(c.readMaskKey))if err != nil {return noFrame, err}copy(c.readMaskKey[:], p)}// 5. For text and binary messages, enforce read limit and return.if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {c.readLength += c.readRemaining// Don't allow readLength to overflow in the presence of a large readRemaining// counter.if c.readLength < 0 {return noFrame, ErrReadLimit}if c.readLimit > 0 && c.readLength > c.readLimit {if err := c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)); err != nil {return noFrame, err}return noFrame, ErrReadLimit}return frameType, nil}// 6. Read control frame payload.var payload []byteif c.readRemaining > 0 {payload, err = c.read(int(c.readRemaining))if err := c.setReadRemaining(0); err != nil {return noFrame, err}if err != nil {return noFrame, err}if c.isServer {maskBytes(c.readMaskKey, 0, payload)}}// 7. Process control frame payload.switch frameType {case PongMessage:if err := c.handlePong(string(payload)); err != nil {return noFrame, err}case PingMessage:if err := c.handlePing(string(payload)); err != nil {return noFrame, err}case CloseMessage:closeCode := CloseNoStatusReceivedcloseText := ""if len(payload) >= 2 {closeCode = int(binary.BigEndian.Uint16(payload))if !isValidReceivedCloseCode(closeCode) {return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode))}closeText = string(payload[2:])if !utf8.ValidString(closeText) {return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")}}if err := c.handleClose(closeCode, closeText); err != nil {return noFrame, err}return noFrame, &CloseError{Code: closeCode, Text: closeText}}return frameType, nil
}
  • 第一步丟棄之前的幀的數據的原因是我們希望盡可能的從一個干凈的狀態開始,這些幀既然現在還存在就說明上一次接收的數據已經決定丟棄這些幀,那么這些幀是要被這次的數據忽略的,在這個函數中用到了io.Discard結構體,這個結構體很簡單,寫入它的所有數據都會被丟棄,通過io.CopyN將當前讀取器緩沖區中上一個幀的殘留數據全部清除
  • 后面的代碼都是對協議的實現,理解上面的幀格式表之后,不難理解代碼內容

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/711833.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/711833.shtml
英文地址,請注明出處:http://en.pswp.cn/news/711833.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Redis 緩存數據庫

redis 中文網 http://www.redis.cn/ redis.net.cn 兩種數據庫陣營 1.關系型數據庫 MySQL Oracle DB2 SQL Server 等基于二維表結構存儲數據的文件型磁盤數據庫 缺點: 因為數據庫的特征是磁盤文件型數據庫, 就造成每次查詢都有IO操作, 海量數據查詢速度較慢 2.NoSQL數據庫 …

C++中的常對象、常函數

一、常對象的概念 常對象就是用 const 修飾的對象&#xff0c;常對象必須初始化且不可被修改。 //以日期類對象為例 const Date d1(2004, 5, 25); 二、常對象只能調用常函數 常對象只能調用常函數&#xff0c;不能調用其他函數。 以日期類為例&#xff0c;類中有成員函數P…

lv20 QT 常用控件 2

1 QT GUI 類繼承簡介 布局管理器 輸出控件 輸入控件 按鈕 容器 2 按鈕示例 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QCheckBox> #include <QLineEdit> #include <QPushButton>class Widget : public QWidget {Q_OBJECTpublic…

SLAM面試代碼題:點云去畸變

題目 假設已知一幀點云每個點的時間戳和它的位姿,要求對點云去畸變 解題思路 定義一個點云的struct利用時間戳,把一幀內每個時刻的點云都變換到這一幀的起始時間處位置使用線性插值,旋轉使用球面非線性插值// 點云去畸變 #include <iostream> #include <Eigen/Co…

擊鼓傳花游戲

有N個小朋友圍成一圈玩擊鼓傳花游戲&#xff0c;將小朋友編號為1-N&#xff0c;從1號開始傳花&#xff0c;每次傳3個&#xff0c;拿到花的小朋友表演節目后退出。任給N&#xff0c;問最后一個表演的小朋友編號是多少&#xff1f;例如&#xff1a;輸入5&#xff0c;從1號開始傳花…

基于springboot+vue的共享汽車管理系統(前后端分離)

博主主頁&#xff1a;貓頭鷹源碼 博主簡介&#xff1a;Java領域優質創作者、CSDN博客專家、阿里云專家博主、公司架構師、全網粉絲5萬、專注Java技術領域和畢業設計項目實戰&#xff0c;歡迎高校老師\講師\同行交流合作 ?主要內容&#xff1a;畢業設計(Javaweb項目|小程序|Pyt…

Linux中的echo命令

echo?命令是在Linux系統中常用的用于輸出文本或變量內容的命令。它可以將指定的文本或變量的值輸出到終端上。下面是關于 ?echo?命令的使用說明和示例&#xff1a; 1. 基本語法&#xff1a; echo [選項] [字符串或變量] 2. 使用示例&#xff1a; 輸出文本內容&#xff1…

3d模型版本轉換器注意事項---模大獅模型網

在使用3D模型版本轉換器時&#xff0c;有一些注意事項可以幫助您順利完成模型轉換并避免不必要的問題&#xff1a; 數據完整性&#xff1a;在進行模型轉換之前&#xff0c;確保您的原始3D模型文件沒有損壞或缺失數據。損壞的文件可能導致轉換器無法正常處理或輸出錯誤的結果。 …

力扣經典題目解析--滑動窗口最大值

原題地址: . - 力扣&#xff08;LeetCode&#xff09; 給你一個整數數組 nums&#xff0c;有一個大小為 k 的滑動窗口從數組的最左側移動到數組的最右側。你只可以看到在滑動窗口內的 k 個數字。滑動窗口每次只向右移動一位。 返回 滑動窗口中的最大值 。 示例 1&#xff1a;…

小程序自定義組件

自定義組件 1. 創建-注冊-使用組件 組件介紹 小程序目前已經支持組件化開發&#xff0c;可以將頁面中的功能模塊抽取成自定義組件&#xff0c;以便在不同的頁面中重復使用&#xff1b; 也可以將復雜的頁面拆分成多個低耦合的模塊&#xff0c;有助于代碼維護。 開發中常見的…

111790-37-5 ,生物素-氨基,一種生物素化合物,可與-NHS、-COOH反應

您好&#xff0c;歡迎來到新研之家 文章關鍵詞&#xff1a;111790-37-5 &#xff0c;生物素-氨基&#xff0c;生物素氨基&#xff0c;Biotin-NH2&#xff0c;Biotin-amine 一、基本信息 【產品簡介】&#xff1a;Biotin-NH2 provides a convenient biotinylation method for…

OSCP靶場--DVR4

OSCP靶場–DVR4 考點(1.windows&#xff1a;路徑遍歷獲取私鑰getshell 2.ssh shell中runas切換用戶) 1.nmap掃描 ┌──(root?kali)-[~/Desktop] └─# nmap -sV -sC -p- 192.168.161.179 --min-rate 2000 Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-29 07:14 EST…

Springboot接口參數校驗

在設計接口時我們通常需要對接口中的非法參數做校驗&#xff0c;以降低在程序運行時因為一些非法參數而導致程序發生異常的風險&#xff0c;例如登錄的時候需要校驗用戶名密碼是否為空&#xff0c;創建用戶的時候需要校驗郵件、手機號碼格式是否準確。如果在代碼中對接口參數一…

系統集成Prometheus+Grafana

根據產品需求在自己的系統中添加一個系統監控的頁面&#xff0c;其中有主機信息的顯示&#xff0c;也有一些業務信息的顯示。調研后的方案是 主機信息通過Prometheus采集和存儲&#xff0c;業務信息通過自己系統的調度任務統計后存儲在Mysql中&#xff0c;使用Grafana對接Prome…

Java必須掌握的繼承的特點和繼承體系的設計(含面試大廠題和源碼)

Java繼承是面向對象編程的一個基本特性&#xff0c;它允許一個類繼承另一個類的屬性和方法。設計良好的繼承體系是高質量軟件開發的關鍵。在大廠面試中&#xff0c;面試官可能會詢問關于Java繼承特點及如何設計一個合理的繼承體系的問題&#xff0c;以評估你的面向對象設計能力…

ICLR 2024|ReLU激活函數的反擊,稀疏性仍然是提升LLM效率的利器

論文題目&#xff1a; ReLU Strikes Back: Exploiting Activation Sparsity in Large Language Models 論文鏈接&#xff1a; https://arxiv.org/abs/2310.04564 參數規模超過十億&#xff08;1B&#xff09;的大型語言模型&#xff08;LLM&#xff09;已經徹底改變了現階段人工…

gcc和g++的區別,如何看自己的編譯器支持的C++的版本

gcc和g的區別 用一句話來說&#xff0c;就是gcc將程序視為c語言的&#xff0c;g將程序視為C的 gcc和g的區別主要在于它們處理不同后綴的文件類型、編譯和連接階段的不同調用方式&#xff0c;以及它們對C特性的支持方式。以下是詳細介紹&#xff1a;123 文件類型。gcc將后綴為…

通過多線程并發方式實現服務器

與多進程實現對比來看。 示例來源于網絡視頻 #include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <ctype.h> #include <unistd.h> #include <fcntl.h>#include "wrap.h"#de…

【C++ 測試】

C 測試 一、二維數組二、私有成員三、function用法四、類里面創建另一個類五、lambda六、Map動態申請 一、二維數組 #include <iostream> #include<windows.h> #include <map> // SetConsoleOutputCP ( CP_UTF8 ) ; using namespace std;void test1() {map…

求最短路徑之迪杰斯特拉算法

對fill用法的介紹 1.用鄰接矩陣實現 const int maxn100; const int INF100000000;//無窮大&#xff0c;用來初始化邊 int G[maxn][maxn];//用鄰接矩陣存儲圖的信息 int isin[maxn]{false};//記錄是否已被訪問 int minDis[maxn];//記錄到頂點的最小距離void Dijkstra(int s,in…