Golang實(shí)現(xiàn)WebSocket協(xié)議

一、什么是websocket

Websocket是一個(gè)應(yīng)用層協(xié)議,它必須依賴HTTP協(xié)議進(jìn)行一次握手,握手成功后,數(shù)據(jù)直接從TCP通道傳輸,此時(shí)就與HTTP無關(guān)了。所以websocket分為握手和數(shù)據(jù)傳輸兩個(gè)階段。

1. 握手階段

客戶端發(fā)送消息:

GET ws://192.168.2.123:2021/ws HTTP/1.1
Host: 192.168.2.123:2021
Connection: Upgrade
Upgrade: websocket
Origin: http://echo.localhost.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: z3HD6sns4+TSzfTr8NG56A==
  • Connection 告訴服務(wù)端對協(xié)議進(jìn)行升級,具體升級內(nèi)容取決于 Upgrade部分
  • Sec-WebSocket-Key 為了保證握手一致性,由客戶端生成隨機(jī)字符串并base64編碼,發(fā)送給服務(wù)端
  • Sec-WebSocket-Version 協(xié)議版本,常用13
  • Upgrade 升級至websocket協(xié)議

服務(wù)端返回消息:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-Websocket-Accept: dV84ft1FH/yq3Obi5LnPAUBLaas=
  • 狀態(tài)碼101 代表協(xié)議升級成功
  • ConnectionUpgrade 內(nèi)容代表協(xié)議成功升級為websocket
  • Sec-WebSocket-Version 代表協(xié)議版本號(hào),常用13
  • Sec-WebSocket-Accept計(jì)算方法偽代碼如下:
base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))

其中Sec-WebSocket-Key為客戶端傳入,258EAFA5-E914-47DA-95CA-C5AB0DC85B11為固定值。

2. 傳輸階段

Websocket的數(shù)據(jù)傳輸是frame形式傳輸?shù)模热鐣?huì)將一條消息分為幾個(gè)frame,按照先后順序傳輸出去。

websocket傳輸使用的協(xié)議如下圖:


參數(shù)說明如下:

  • FIN:1位,用來表明這是一個(gè)消息的最后的消息片斷,當(dāng)然第一個(gè)消息片斷也可能是最后的一個(gè)消息片斷;

  • RSV1, RSV2, RSV3: 分別都是1位,如果雙方之間沒有約定自定義協(xié)議,那么這幾位的值都必須為0,否則必須斷掉websocket連接;

  • Opcode: 4位操作碼,定義有效負(fù)載數(shù)據(jù),如果收到了一個(gè)未知的操作碼,連接也必須斷掉,以下是定義的操作碼:

    %x0 表示連續(xù)消息片斷
    %x1 表示文本消息片斷
    %x2 表未二進(jìn)制消息片斷
    %x3-7 為將來的非控制消息片斷保留的操作碼
    %x8 表示連接關(guān)閉
    %x9 表示心跳檢查的ping
    %xA 表示心跳檢查的pong
    %xB-F 為將來的控制消息片斷的保留操作碼
    
  • Mask: 1位,定義傳輸?shù)臄?shù)據(jù)是否有加掩碼,如果設(shè)置為1,掩碼鍵必須放在masking-key區(qū)域,客戶端發(fā)送給服務(wù)端的所有消息,此位的值都是1;

  • Payload length: 傳輸數(shù)據(jù)的長度,以字節(jié)的形式表示:7位、7+16位、或者7+64位。如果這個(gè)值以字節(jié)表示是0-125這個(gè)范圍,那這個(gè)值就表示傳輸數(shù)據(jù)的長度;如果這個(gè)值是126,則隨后的兩個(gè)字節(jié)表示的是一個(gè)16進(jìn)制無符號(hào)數(shù),用來表示傳輸數(shù)據(jù)的長度;如果這個(gè)值是127,則隨后的是8個(gè)字節(jié)表示的一個(gè)64位無符合數(shù),這個(gè)數(shù)用來表示傳輸數(shù)據(jù)的長度。多字節(jié)長度的數(shù)量是以網(wǎng)絡(luò)字節(jié)的順序表示。負(fù)載數(shù)據(jù)的長度為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)之和,擴(kuò)展數(shù)據(jù)的長度可能為0,因而此時(shí)負(fù)載數(shù)據(jù)的長度就為應(yīng)用數(shù)據(jù)的長度;

  • Masking-key: 0或4個(gè)字節(jié),客戶端發(fā)送給服務(wù)端的數(shù)據(jù),都是通過內(nèi)嵌的一個(gè)32位值作為掩碼的;掩碼鍵只有在掩碼位設(shè)置為1的時(shí)候存在;

  • Payload data: (x+y)位,負(fù)載數(shù)據(jù)為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)長度之和;

  • Extension data: x位,如果客戶端與服務(wù)端之間沒有特殊約定,那么擴(kuò)展數(shù)據(jù)的長度始終為0,任何的擴(kuò)展都必須指定擴(kuò)展數(shù)據(jù)的長度,或者長度的計(jì)算方式,以及在握手時(shí)如何確定正確的握手方式。如果存在擴(kuò)展數(shù)據(jù),則擴(kuò)展數(shù)據(jù)就會(huì)包括在負(fù)載數(shù)據(jù)的長度之內(nèi);

  • Application data: y位,任意的應(yīng)用數(shù)據(jù),放在擴(kuò)展數(shù)據(jù)之后,應(yīng)用數(shù)據(jù)的長度=負(fù)載數(shù)據(jù)的長度-擴(kuò)展數(shù)據(jù)的長度。

