分类 golang 下的文章

pinter_safe指针分析与运用


go unsafe pointer 指针 & c pointer 指针

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
在go里可以使用unsafe包 里的pinter 来达到 像c语言那样任意的操作指针和内存,因为go里的指针是有许多限制的
比如 go 里的结构体 有私有、公有属性 也就是可否导出的属性,go里的指针是不能直接操作私有属性的成员的,但是使用原始指针就可以实现

一、使用unsafe指针修改 结构体值

go 里正常的操作是访问结构体的字段来进行修改,而原始的c语言是可以通过指针地址的偏移量来修改值的

type Prog struct {
    name string
    aget string
}
func main(){
    p := Prog{"a","1"}
    //正常的修改方式如下
    p.name = "go"
    p.age = "2"
    fmt.Println(p) //{go 2 xxxx}
    
    /**
     *而基于c语言的通过内存地址偏移量来进行修改值如下
     */
     //name 指向的是结构体首地址 同时也是结构体第一个字段name的地址
    name := (*string)(unsafe.Pointer(&p)) //name 是字符串指针
    *name  = "pointer name"
    //这样就可以定位到age所在的内存地址    
    age := (*string)(unsafe.Pointer( uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))  
    *age = "pointer age"
    fmt.Println(p) //{pointer name pointer age xxxx}
    
}

内存分配的一个知识点 结构体地址 也是结构体首个成员的地址

内存地址是连续的 数组相邻的元素 可以通过指针位移 该元素字节大小 的位置来定位下一个元素

通过unsafe.Offsetof() 偏移量可以定位 具体的成员地址

二、@unsafe.Sizeof() 计算字节大小

上面定位结构体成员的方法时使用 unsafe.Offsetof()方法定位的,还可以用sizeof来计算,如下

    //相当于计算 出age成员 string类型的字节大小 然后在指针进行位移 就可以定位到age的地址 因为内存地址是连续的
    age := (*string)(unsafe.Pointer( uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string("")))) 
    *age = "pointer age"
    fmt.Println(p) //{pointer name pointer age xxxx}

三、go通过指针操作结构体私有成员

通过unsafe 包的指针直接越过go的 私有公有指针限制,操作结构体的私有成员,要保证该结构体不在当前包下

package test
type Pro struct {
    name string
    age int
    des string
}


//------ 现在我们要直接更改des的值
pcakge main
p := test.pro{"xiaodo",18,"do"}
//p.name = "do" go的指针直接操作是非法的,直接编译错误

des := (*string)( unsafe.Pointer( uintptr(unsafe.Pointer(&p) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(strint(""))    )  )
*des = "change"
这样就可以直接操作go 内存了

四、通过指针 零拷贝 转换 string slice

在网络编程中,基本上都是接受的byte 切片 如果采用默认的转换方式 string 转换 会发生多次内存拷贝,因此增加了负担

完成这个任务,我们需要了解 slice 和 string 的底层数据结构:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

进行转换

//也可以直接转换 return *(*[]byte)(unsafe.Pointer(&s))
func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len:  stringHeader.Len,
        Cap:  stringHeader.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&bh))
}

//也可以直接进行转换  return *(*string)(unsafe.Pointer(&b)))
func bytes2string(b []byte) string{
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }

    return *(*string)(unsafe.Pointer(&sh))
}

两种方式的压相差10倍的数量级

//采用默认的拷贝方式进行 转换
BenchmarkByteToStringDefault-4          300000000                5.38 ns/op            0 B/op          0 allocs/op
//采用指针o拷贝进行转换
BenchmarkByteToStringPointer-4          2000000000               0.30 ns/op            0 B/op          0 allocs/op

3-websocket协议实现


websocket链接的实现

编写基本的httpserver

启动一个基本的httpserver,提供两个接口,一个index返回主页,另一个是就是我们自定义的websocket协议接口

@main.go

package main

import (
    "html/template"
    "log"
    "net/http"
    "http/websocket"
)
//websocket处理器
func echo(w http.ResponseWriter, r *http.Request){
    //协议升级
    c,err := websocket.Upgrade(w,r)
    if err != nil {
        log.Print("Upgrade error:",err)
        return
    }
    defer c.Close()
    //循环处理数据,接受数据,然后返回
    for {
        message,err := c.ReadData()
        if err != nil {
            log.Println("read:",err)
            break
        }
        log.Printf("recv:%s",message)
        c.SendData(message)
    }
}

//index
func home(w http.ResponseWriter, r *http.Request){
    homeTemplate.Execute(w,"ws://"+r.Host+"/echo")
}

