WebSocket


什么是WebSocket

WebSocket是一种网络传输协议,可以在单个TCP连接上进行全双工通信,位于OSI模型的应用层.

特点如下:

  • 允许服务端主动向客户端推送数据
  • 在HTTP端口80和443工作,支持HTTP代理和中介
  • WebSocket使用ws:// 或者 wss:// 表示,后者是使用了TLS的WebSocket
  • 默认情况下,WebSocket使用80端口;运行在TLS上时,用的是443端口

背景是:之前要实现服务端推送,一般方法是轮询,但是轮询比较耗费资源和带宽.

握手协议

WebSocket是独立的,创建在TCP上的协议

WebSocket通过HTTP/1.1 协议的101状态码进行握手

为了创建WebSocket连接,需要浏览器发出请求,服务器响应,然后建立连接,这个过程叫做握手

只能使用HTTP GET方法

请求头

GET ws://123.207.136.134:9010/ajaxchattest HTTP/1.1
Host: 123.207.136.134:9010
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36
Upgrade: websocket
Origin: http://coolaf.com
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Sec-WebSocket-Key: HCcmZqRqFs9xKT0mNie1yg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

响应头

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: /tsJD4thfwT/XAudprSdZLpVFzU=

可以看到,与普通HTTP请求相比,多了下面这几个header:

Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: HCcmZqRqFs9xKT0mNie1yg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13

服务端返回Header则多了:

Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: /tsJD4thfwT/XAudprSdZLpVFzU=

其中Upgrade: websocketConnection: Upgrade是RFC中规定写死的.

Sec-WebSocket-Version一般都是等于13

Sec-WebSocket-Key

一个客户端随机生成的base64字符串.

func generateChallengeKey() (string, error) {
    p := make([]byte, 16)
    if _, err := io.ReadFull(rand.Reader, p); err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(p), nil
}

Sec-WebSocket-Accept

计算规则如下:

  1. 拼接 Sec-WebSocket-Key 和 WebSocket规定的GUID值(RFC中规定为258EAFA5-E914-47DA-95CA-C5AB0DC85B11)
  2. 使用SHA-1摘要算法进行加密,通常是40个16进制数
  3. 进行Base64编码
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))
}

服务端响应之后,客户端会进行校验,如果状态码和Sec-WebSocket-Key/Sec-WebSocket-Accept对应上,则握手成功,后面会进行数据传递了.

数据传输

WebSocket使用Data Frame为单位进行数据传输的.由一个或者多个Data Frame组成一个完整的消息.

DataFrame

      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 ...                |
     +---------------------------------------------------------------+

上面这个是Data Frame的格式,下面来具体看一下Data Frame的格式:

  • FIN

    1 bit;

    表示Frame是否为消息的最后一个片段(fragment),即是否为最后一帧.

    如果是0,表示不是最后一帧;如果是1表示是最后一帧

  • RSV1/RSV2/RSV3

    各占 1 bit;

    除非协商了一个extension(定义了非0值的意义),否则这三个值都必须为0;

    如果这三个中出现了非0值,但是缺少了对应的extension,websocket连接失败.

  • Opcode

    4 bit,最多可以表示16种操作码,但是目前并没有用到这么多;

    用于解释Payload数据,即Payload数据的类型,便于程序对不同的Payload数据做出对应的解析.

    • %x0 : 表示这是一个连续帧,当Opcode = 0时,表示本次数据采用了数据分片,档案收到的数据帧为其中一个数据分片
    • %x1 : 表示这是一个文本帧
    • %x2: 表示这是一个二进制帧
    • %x8 : 表示连接断开
    • %x9 : 表示这是一个Ping操作 (用于心跳机制)
    • %xA : 表示这是一个pong操作 (用于心跳机制)

    这里的“%x”表示的是16进制

  • Mask

    1 bit;

    表示是否要对Payload数据进行掩码操作.

    从客户端向服务端发送数据时,需要对数据进行掩码操作,如果服务端接收到的数据没有经过掩码操作,则断开连接;

    从服务端向客户端发送数据时,不需要对数据进行掩码操作;

    也就是说,所有从客户端向服务端发送的Frame的Mask都为1.

    如果这里Mask设置为1,则后面的Masking-key会有对应的数据.否则masking-key没有数据为空.

    Masking-key就是为了对Payload数据进行反掩码操作.

  • Payload len 和 Extended Payload length

    7 bit / 7 + 16 bit / 7 + 64 bit,即Payload len为7bit, Extended payload length 可能为0bit,16bit,或者64bit.

    Payload len的单位是字节,这是什么意思呢?

    7bit以十进制无符号整数表示最大为127, 比如这里7个bit的payload len表示十进制数 12,则表示payload数据有12byte.

    但是这里显然不是这么简单,要不然也没必要使用Extended payload length了.

    假设 x 表示 payload len 7bit数代表的无符号整数,则

    • x 在 [0,125]范围内是, payload数据长度为x字节
    • x = 126,则extension payload length为16bit,这16bit换算成一个无符号的整数为 y,则payload数据长度为y字节
    • x = 127,则extension payload length 为64未,这 64bit换算成一个无符号整数为y,则payload数据长度为y字节.

    如果extension payload length不为空,则二进制表达采用的是BigEndian,即大端序,一个个字节为最高位字节.

    Payload数据长度 = extension data + application data的长度,这里extension data的长度可以为0.

  • Masking-key

    0 或者 32 bit.

    前面说过,如果mask = 1,则这里Masking-key有值,为一个32bit的数.

    如果mask = 0,则没有Masking-key.

    这里的Masking-key就是一个随机生成的32未随机数.掩码操作不会影响payload的长度.

    掩码算法/反掩码算法如下:

    原始第x个字节,原始字节数组为A,掩码之后的字节数组为T,则有

    T[x] = A[x] XOR MaskingKey[x mod 4] 

    掩码操作并不保证安全性.

  • Payload data

    Payload data = extension data + application data.

    拓展数据: 如果没有协商使用拓展(前面的RSV1/RSV2/RSV3),则拓展数据为0字节;所有拓展都必须告知拓展数据的长度,或者可以如何计算出拓展数据的长度.拓展数据必须在握手阶段之前就协商好.

    应用数据: 任意的应用数据,都在拓展数据之后,占据了数据剩余帧的位置.payload length - 拓展数据长度就是应用数据的长度了.