二、golang實(shí)現(xiàn)websocket簡單案例

package wss

import (
    "crypto/sha1"
    "encoding/base64"
    "encoding/binary"
    "errors"
    "fmt"
    "log"
    "math"
    "net"
    "net/http"
    "net/textproto"
    "strings"
)

type WsSocket struct {
    MaskingKey []byte
    Conn       net.Conn
}

func NewWsSocket(conn net.Conn) *WsSocket {
    return &WsSocket{Conn: conn}
}

// 讀取數(shù)據(jù)幀
func (ws *WsSocket) ReadIframe() (data []byte, opcode byte, err error) {
    err = nil
    // 第一個(gè)字節(jié):FIN + RSV1-3 + OPCODE
    opcodeByte := make([]byte, 1)
    ws.Conn.Read(opcodeByte)
    fin := opcodeByte[0] >> 7
    rsv1 := opcodeByte[0] >> 6 & 1
    rsv2 := opcodeByte[0] >> 5 & 1
    rsv3 := opcodeByte[0] >> 4 & 1
    opcode = opcodeByte[0] & 15 // 取出后四bit位
    log.Println(fin, rsv1, rsv2, rsv3, opcode)
    log.Println("opcode:", opcode)

    payloadLenByte := make([]byte, 1)
    ws.Conn.Read(payloadLenByte)
    // 取出mask位標(biāo)識(shí): 掩碼, 定義payload數(shù)據(jù)是否進(jìn)行了掩碼處理,如果是1表示進(jìn)行了掩碼處理
    mask := payloadLenByte[0] >> 7
    payloadLen := int(payloadLenByte[0] & 0x7F) // 0111 1111

    if payloadLen == 126 {
    // 讀取兩個(gè)字節(jié)
        extendedByte := make([]byte, 2)
        ws.Conn.Read(extendedByte)
        // 重置payloadLen,采用大端字節(jié)序
        payloadLen = int(binary.BigEndian.Uint16(extendedByte))
    }

    if payloadLen == 127 {
    // 讀取8個(gè)字節(jié)
        extendedByte := make([]byte, 8)
        ws.Conn.Read(extendedByte)
        // 重置payloadLen,采用大端字節(jié)序
        payloadLen = int(binary.BigEndian.Uint64(extendedByte))
    }
    // 掩碼鍵
    maskingByte := make([]byte, 4)
    if mask == 1 {
        ws.Conn.Read(maskingByte)
        ws.MaskingKey = maskingByte
    }

    payloadDataByte := make([]byte, payloadLen)
    ws.Conn.Read(payloadDataByte)
    log.Println("data:", payloadDataByte)
    dataByte := make([]byte, payloadLen)

    for i := 0; i < payloadLen; i++ {
        if mask == 1 {
            dataByte[i] = payloadDataByte[i] ^ maskingByte[i%4]
        } else {
            dataByte[i] = payloadDataByte[i]
        }
    }

    if fin == 1 {
        data = dataByte
        return
    }
    // 遞歸讀取數(shù)據(jù)
    nextData, opcode, err := ws.ReadIframe()
    if err != nil {
        return
    }
    data = append(data, nextData...)

    return
}