func main(){
    log.SetFlags(0)

    http.HandleFunc("/echo",echo)
    http.HandleFunc("/",home)
    log.Fatal(http.ListenAndServe("0.0.0.0:8000",nil))
}
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {

    var output = document.getElementById("output");
    var input = document.getElementById("input");
    var ws;

    var print = function(message) {
        var d = document.createElement("div");
        d.innerHTML = message;
        output.appendChild(d);
    };

    document.getElementById("open").onclick = function(evt) {
        if (ws) {
            return false;
        }
        ws = new WebSocket("{{.}}");
        ws.onopen = function(evt) {
            print("OPEN");
        }
        ws.onclose = function(evt) {
            print("CLOSE");
            ws = null;
        }
        ws.onmessage = function(evt) {
            print("RESPONSE: " + evt.data);
        }
        ws.onerror = function(evt) {
            print("ERROR: " + evt.data);
        }
        return false;
    };

    document.getElementById("send").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        print("SEND: " + input.value);
        ws.send(input.value);
        return false;
    };

    document.getElementById("close").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        ws.close();
        return false;
    };

});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>
点击 "Open" 开始一个新的WebSocket链接,
“Send" 将内容发送到服务器,
"Close" 将关闭链接。
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="input" type="text" value="hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))

index 返回我们自定义的一段html代码,

echo 接口进行升级我们的websocket

页面如下

index

自定义的webscoket upgrade进行升级

根据之前的协议分析,我知道握手的过程其实就是检查 HTTP 请求头部字段的过程,值得注意的一点就是需要针对客户端发送的 Sec-WebSocket-Key 生成一个正确的 Sec-WebSocket-Accept 只。关于生成的 Sec-WebSocket-Accpet 的实现,可以参考之前的分析。握手过程的具体代码如下:

@upgrade.go

package websocket

import(
    "net/http"
    "net"
    "errors"
    "log"
    "bufio"
)

func Upgrade(w http.ResponseWriter,r *http.Request)(c *Conn,err error){
    //是否是Get方法
    if r.Method != "GET" {
        http.Error(w,http.StatusText(http.StatusMethodNotAllowed),http.StatusMethodNotAllowed)
        return nil,errors.New("websocket:method not GET")
    }
    //检查 Sec-WebSocket-Version 版本
    if values := r.Header["Sec-Websocket-Version"];len(values) == 0 || values[0] != "13" {
        http.Error(w,http.StatusText(http.StatusBadRequest),http.StatusBadRequest)
        return nil,errors.New("websocket:version != 13")
    }

    //检查Connection 和  Upgrade
    if !tokenListContainsValue(r.Header,"Connection","upgrade") {
        http.Error(w,http.StatusText(http.StatusBadRequest),http.StatusBadRequest)
        return nil,errors.New("websocket:could not find connection header with token 'upgrade'")
    }
    if !tokenListContainsValue(r.Header,"Upgrade","websocket") {
        http.Error(w,http.StatusText(http.StatusBadRequest),http.StatusBadRequest)
        return nil,errors.New("websocket:could not find connection header with token 'websocket'")
    }


    //计算Sec-Websocket-Accept的值
    challengeKey := r.Header.Get("Sec-Websocket-Key")
    if challengeKey == "" {
        http.Error(w,http.StatusText(http.StatusBadRequest),http.StatusBadRequest)
        return nil,errors.New("websocket:key missing or blank")
    }

    var (
        netConn net.Conn
        br *bufio.Reader
    )
    h,ok := w.(http.Hijacker)
    if  !ok {
        http.Error(w,http.StatusText(http.StatusInternalServerError),http.StatusInternalServerError)
        return nil,errors.New("websocket:response dose not implement http.Hijacker")
    }
    var rw *bufio.ReadWriter
    //接管当前tcp连接,阻止内置http接管连接
    netConn,rw,err = h.Hijack()
    if err != nil {
        http.Error(w,http.StatusText(http.StatusInternalServerError),http.StatusInternalServerError)
        return nil,err
    }

    br = rw.Reader
    if br.Buffered() > 0 {
        netConn.Close()
        return nil,errors.New("websocket:client send data before hanshake is complete")
    }
    // 构造握手成功后返回的 response
    p := []byte{}
    p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
    p = append(p, computeAcceptKey(challengeKey)...)
    p = append(p, "\r\n\r\n"...)
    //返回repson 但不关闭连接
    if _,err = netConn.Write(p);err != nil {
        netConn.Close()
        return nil,err
    }
    //升级为websocket
    log.Println("Upgrade http to websocket successfully")
    conn := newConn(netConn)
    return conn,nil
}

