# 四.websocket

前言

  • WebSocket_API 规范定义了一个 API 用以在网页浏览器和服务器建立一个 socket 连接。通俗地讲:在客户端和服务器之间有一个持久的连接,两边可以在任意时间开始发送数据。
  • html5 开始提的一种浏览器与服务器进行全双工的网络技术
  • 属于应用层协议,它基于 tcp 传输协议,并复用 http 的握手通道。

# 1.websocket 优势

  • 支持双向通信,实时性更强
  • 更好的二进制支持
  • 较少的控制开销,连接创建后,ws 客户端、服务端进行数据交换时,协议控制数据包头部较小。

# 2.websocket 实战

# 2.1 客户端代码

{ "usename": "zbc", "password": "123" }
<template>
  <div>{{ text }}</div>
</template>

<script>
export default {
  data() {
    return {
      text: { usename: "zbc", password: "123" },
    }
  },
  mounted() {
    let ws = new WebSocket("ws://localhost:8090/ws/socket")
    let that = this
    ws.onopen = function() {
      console.log("客户端连接成功")
      ws.send(JSON.stringify({ type: "login", payload: this.text }))
    }
    ws.onmessage = function(event) {
      that.text = event.data
      console.log("收到服务器端的响应", event.data)
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
显示代码 复制代码

# 2.2 服务端代码

let express = require("express")
let app = express()
app.use(express.static(__dirname))
app.listen(3000, () => {
  console.log(3000)
})
let WebSocketServer = require("ws").Server
let wsServer = new WebSocketServer({ port: 8888 })
wsServer.on("connection", (socket) => {
  console.log("客户端连接成功")
  socket.on("message", (message) => {
    console.log("接受客户端的消息", message)
    setInterval(function() {
      socket.send("服务器已收到:" + new Date().toLocaleString())
    }, 1000)
    socket.send("服务器回应" + message)
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.如何建立连接

  • websocket 复用了 http 的握手通道。具体指的是,客户端通过 http 请求与 websocket 服务端协商升级协议。协议升级完成后,后续的数据交换则遵照 websocket 的协议。

# 1.客户端:申请协议升级

  • 首先,客户端发起协议升级请求。可以看到,采用的是标准的 http 格式,并且只支持 get 方法。
GET ws://localhost:8888/HTTP/1.1
Host:localhost:监听8888端口
Connection:Upgrade //表示要升级协议
Sec-WebSocket-Version:13 //表示 websocket 的版本
Sec-WebSocket-Key: xl0sD4LsfBfipcm8jULjYA== //与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接
1
2
3
4
5

# 2 服务端:响应协议升级

  • 服务端返回内容如下,状态代码 101 表示协议切换。到此完成协议升级,后续的交互按照新的协议来。
HTTP/1.1 101 Switching Protocols
Upgrade:websocket //表示要升级到 websocket 协议
Connection:Upgrade
Sec-WebSocket-Accept:QVi4FvBm2f5rkYzKe4E4ObfpDOE=
1
2
3
4

# 3.Sec-WebSocket-Accept 的计算

# Sec-WebSocket-Accept 根据客户端请求首部的 Sec-WebSocket-Key 计算出来。计算公式为:

  • 将 Sec-WebSocket-Key 和 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
  • 通过 SHA1 计算出摘要,并转成 base64 字符串。
const crypto=require('crypto')
const number='258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
cosnt webSocket='QVi4FvBm2f5rkYzKe4E4ObfpDOE='
let websocketAccept= require('crypto').createHash('sha1').update(webSocketKey+number).digest('base64');
console.log(websocketAccept)
1
2
3
4
5

# 4.Sec-WebSocker-Key/Accept 的作用

  • 避免服务端收到非法的 websocket 连接
  • 确保服务端理解 websocket 连接
  • 用浏览器里发起 ajax 请求,设置 header 时,Sec-WebSocket-Key 以及其他相关的 header 是被禁止的
  • Sec-WebSocket-Key 主要目的并不是确保数据的安全性,因为 Sec-WebScoket-Key、Sec-WebScoket-Accept 的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)

# 4.数据帧格式

  • 1.WebSocket 客户端、服务端通信的最小单位是帧,由 1 个或多个帧组成一条完整的消息(message)

    • 发送端:将消息切割成多个帧,并发送给服务端
    • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息
  • 2.数据帧格式

    • 单位是比特。比如 FIN、RSV1 各占据 1 比特,opcode 占据 4 比特。
    • FIN:1 比特如果是 1,表示这是消息(message)的最后一个分片(fragment),如果是 0,表示不是消息(message)的最后一个分片(fragment)
    • RSV1,RSV2,RSV3:各占 1 个比特。一般情况下全为 0。当客户端、服务端协商采用 websocket 扩展时,者三个标志可以非 0,且值的含义由扩展进行定义。如果出现非零的值,并且没有采用 webSocket 扩展,连接出错。
    • Opcode:4 个比特。操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端该断开连接(fail the connection)
      • %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
      • %x1:表示这是一个文本帧(frame)
      • %x2:表示这是一个二进制帧(frame)
      • %x3-7:保留的操作代码,用于后续定义的非控制帧。
      • %x8:表示连接断开。
      • %x9:表示这是一个 ping 操作。
      • %xA:表示这是一个 pong 操作。
      • %xB-F:保留的操作代码,用于后续定义的控制帧。
    • Mask:1 个比特。表示是否要对数据载荷进行掩码操作。
      • 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作,如果服务端接受到的数据没有进行过掩码操作,服务端需要端口连接。
      • 如果 Mask 是 1,那么在 Masking-key 中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask 都是 1。
    • Payload length:数据载荷的长短,单位是字节。为 7 位,或 7+16 位,或 7+64 位。
      • Payload length =x 为 0~125:数据的长度为 x 字节。
      • Payload length =x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度。
      • Payload length =x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高为为 0),该无符号整数的值为数据的长度。
      • 如果 payload length 占用了多个字节的话,payload length 的二进制表达采用网络序(big endian ,重要的位在前)
    • Masking-key:0 或 4 字节(32 位)所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask 为 1,且携带了 4 个字节的 Masking-key。如果 Mask 为 0,则没有 Masking-key。载荷数据的长度,不包括 mask key 的长度。
    • Payload data:(x+y)字节
      • 载荷数据:包括了扩展数据、应用数据。其中,扩展数据 x 字节,应用数据 y 字节。
      • 扩展数据:如果没有协商使用扩展的话,扩展数据为 0 字节。所有的扩展必须声明扩展数据的长度,或者可以如何扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
      • 应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 扩展数据长度,就得到应用数据的长度。
  • 3 掩码算法

    • 掩码键(Masking-Key)是由客户端挑选出来的 32 位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
      • 对索引 i 取余 4 得到 j,因为掩码一共就是 4 个字节。
      • 对原来的索引进行异或对应的掩码字节。
      • 异或就是两个数的二进制形式,按位对比,相同取 0,不同取 1
    function unmask(buffer,mask){
      const length=buffer.length;
      for(let i=0;i<length;i++){
        buffer[i]^=mask[!&3]
      }
    }
    
    1
    2
    3
    4
    5
    6

# 案例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>websocket</title>
  </head>

  <body>
    <script>
      let ws = new WebSocket("ws://localhost:8888")
      ws.onopen = function() {
        console.log("客户端连接成功")
        //在向服务器发送一个消息
        ws.send({
          type: "login",
          payload: { username: "zbc", password: "123" },
        })
      }
      //绑定事件是用加属性的方式
      ws.onmessage = function(event) {
        console.log("收到服务器端的响应", event.data)
      }
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let express = require("express")
let app = express()
app.use(express.static(__dirname))
//http服务器
app.listen(3000, () => {
  console.log(3000)
})

let WebSocketServer = require("ws").Server
//用ws模块启动一个websocket服务器,监听8888端口
let wsServer = new WebSocketServer({ port: 8888 })
//监听客户端的连接请求,当客户端连接服务器的时候,就会触发connection事件
//socket代表一个客户端,不是所有客户端共享的,而是每个客户端都有一个socket
wsServer.on("connection", (socket) => {
  console.log("客户端连接成功")
  //监听对方发过来的消息
  socket.on("message", (message) => {
    console.log("接受客户端的消息", message)
    socket.send("服务器回应" + message)
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//如果使用tcp协议服务器模拟一共websocket服务器
let net = require("net")
const crypto = require("crypto")
const CODE = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
//创建了一个tcp服务器,参数是一个socket对象
//tcp服务器传输层,可以接受http请求和websocket请求
let server = net.createServer((socket) => {
  console.log("socket")
  //once表示值监听一次就销毁了
  socket.once("data", (data) => {
    //data就是消息内容,是一个buffer
    data = data.toString()
    //判断正则是否匹配,如果匹配的话,则认为这个请求是一个升级协议的请求
    if (data.match(/Upgrade:websocket/)) {
      let rows = data.split("\r\n")
      rows = rows.slice(1, -2)
      const headers = {}
      rows.forEach((row) => {
        let [key, val] = row.split(":")
        headers[key] = val
      })
      if (headers[`Sec-WebSocket-Version`] == 13) {
        let SecWebSocketKey = headers["Sec-WebSocket-key"]
        let SecWebSocketAccept = crypto
          .createHash("sha")
          .update(SocWebSocketKey + CODE)
          .digest("base64")
        let response = [
          `HTTP/1.1 101 Switching Protocols`,
          `Upgrade:websocket`,
          `Connection:Upgrade`,
          `Sec-WebSocket-Accept:${SecWebSocketAccept}`,
          `\r\n`,
        ].join("\r\n")
        socket.write(response)
        //握手只有一次,后面就是不停的收发消息了,这个data就是消息了
        socket.on("data", (buffers) => {
          //判断是否是结束帧——fin是一个boolean
          let _fin = (buffers[0] & 0b10000000) === 0b10000000
          //取得第一个字节的后四位,得到是一个十进制数
          let _opcode = buffers[0] & 0b10001111
          //判断是否有掩码操作
          let _isMask = (buffers[1] & 0b10000000) === 0b10000000
          //buffer1的后七位代表负载的长度
          let _payloadLength = buffers[1] & 0b01111111
          //取得掩码,跳过前两个字段 取四个字节
          let _mask = buffers.slice(2, 6)
          //hello经过掩码处理过的hello
          let _payload = buffers.slice(6)
          if (_isMask) {
            mask(_payload, _mask)
          }
          //拼响应数据
          let response = Buffer.alloc(_payload.length + 2)
          response[0] = _opcode | 0b10000000
          response[1] = _payload.length
          _payload.copy(response, 2)
          socket.write(response)
        })
      }
    }
  })
})
function mask(buffers, mask) {
  for (let i = 0; i < buffers.length; i++) {
    buffers[i] ^= mask[i % 4]
  }
}
server.listen(9999)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/* 请求头
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: UM_distinctid=165fca0a2af3d0-0f501090bb29f9-1d67446b-25800-165fca0a2b05e7; CNZZDATA1254020586=1375640483-1537541642-%7C1537541642; _ga=GA1.1.1745777516.1537541711
Sec-WebSocket-Key: xl0sD4LsfBfipcm8jULjYA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
 */

/*
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: QVi4FvBm2f5rkYzKe4E4ObfpDOE=
 */
let crypto = require("crypto")
const CODE = ""
const secWebsocketKey = "xl0sD4LsfBfipcm8jULjYA=="
const websocketAccept = crypto
  .createHash("sha1")
  .update(secWebsocketKey + CODE)
  .digest("base64")
console.log(websocketAccept)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31