心跳机制

上述Opcode中定义的Ping/Pong,其实就是为了提供心跳机制的.

为了确认客户端和服务端之间的连接可用,需要定时发送心跳,用于确保连接可正常使用和在连接异常的时候断开连接节省资源和重新连接.

谁来发送Ping,谁来发送Pong,没有特别的限制.

如果客户端支持Ping,那么客户端发送Ping,服务端尽可能快的响应pong.

但是浏览器目前不支持Ping操作,因此由服务端发送Ping,浏览器响应Pong.

可以在两端设置超时机制,过了指定的时间如果还没收到ping/pong,则断开连接或者重新连接.

数据分片

就是将消息分割成多个数据帧.

根据上面的说明,其实就是根据FIN和Opcode进行判断.

  • 不分片

    FIN = 1, Opcode = %x1 或者 %x2

  • 分片

    第一针: FIN = 0, Opcode = %x1 或者 %x2

    第二帧: FIN = 0, Opcode = %x0

    第三帧: FIN = 1, Opcode = %x0

至于为什么是这样的,如果仔细看了上面关于数据帧协议说明这部分,很容易理解.

使用场景有哪些

websocket是用于C/S双向通信的,可以想象这样的场景有哪些?

  • 推送
  • 聊天
  • 订阅
  • 多玩家游戏
  • 协同编辑
  • 数据统计
  • 股票价格实时更新
  • 体育实况
  • 基于位置的应用
  • 在线教育

客户端如何使用

Android

val okHttpClient = OkHttpClient()
        val request = Request.Builder()
                .url("ws://echo.websocket.org")
                .build()
        okHttpClient.newWebSocket(request,object : WebSocketListener(){

            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                ToastUtils.showShortToast("OnOpen")
                webSocket.send("Hello...");
                webSocket.send("...World!");
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                super.onMessage(webSocket, text)
                ToastUtils.showShortToast(text)
            }

            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                super.onMessage(webSocket, bytes)
                ToastUtils.showShortToast(bytes.toString())
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosed(webSocket, code, reason)
                ToastUtils.showShortToast("OnClosed")
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                super.onFailure(webSocket, t, response)
                ToastUtils.showShortToast("OnFailure: ${response?.message},${t.message}")
            }

        })

Javascript

var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
    console.log('server: receive connection.');
    ws.on('message', function incoming(message) {
        console.log('server: received: %s', message);
    });
    ws.send('world');
});
app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html');
});
app.listen(3000);

服务端如何使用

Golang

package main

import (
    "flag"
    "fmt"
    "github.com/gorilla/websocket"
    "log"
    "net/http"
)

var upgrader = websocket.Upgrader{} // use default options

func echo(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        c.WriteJSON("received data successfully!")
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}


func main() {
    flag.Parse()
    log.SetFlags(0)
    http.HandleFunc("/", echo)
    fmt.Println("websocket : ws://localhost:80")
    fmt.Println("push : http://localhost:80/push")
    log.Fatal(http.ListenAndServe(":80", nil))
}

测试地址

  1. http://websocket.org/echo.html

参考文章

  1. https://www.rfc-editor.org/rfc/rfc6455.txt

文章作者: 姜康
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 姜康 !
评论
 上一篇
浅谈TLS 浅谈TLS
基本概念TLS协议的目的就是为网络通信的两端提供一个安全的通道. 具有以下特性: 身份认证 确保交换信息的各方是他们声称的身份 加密 通道上传输的数据仅对两个端点可见. 不过TLS默认并不隐藏传输数据的长度,但是两端可以向TLS记录中填
2020-11-29
下一篇 
Dart中的Isolate Dart中的Isolate
基本概念每个Isolate有自己的内存和EventLoop.不同的Isolate只能通过传递消息来进行通信. Dart的Isolate没有内存共享机制,这样设计有一个好处,就是在处理内存分配和回收时,无需加锁,因为仅一个线程,并不会抢占.
2020-11-25
  目录