握手过程的代码比较直观,就不多做解释了。到这里 WebSocket 的实现就基本完成了,可以看到有了之前的各种约定,我们实现 WebSocket 协议也是比较简单的。

封装的websocket结构体和对应的方法

@conn.go

package websocket

import (
    "fmt"
    "encoding/binary"
    "log"
    "errors"
    "net"
)

const (
    /*
    * 是否是最后一个数据帧
    * Fin Rsv1 Rsv2 Rsv3 Opcode
    *  1  0    0    0    0 0 0 0  => 128
    */
    finalBit = 1 << 7

    /*
    * 是否需要掩码处理
    *  Mask payload-len 第一位mask表示是否需要进行掩码处理 后面
    *  7位表示数据包长度 1.0-125 表示长度 2.126 后面需要扩展2 字节 16bit
    *  3.127则扩展8bit
    *  1    0 0 0 0 0 0 0  => 128
    */
    maskBit = 1 << 7

    /*
    * 文本帧类型
    * 0 0 0 0 0 0 0 1
    */
    TextMessage = 1
    /*
    * 关闭数据帧类型
    * 0 0 0 0 1 0 0 0
    */
    CloseMessage = 8
)

//websocket 连接
type Conn struct {
    writeBuf []byte
    maskKey [4]byte
    conn net.Conn
}
func newConn(conn net.Conn)*Conn{
    return &Conn{conn:conn}
}
func (c *Conn)Close(){
    c.conn.Close()
}

//发送数据
func (c *Conn)SendData(data []byte){
    length := len(data)
    c.writeBuf = make([]byte,10 + length)

    //数据开始和结束位置
    payloadStart := 2
    /**
    *数据帧的第一个字节,不支持且只能发送文本类型数据
    *finalBit 1 0 0 0 0 0 0 0
    *                |
    *Text     0 0 0 0 0 0 0 1
    * =>      1 0 0 0 0 0 0 1
    */
    c.writeBuf[0] = byte(TextMessage) | finalBit
    fmt.Printf("1 bit:%b\n",c.writeBuf[0])

    //数据帧第二个字节,服务器发送的数据不需要进行掩码处理
    switch{
    //大于2字节的长度
    case length >= 1 << 16 ://65536
        //c.writeBuf[1] = byte(0x00) | 127 // 127
        c.writeBuf[1] = byte(127) // 127
        //大端写入64位
        binary.BigEndian.PutUint64(c.writeBuf[payloadStart:],uint64(length))
        //需要8byte来存储数据长度
        payloadStart += 8
    case length > 125:
        //c.writeBuf[1] = byte(0x00) | 126
        c.writeBuf[1] = byte(126)
        binary.BigEndian.PutUint16(c.writeBuf[payloadStart:],uint16(length))
        payloadStart += 2
    default:
        //c.writeBuf[1] = byte(0x00) | byte(length)
        c.writeBuf[1] = byte(length)
    }
    fmt.Printf("2 bit:%b\n",c.writeBuf[1])

    copy(c.writeBuf[payloadStart:],data[:])
    c.conn.Write(c.writeBuf[:payloadStart+length])
}

