Golang实现WebSocket协议

一、什么是websocket

Websocket是一个应用层协议,它必须依赖HTTP协议进行一次握手,握手成功后,数据直接从TCP通道传输,此时就与HTTP无关了。所以websocket分为握手和数据传输两个阶段。

1. 握手阶段

客户端发送消息:

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 告诉服务端对协议进行升级,具体升级内容取决于 Upgrade部分
  • Sec-WebSocket-Key 为了保证握手一致性,由客户端生成随机字符串并base64编码,发送给服务端
  • Sec-WebSocket-Version 协议版本,常用13
  • Upgrade 升级至websocket协议

服务端返回消息:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-Websocket-Accept: dV84ft1FH/yq3Obi5LnPAUBLaas=
  • 状态码101 代表协议升级成功
  • ConnectionUpgrade 内容代表协议成功升级为websocket
  • Sec-WebSocket-Version 代表协议版本号,常用13
  • Sec-WebSocket-Accept计算方法伪代码如下:
base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))

其中Sec-WebSocket-Key为客户端传入,258EAFA5-E914-47DA-95CA-C5AB0DC85B11为固定值。

2. 传输阶段

Websocket的数据传输是frame形式传输的,比如会将一条消息分为几个frame,按照先后顺序传输出去。

websocket传输使用的协议如下图:


参数说明如下:

  • FIN:1位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断;

  • RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉websocket连接;

  • Opcode: 4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:

    %x0 表示连续消息片断
    %x1 表示文本消息片断
    %x2 表未二进制消息片断
    %x3-7 为将来的非控制消息片断保留的操作码
    %x8 表示连接关闭
    %x9 表示心跳检查的ping
    %xA 表示心跳检查的pong
    %xB-F 为将来的控制消息片断的保留操作码
    
  • Mask: 1位,定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;

  • Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度;

  • Masking-key: 0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在;

  • Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和;

  • Extension data: x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内;

  • Application data: y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。

二、golang实现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}
}

// 读取数据帧
func (ws *WsSocket) ReadIframe() (data []byte, opcode byte, err error) {
    err = nil
    // 第一个字节: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位标识: 掩码, 定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理
    mask := payloadLenByte[0] >> 7
    payloadLen := int(payloadLenByte[0] & 0x7F) // 0111 1111

    if payloadLen == 126 {
    // 读取两个字节
        extendedByte := make([]byte, 2)
        ws.Conn.Read(extendedByte)
        // 重置payloadLen,采用大端字节序
        payloadLen = int(binary.BigEndian.Uint16(extendedByte))
    }

    if payloadLen == 127 {
    // 读取8个字节
        extendedByte := make([]byte, 8)
        ws.Conn.Read(extendedByte)
        // 重置payloadLen,采用大端字节序
        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
    }
    // 递归读取数据
    nextData, opcode, err := ws.ReadIframe()
    if err != nil {
        return
    }
    data = append(data, nextData...)

    return
}

// websocket:发送数据帧(简单版)
func (ws *WsSocket) SendIframe(data []byte) error {
    length := len(data)
    if length <= 0 {
        return errors.New("data cannot be empty")
    }

    //注意: 服务端发送数据,不使用掩码操作
    ws.Conn.Write([]byte{0x81}) // 1000 0001 : 前段部分表示FIN、RSV1-3,后半部分表示opcode:0X1(文本数据帧)
    switch  {
    case length <= 125:
        var payLenByte byte
        payLenByte = byte(0) | byte(length) //mask + payloadLength: mask位设置为0
        ws.Conn.Write([]byte{payLenByte})
    case length <= math.MaxUint16:
        // 处理126的情况
        ws.Conn.Write([]byte{0x7e}) // 01111110:  mask + payloadLength,mask设置为0,payloadLength为126
        // 2个字节
        buf := make([]byte, 2)
        binary.BigEndian.PutUint16(buf, uint16(length))  // 采用大端字节序
        ws.Conn.Write(buf)
    default:
        // 处理127的情况
        ws.Conn.Write([]byte{0x7f}) // 01111111:  mask + payloadLength,mask设置为0,payloadLength为127
        // 8个字节
        buf := make([]byte, 8)
        binary.BigEndian.PutUint64(buf, uint64(length)) // 采用大端字节序
        ws.Conn.Write(buf)
    }

    // 发送数据
    ws.Conn.Write(data)

    return nil
}

// 升级协议
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
}

// 验证请求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,域名根据实际情况自定义配置)

<!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");
    // 监听打开连接
    ws.onopen = function(e) {
        log("Connected");
        var msgObj = {content:"this is message"}
        sendMessage(JSON.stringify(msgObj))
    }
    // 监听关闭连接
    ws.onclose = function(e) {
        log("Disconnected: " + e.code);
    }
   // 监听错误
    ws.onerror = function(e) {
        log("Error ");
    }
    // 监听消息
    ws.onmessage = function(e) {
        log("Message received: " + e.data);
       // ws.close();
    }
}
// 发送消息
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>

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容