// websocket:發(fā)送數(shù)據(jù)幀(簡單版)
func (ws *WsSocket) SendIframe(data []byte) error {
    length := len(data)
    if length <= 0 {
        return errors.New("data cannot be empty")
    }

    //注意: 服務(wù)端發(fā)送數(shù)據(jù),不使用掩碼操作
    ws.Conn.Write([]byte{0x81}) // 1000 0001 : 前段部分表示FIN、RSV1-3,后半部分表示opcode:0X1(文本數(shù)據(jù)幀)
    switch  {
    case length <= 125:
        var payLenByte byte
        payLenByte = byte(0) | byte(length) //mask + payloadLength: mask位設(shè)置為0
        ws.Conn.Write([]byte{payLenByte})
    case length <= math.MaxUint16:
        // 處理126的情況
        ws.Conn.Write([]byte{0x7e}) // 01111110:  mask + payloadLength,mask設(shè)置為0,payloadLength為126
        // 2個(gè)字節(jié)
        buf := make([]byte, 2)
        binary.BigEndian.PutUint16(buf, uint16(length))  // 采用大端字節(jié)序
        ws.Conn.Write(buf)
    default:
        // 處理127的情況
        ws.Conn.Write([]byte{0x7f}) // 01111111:  mask + payloadLength,mask設(shè)置為0,payloadLength為127
        // 8個(gè)字節(jié)
        buf := make([]byte, 8)
        binary.BigEndian.PutUint64(buf, uint64(length)) // 采用大端字節(jié)序
        ws.Conn.Write(buf)
    }

    // 發(fā)送數(shù)據(jù)
    ws.Conn.Write(data)

    return nil
}

// 升級協(xié)議
func Upgrade(w http.ResponseWriter, r *http.Request) *WsSocket {
    errCode, err := verifyClientRequest(w, r)
    if err != nil {
        http.Error(w, err.Error(), errCode)
        return nil
    }

    // 劫持http,獲取底層TCP連接
    hj, ok := w.(http.Hijacker)
    if !ok {
        err = errors.New("http.ResponseWriter does not implement http.Hijacker")
        http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
        return nil
    }

    w.Header().Set("Upgrade", "websocket")
    w.Header().Set("Connection", "Upgrade")

    key := r.Header.Get("Sec-WebSocket-Key")
    w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key))

    w.WriteHeader(http.StatusSwitchingProtocols)

    netConn, _, err := hj.Hijack()
    if err != nil {
        err = fmt.Errorf("failed to hijack connection: %w", err)
        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        return nil
    }

    ws := NewWsSocket(netConn)

    return ws
}

// 驗(yàn)證請求header
func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) {
    if !r.ProtoAtLeast(1, 1) {
        return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
    }

    if !headerContainsToken(r.Header, "Connection", "Upgrade") {
        w.Header().Set("Connection", "Upgrade")
        w.Header().Set("Upgrade", "websocket")
        return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
    }

    if !headerContainsToken(r.Header, "Upgrade", "websocket") {
        w.Header().Set("Connection", "Upgrade")
        w.Header().Set("Upgrade", "websocket")
        return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade"))
    }

    if r.Method != "GET" {
        return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method)
    }

    if r.Header.Get("Sec-WebSocket-Version") != "13" {
        w.Header().Set("Sec-WebSocket-Version", "13")
        return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version"))
    }

    if r.Header.Get("Sec-WebSocket-Key") == "" {
        return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key")
    }

    return 0, nil
}

