WebSocket协议

4/18/2020 网络

# WebSocket由来

通信只能由客户端发起,不具备服务器推送能力。

应用场景:即时聊天通信、多玩家游戏、在线协同编辑/编辑、实时数据流的拉取与推送、体育/游戏实况、实时地图位置;

实时更新或连续数据流,则可以使用WebSocket;获取旧数据或只获取一次数据,则用HTTP协议;

# WebSocket 与 HTTP 的区别

相同点: 都是一样基于TCP的,都是可靠性传输协议。都是应用层协议。

联系: WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。

不同点:1、WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的, HTTP1.1 中有一个 keep-alive,在一个 HTTP 连接中,可以发送多个 Request接收多个 Response,但是 一个 Request 只能有一个 Response 2、WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接。

注意:虽然HTTP/2也具备服务器推送功能,但HTTP/2 只能推送静态资源,无法推送指定的信息。

# Websocket的优缺点

优点:WebSocket协议一旦建议后,互相沟通所消耗的请求头是很小的;服务器可以向客户端推送消息了 缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别(IE10)

# WebSocket连接的过程

首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等; 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据; 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

  • 客户端发起协议升级请求
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
1
2
3
4
5

WebSocket复用了HTTP的握手通道。--- 客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。

Connection: Upgrade:表示要升级协议

Upgrade: websocket:表示要升级到websocket协议。

Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Version header,里面包含服务端支持的版本号。

Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。

  • 服务端:响应协议升级

    状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
1
2
3
4

# websocket 断线重连

  • 判断在线离线?

当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果不存在就存入db或者缓存中, 第二次客户端定时再次发送请求依旧携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果存在就把上次的时间戳拿取出来,使用当前时间戳减去上次的时间,得出的毫秒秒数判断是否大于指定的时间,若小于的话就是在线,否则就是离线;

  • 怎么断线?

    1. 主动断开连接,ws.close();
  • 断线原因

  1. websocket超时没有消息自动断开连接;【 超时 】

解决: 在小于服务端设置的超时时间内发送心跳包,有2中方案: 客户端主动发送上行心跳包,或者服务端主动发送下行心跳包。

  1. websocket异常:服务端出现中断,交互切屏等客户端异常中断;【 中断 】

解决:客户端通过onclose 关闭连接,服务端再次上线时则需要清除之间存的数据,若不清除则会造成只要请求到服务端的都会被视为离线。

措施:

异常中断 ---> 重连:引入reconnecting-websocket.min.js,ws建立链接方法使用js库api方法

断网监测 ----> 使用js库:offline.min.js

# 心跳包机制

下一个定时器,在一定时间间隔下发送一个空包给客户端,然后客户端反馈一个同样的空包回来,服务器如果在一定时间内收不到客户端发送过来的反馈包,那就只有认定说掉线了。

  1. 客户端每隔一个时间间隔发生一个探测包给服务器
  2. 客户端发包时启动一个超时定时器
  3. 服务器端接收到检测包,应该回应一个包
  4. 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
  5. 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

# 客户端的 API

  • 新建WebSocket实例

    WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。

var ws = new WebSocket('ws://localhost:8080');
1
  • webSocket.readyState 当前状态
switch (ws.readyState) {
  case WebSocket.CONNECTING:    //  CONNECTING:值为0,表示正在连接
  case WebSocket.OPEN:			//  值为1,表示连接成功,可以通信了
  case WebSocket.CLOSING:		// 值为2,表示连接正在关闭
  case WebSocket.CLOSED:		//  值为3,表示连接已经关闭,或者打开连接失败
}
1
2
3
4
5
6
  • webSocket.onopen --- 连接成功后的回调函数

  • webSocket.onclose --- 连接关闭后的回调函数

  • webSocket.onmessage --- 收到服务器数据后的回调函数

  • webSocket.send() --- 向服务器发送数据

  • webSocket.bufferedAmount --- 表示还有多少字节的二进制数据没有发送。用来判断发送是否结束

  • webSocket.onerror --- 报错时的回调函数

# 服务端实现

Node 实现有三种: µWebSockets、Socket.IO、WebSocket-Node

websocketd --port=8080 bash ./counter.sh
1

启动一个 WebSocket 服务器,端口是8080。每当客户端连接这个服务器,就会执行counter.sh脚本,并将它的输出推送给客户端.

 //   客户端的 JavaScript 代码
var ws = new WebSocket('ws://localhost:8080/');
ws.onmessage = function(event) {
  console.log(event.data);
};
1
2
3
4
5

# 在vue项目中使用websocket

<template>
  <div class="box"> websocket </div>
</template>
<script>
  const heartCheck = {
    timeout: 60 * 1000,
    timer: null,
    serverTimer: null,
    reset() {
      this.timer && clearTimeout(this.timer)
      this.serverTimer && clearTimeout(this.serverTimer)
    },
    start(ws) {
      this.reset()
      this.timer = setTimeout(() => {
        // onmessage拿到返回的心跳就说明连接正常
        ws.send(JSON.stringify({ heart: 1 }))
        // 如果超过一定时间还没响应(响应后触发重置),说明后端断开了
        this.serverTimer = setTimeout(() => { ws.close() }, this.timeout)
      }, this.timeout)
    }
  }
  export default {
    name: 'Websocket',
    data() {
      return {
        wsuri: 'ws://123.207.167.163:9010/ajaxchattest', // ws wss
        lockReconnect: false, // 连接失败不进行重连
        maxReconnect: 5, // 最大重连次数,若连接失败
        socket: null
      }
    },
    mounted() { this.initWebSocket() },
    methods: {
      reconnect() {
        console.log('尝试重连')
        if (this.lockReconnect || this.maxReconnect <= 0) { return }
        setTimeout(() => {
          // this.maxReconnect-- // 不做限制 连不上一直重连
          this.initWebSocket()
        }, 60 * 1000)
      },
      initWebSocket() {
        try {
          if ('WebSocket' in window) { this.socket = new WebSocket(this.wsuri) } 
          else { console.log('您的浏览器不支持websocket') }
          this.socket.onopen = this.websocketonopen
          this.socket.onerror = this.websocketonerror
          this.socket.onmessage = this.websocketonmessage
          this.socket.onclose = this.websocketclose
        } catch (e) { this.reconnect() }
      },
      websocketonopen() {
        console.log('WebSocket连接成功', this.socket.readyState)
        heartCheck.start(this.socket)
        // this.socket.send('发送数据')
        this.websocketsend()
      },
      websocketonerror(e) {
        console.log('WebSocket连接发生错误', e)
        this.reconnect()
      },
      websocketonmessage(e) {
        let data = JSON.parse(e.data)
        console.log('得到响应', data, '可以渲染网页数据...')
        // 消息获取成功,重置心跳
        heartCheck.start(this.socket)
      },
      websocketclose(e) {
        console.log('connection closed (' + e.code + ')')
        this.reconnect()
      },
      websocketsend() {
        let data = { id: 'a1b2c3' }
        this.socket.send(JSON.stringify(data))
      }
    },
    destroyed() { this.socket.close() }
  }
</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
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
70
71
72
73
74
75
76
77
78
79
80