美文网首页
Golang实现WebSocket协议

Golang实现WebSocket协议

作者: echo1028 | 来源:发表于2021-03-25 14:04 被阅读0次

一、什么是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>

参考链接

相关文章

  • Golang实现WebSocket协议

    一、什么是websocket Websocket是一个应用层协议,它必须依赖HTTP协议进行一次握手,握手成功后,...

  • 在 go 中实现 websocket 服务

    目标:了解 websocket ,能够使用 golang 来实现 websocket 服务 要求:了解 go 基本...

  • 基于koa的前后端分离的socket.io使用

    1、websocket websocket是html5出的协议,它是基于TCP协议,利用http协议建立连接,实现...

  • 2018-10-09 WebSocket通信过程与实现

    来源:WebSocket通信过程与实现 什么是 WebSocket ? WebSocket 是一种标准协议,用于在...

  • WebSocket协议

    参考 WebSocket协议分析及实现 WebSocket 是什么原理?为什么可以实现持久连接? RFC6455 ...

  • Golang实现WebSocket

    酝酿情绪中······ 相关链接:WebSocket协议分析RFC 6455

  • Laya WebSocket

    WebSocket 是基于 TCP 的协议,使Html5可以实现进行全双工通讯的协议。websocket 常见于浏...

  • socket与WebSocket协议

    本文整理了对Socket与WebSocket协议的理解,基于WebSocket聊天室的实现及实现原理,Worker...

  • 足球即时比分 实时赔率

    bet365网站的数据是通过websocket协议实现数据的实时更新,所以需要通过websocket协议来建立客户...

  • websocket协议

    WebSocket协议是基于TCP的一种网络协议;WebSocket的诞生是因为http不能长时间实现双工通信是通...

网友评论

      本文标题:Golang实现WebSocket协议

      本文链接:https://www.haomeiwen.com/subject/daoxhltx.html