//读取数据
func (c *Conn)ReadData()(data []byte,err error){
    var b [8]byte
    //读取数据帧的前两个字节
    if _,err := c.conn.Read(b[:2]); err != nil {
        return nil,err
    }
    //开始解析第一个字节 是否还有后续数据帧
    final := b[0] & finalBit != 0
    fmt.Printf("read data 1 bit :%b\n",b[0])
    //不支持数据分片
    if !final {
        log.Println("Recived fragemented frame,not support")
        return nil,errors.New("not suppeort fragmented message")
    }

    //数据帧类型
    /*
    *1 0 0 0  0 0 0 1
    *        &
    *0 0 0 0  1 1 1 1
    *0 0 0 0  0 0 0 1
    * => 1 这样就可以直接获取到类型了
    */
    frameType := int(b[0] & 0xf)
    //如果 关闭类型,则关闭连接
    if frameType == CloseMessage {
        c.conn.Close()
        log.Println("Recived closed message,connection will be closed")
        return nil,errors.New("recived closed message")
    }
    //只实现了文本格式的传输,编码utf-8
    if frameType != TextMessage {
        return nil,errors.New("only support text message")
    }
    //检查数据帧是否被掩码处理
    //maskBit => 1 0 0 0 0 0 0 0 任何与他 要么为0 要么为 128
    mask := b[1] & maskBit != 0
    //数据长度
    payloadLen := int64(b[1] & 0x7F)//0 1 1 1 1 1 1 1 1 127
    dataLen := int64(payloadLen)
    //根据payload length 判断数据的真实长度
    switch payloadLen {
    case 126://扩展2字节
        if _,err := c.conn.Read(b[:2]);err != nil {
            return nil,err
        }
        //获取扩展二字节的真实数据长度
        dataLen = int64(binary.BigEndian.Uint16(b[:2]))
    case 127 :
        if _,err := c.conn.Read(b[:8]);err != nil {
            return nil,err
        }
        dataLen = int64(binary.BigEndian.Uint64(b[:8]))
    }

    log.Printf("Read data length :%d,payload length %d",payloadLen,dataLen)
    //读取mask key
    if mask {//如果需要掩码处理的话 需要取出key
        //maskKey 是 4 字节  32位
        if _,err := c.conn.Read(c.maskKey[:]);err != nil {
        return nil ,err
        }
    }
    //读取数据内容
    p := make([]byte,dataLen)
    if _,err := c.conn.Read(p);err != nil {
        return nil,err
    }
    if mask {
        maskBytes(c.maskKey,p)//进行解码
    }
    return p,nil
}

http 头部检查

import (
    "crypto/sha1"
    "encoding/base64"
    "strings"
    "net/http"
)


var KeyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
//握手阶段使用 加密key返回 进行握手
func computeAcceptKey(challengeKey string)string{
    h := sha1.New()
    h.Write([]byte(challengeKey))
    h.Write(KeyGUID)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

//解码
func maskBytes(key [4]byte,b []byte){
    pos := 0
    for i := range b {
        b[i] ^= key[pos & 3]
        pos ++
    }
}

// 检查http 头部字段中是否包含指定的值
func tokenListContainsValue(header http.Header, name string, value string)bool{
    for _,v := range header[name] {
        for _, s := range strings.Split(v,","){
            if strings.EqualFold(value,strings.TrimSpace(s)) {
                return true
            }
        }
    }
    return false
}

2-websocket协议算法


websocket协议中的一些算法

在分析 WebSocket 协议握手过程和数据帧格式过程中,我们讲到了一些算法,下面我们讲解下具体实现。

Sec-WebSocket-Accept的计算方法

从上面的分析中,我们知道字段的值是通过固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11加上请求中Sec-WebSocket-Key字段的值,然后再对其结果通过 SHA1 哈希算法求出的结果。可以通过以下 golang 代码实现:

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))
}

掩码处理

浏览器发送给服务器的数据帧是经过掩码处理的,那怎么样对数据进行解码呢?以下是来自RFC6455文档的解释

具体的流程是:将传输的数据按字节 byte 处理,同时将 Masking-key 代表的值也按字节处理。假如 data-byte-i 代表的是数据的第 i 个字节,那么 j = i MOD 4,然后从Maksing-key中(一共有 4 个字节)取出第 j 个字节 mask-key-byte-j,然后将 data-byte-imask-key-byte-j 代表的字节进行异或操作,取得结果就是最终的结果。该操作可以用如下 golang 代码实现:

func maskBytes(key [4]byte,pos int,b[]byte)int{
    for i := range b{
        b[i] ^= key[pos & 3]
        pos++
    }
    return pos & 3
}

注意以上的操作,pos & 3这里代表的操作是pos%4,因为 a % (2 ^ n) 等价于 a & (2^n -1),在这里使用按位与操作更加高效


1-websocket协议分析


WebSocket协议详解

WebSocket 协议解决了浏览器和服务器之间的全双工通信问题。在 WebSocket 出现之前,浏览器如果需要从服务器及时获得更新,则需要不停的对服务器主动发起请求,也就是 Web 中常用的poll技术。这样的操作非常低效,这是因为每发起一次新的 HTTP 请求,就需要单独开启一个新的 TCP 链接,同时 HTTP 协议本身也是一种开销非常大的协议。为了解决这些问题,所以出现了 WebSocket 协议。WebSocket 使得浏览器和服务器之间能通过一个持久的 TCP 链接就能完成数据的双向通信。关于 WebSocket 的 RFC 提案,可以参看RFC6455。