func headerContainsToken(h http.Header, key, token string) bool {
    token = strings.ToLower(token)

    for _, t := range headerTokens(h, key) {
        if t == token {
            return true
        }
    }
    return false
}

func headerTokens(h http.Header, key string) []string {
    key = textproto.CanonicalMIMEHeaderKey(key)
    var tokens []string
    for _, v := range h[key] {
        v = strings.TrimSpace(v)
        for _, t := range strings.Split(v, ",") {
            t = strings.ToLower(t)
            t = strings.TrimSpace(t)
            tokens = append(tokens, t)
        }
    }
    return tokens
}

var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
// 加密SecWebSocketKey
func secWebSocketAccept(secWebSocketKey string) string {
    h := sha1.New()
    h.Write([]byte(secWebSocketKey))
    h.Write(keyGUID)

    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
package main

import (
    "library/wss"
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        ws := wss.Upgrade(w, r)
        for {
            data, opcode, _ := ws.ReadIframe()
            fmt.Println("read data:", string(data))
            fmt.Println("opcode:", opcode)
            if opcode == 8 || len(data) == 0 {
                ws.Conn.Write([]byte{0x8})
                ws.Conn.Close()
                break
            }

            err := ws.SendIframe(data)
            if err != nil {
                log.Println("sendIframe err:", err)
            }
            log.Println("send data")
        }
    })

    log.Fatal(http.ListenAndServe(":2021", nil))
}

前端使用案例:(瀏覽器訪問: http://echo.localhost.com/wss.html,域名根據(jù)實(shí)際情況自定義配置)

<!DOCTYPE html>
<title>WebSocket Echo Client</title>
<h2>Websocket Echo Client</h2>
<div id="output"></div>
<script>
function setup() {
    output = document.getElementById("output");
    // 建立websocket連接
    ws = new WebSocket("ws://192.168.2.123:2021/ws");
    // 監(jiān)聽打開連接
    ws.onopen = function(e) {
        log("Connected");
        var msgObj = {content:"this is message"}
        sendMessage(JSON.stringify(msgObj))
    }
    // 監(jiān)聽關(guān)閉連接
    ws.onclose = function(e) {
        log("Disconnected: " + e.code);
    }
   // 監(jiān)聽錯(cuò)誤
    ws.onerror = function(e) {
        log("Error ");
    }
    // 監(jiān)聽消息
    ws.onmessage = function(e) {
        log("Message received: " + e.data);
       // ws.close();
    }
}
// 發(fā)送消息
function sendMessage(msg){
    ws.send(msg);
    log("Message sent");
}
function log(s) {
    var p = document.createElement("p");
    p.style.wordWrap = "break-word";
    p.textContent = s;
    output.appendChild(p);
    console.log(s);
}
setup();
</script>
</html>

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言:前段時(shí)間,在公司的項(xiàng)目中用到了WebSocket,當(dāng)時(shí)沒有時(shí)間好好整理。最近,趁著有時(shí)間,就好好梳理了一下W...
    齊舞647閱讀 4,198評論 0 16
  • WebSocket的實(shí)現(xiàn)原理 一、什么是websocket Websocket是應(yīng)用層第七層上的一個(gè)應(yīng)用層協(xié)議,它...
    yongfutian閱讀 50,761評論 3 19
  • 一、WebSocket 是什么?WebSocket是HTML5中的協(xié)議。HTML5 Web Sockets規(guī)范定義...
    何向宇閱讀 2,583評論 0 12
  • 基本知識(shí) WebSocket 是一種應(yīng)用層協(xié)議,基于TCP協(xié)議;WebSocket protocol 是HTML5...
    freelamb閱讀 1,928評論 0 2
  • 1 WebSocket 原理 1.1 背景 WebSocket 是基于Http 協(xié)議的改進(jìn),Http 為無狀態(tài)協(xié)議...
    赤兔歡閱讀 15,054評論 1 1

友情鏈接更多精彩內(nèi)容