WebSocket 和 HTTP 协议一般情况下都工作在浏览器中,但 WebSocket 是一种完全不同于 HTTP 的协议。尽管,浏览器需要通过 HTTP 协议的GET请求,将 HTTP 协议升级为 WebSocket 协议。升级的过程被称为握手(handshake)。当浏览器和服务器成功握手后,则可以开始根据 WebSocket 定义的通信帧格式开始通信了。像其他各种协议一样,WebSocket 协议的通信帧也分为控制数据帧和普通数据帧,前者用于控制 WebSocket 链接状态,后者用于承载数据。下面我们将一一分析 WebSocket 协议的握手过程以及通信帧格式。

一、websocket握手

握手的过程也就是将HTTP协议升级为WebSocket协议的过程,握手开始首先由浏览器端发送一个GET请求,该请求的HTTP头部信息如下:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protcol: chat, superchat
        Sec-WebSocket-Version: 13

当服务器端,成功验证了以上信息后,则会返回一个形如以下的响应:

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

可以看到,浏览器发送端HTTP请求中,增加了一些新端字段,其作用如下所示:

  • Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
  • Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
    Sec-WebSocket-Key: 必需字段,一个随机的字符串;

Sec-WebSocket-Protocol: 可选字段,可以用于标识应用层的协议;
Sec-WebSocket-Version: 必需字段,代表了 WebSocket 协议版本,值必需是 13, 否则握手失败;

返回端响应中,如果握手成功会返回状态码101的HTTP响应,同时其他字段说明如下:

  • Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
  • Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
  • Sec-WebSocket-Accept: 规定必需的字段,该字段的值是通过固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11加上请求中Sec-WebSocket-Key字段的值,然后再对其结果通过 SHA1 哈希算法求出的结果。
  • Sec-WebSocket-Protocol: 对应于请求中的 Sec-WebSocket-Protocol 字段;

当浏览器和服务端成功握手后,就可以传递数据了,传送数据是按照WebSocket的数据格式生成的

二、WebSocket协议数据帧

数据帧的定义类似与TCP/IP的格式定义,具体看下图:

      0                   1                   2                   3
      0 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 ...                |
     +---------------------------------------------------------------+

以上这张图,一行代表32bit(位),也就是4bytes(字节),总体上包含两份,帧头部和数据内容。每个从WebSocket链接中接受到的数据帧,都要按照以上格式进行解析,这样才能知道该数据帧是用于控制的还是用于传送数据的,关于以上数据帧的各个比特位的解释如下:

  • FIN:1bit,当该比特位值为%x0时,表示后面还有更多的数据帧,%x1时表示这是最后一个数据帧;
  • RSV1,RSV2,RSV3:各占1个比特位。一般情况下全为0,当客户端、服务端协商采用WebSocket扩展时,这三个标识位可以非0,且值当含义由扩展进行定义,如果出现非0当值,且没有采用WebSocket扩展,则链接出错
  • opcode:4 bit,用于表明数据帧当类型,一共可以表示16种帧类型,如下所示:

    • %x0:表示这是一个分片当帧,它属于前面帧当后续帧;
    • %x1:表示该数据帧携带的数据类型是文本类型,且编码utf-8
    • %x2 : 表示携带的是二进制数据;
    • %x3-7 : 保留未使用;
    • %x8 : 表示该帧用于关闭 WebSocket 链接;
    • %x9 : 表示该帧代表了 ping 操作;
    • %xA : 表示该帧代表了 pong 回应;
    • %xB-F : 保留未使用;
  • MASK:1 bit,%x0表示数据帧没有经过掩码计算,而%x1则表示数据帧已经经过掩码计算,得到真正当数据需要解码,一般情况下,只有浏览器发送给服务端当数据帧才需要进行掩码计算;
  • Payload len:7 bit,表示了数据帧携带当数据长度,7 bit 的值根据三种情况,帧的解析有所不同:

    • %x0 - 7D : 也就是从 0 到 125,表示数据长度, 数据总长度也就是 7 bit 代表的长度;
    • %x7E : 7 bit 的值是 126 时,则后续的 2 个字节(16 bit)表示的一个 16 位无符号数,这个数用来表示数据的长度;
    • %x7F : 7 bit 的值是 127 时,则后续的 8 个字节(64 bit)表示的一个 64 位无符号数,这个数用来表示数据的长度;
    • Masking-key: 32 bit, 表示了用于解码的 key,只有当 MASK 比特位的值为 %x1 是,才有该数据;
  • Payload Data: 余下的比特位用于存储具体的数据;

通过以上分析可以看出,WebSocket 协议数据帧的最大头部为 2 + 8 + 4 = 14 bytes 也就是 14 个字节。同时我们要实现 WebSocket 协议,最主要的工作就是实现对数据帧的解析。