我是靠谱客的博主 花痴发卡,最近开发中收集的这篇文章主要介绍websocket,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在这里插入图片描述

请求头特征

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

  • HTTP 必须是 1.1 GET 请求
  • HTTP Header 中 Connection 字段的值必须为 Upgrade
  • HTTP Header 中 Upgrade 字段必须为 websocket
  • Sec-WebSocket-Key 字段的值是采用 base64 编码的随机 16 字节字符串
    • 这个编码后的值是用于服务端握手的创建过程,用来表示接受了这个连接
    • 这部分信息用于服务端证明收到一个有效的 WebSocket 握手操作的认证。这可以帮助服务端不会接收通过 WebSocket 服务任意发送数据的非 WebSocket 客户端的连接(例如 HTTP 客户端)。
  • Sec-WebSocket-Protocol 字段的值记录使用的子协议,比如 binary base64
  • Origin 表示请求来源
    • 缺少此字段的连接不应该认为是来自浏览器。
  • Sec-WebSocket-Version 表示草案的版本
    • 这个header字段的值必须为13。
  • Sec-WebSocket-Extensions 表示客户端期望使用的协议级别的扩展
    • 任何一个扩展凭证都必须是一个注册过的凭证
    • 对数据的操作顺序就是bar(foo(data))
Sec-WebSocket-Extensions: foo, bar; baz=2

响应头特征

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

  • 状态码是 101 表示 Switching Protocols
  • Upgrade / Connection 和请求头一致
  • Sec-WebSocket-Protocol 服务端会从中选择零或者一个支持的协议并且在响应握手中输出它选择的那个协议。
  • Sec-WebSocket-Accept 是通过请求头的 Sec-WebSocket-Key 生成
    • 通过Sec-WebSocket-Key字段的值(作为一个字符串,而不是base64解码后)和"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"串联起来,忽略所有前后空格进行base64 SHA-1编码的值
如果客户端握手时发送的Sec-WebSocket-Keyheader字段的值为"dGhlIHNhbXBsZSBub25jZQ==",那么服务端会把"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"拼接到后面得到"dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"。
然后服务端回对这个字符串进行SHA-1哈希操作,得到0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea。
对这个值进行base64编码,得到结果为"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",然后通过Sec-WebSocket-Accept字段返回这个结果。

轮询、长连接
短轮询:定时发送http请求
长轮询:发送请求直到收到消息or超时后继续发送下一个请求

轮询缺点:

  • 服务器无法主动发送数据
  • 轮询实时性差
  • 轮询浪费较多资源

长连接缺点

  • HTTP keep-alive 开启后虽然 TCP 可以复用,但是 Header 重复的问题并没有解决

websocket连接过程

和 HTTP 一样都是建立在 TCP 协议之上,但只需一次 HTTP 握手,就能建立持久性连接,后续就不走 HTTP 了,而是 WebSocket 特有的数据帧

  • 浏览器、服务器建立TCP连接,三次握手
    • 如果是一条建立在HTTPS(HTTPS+TLS)端口的连接,通过这个链接完成TLS握手过程。
  • TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(开始前的HTTP握手)
  • 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。
  • 当收到了连接成功的消息后,通过TCP通道进行传输通信。
    • WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的

websocket增加的内容
websocket层是基于TCP实现的,增加了以下的内容

  • 增加了一个基于浏览器的同源策略模型
  • 增加了一个地址和协议命名机制来在同一个端口上支持多个服务,在同一个IP地址自持多个主机名
  • 在TCP协议上分层构建框架机制回到TCP使用的IP包机制,但是没有长度限制
  • 包含一个设计用于处理有代理和其他网络中介的情况的额外的结束握手协议

websocket限制

  • 连接最初状态为CONNECTING
  • 如果客户端有一条通过远端主机(IP地址)定义的主机和端口定义的已经建立连接的WebSocket连接,即使这个远端主机被定义为了其他的名字,这个客户端也必须等到当前的这条连接建立成功或者失败才能建立连接
    • 客户端最多有一条连接可以处于CONNECTING状态。如果多个连接尝试同时与一个相同的IP地址建立连接,客户端必须把他们进行排序
  • 客户端可以与单个主机建立的WebSocket连接数量是没有限制的。当建立的连接过多时,服务端可以拒绝和主机/IP地址建立的连接
    • 在Web浏览器环境下,客户端需要考虑在用户打开的多个tab页中设置一个同时建立连接的数目限制。

websocket断开连接

  • 任何一端都可以发送一个包含特定关闭握手的控制帧数据,收到此帧后,另一端在不发送任何数据后会发送一个结束帧作为响应。收到另一端的结束帧后,最开始发送控制帧的端在没有数据需要发送时,就会安全的关闭此连接。
    • 通常大多数情况下,服务端应该先关闭,所以是服务端而不是客户端保持 TIME_WAIT 状态(因为客户端先关闭的话,这会阻止服务端在2 MSL(报文最长存活时间) 内重新打开这条连接,而如果服务器处于 TIME_WAIT 状态下,如果收到了一个带有更大序列号的新的 SYN 包时,也能够立即响应重新打开连接,从而不会对服务器产生影响)

websocket数据帧

  • 在WebSocket协议中,数据是通过一系列数据帧来进行传输的
    • 客户端必须在它发送到服务器的所有帧中添加掩码。服务端禁止在发送数据帧给客户端时添加掩码
    • 掩码的不可预测性对于预防恶意应用作者在网上暴露相关的字节数据至关重要
 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 ...                |
+---------------------------------------------------------------+

FIN: 1 bit。表示这是消息的最后一个片段。第一个片段也有可能是最后一个片段。
RSV1RSV2RSV3: 每个1 bit。必须设置为0,除非扩展了非0值含义的扩展。如果收到了一个非0值但是没有扩展任何非0值的含义,接收终端必须断开WebSocket连接。
Opcode: 4 bit。定义“有效负载数据”的解释。如果收到一个未知的操作码,接收终端必须断开WebSocket连接。下面的值是被定义过的
	%x0 表示一个持续帧;%x1 表示一个文本帧;%x2 表示一个二进制帧;%x3-7 预留给以后的非控制帧;%x8 表示一个连接关闭包;%x9 表示一个ping包;%xA 表示一个pong包;%xB-F 预留给以后的控制帧
Mask: 1 bit。mask标志位,定义“有效负载数据”是否添加掩码。如果设置为1,那么掩码的键值存在于Masking-Key中,这个一般用于解码“有效负载数据”。所有的从客户端发送到服务端的帧都需要设置这个bit位为1。
Payload length: 7 bits, 7+16 bits, or 7+64 bits。以字节为单位的“有效负载数据”长度,如果值为0-125,那么就表示负载数据的长度。如果是126,那么接下来的2个bytes解释为16bit的无符号整形作为负载数据的长度。如果是127,那么接下来的8个bytes解释为一个64bit的无符号整形(最高位的bit必须为0)作为负载数据的长度
Payload data: (x+y) bytes。“有效负载数据”是指“扩展数据”和“应用数据”。
Extension data: x bytes。除非协商过扩展,否则“扩展数据”长度为0 bytes。在握手协议中,任何扩展都必须指定“扩展数据”的长度,包含在总的有效负载长度中。
Application data: y bytes。任意的“应用数据”,占用“扩展数据”后面的剩余所有字段。“应用数据”的长度等于有效负载长度减去“扩展应用”长度
Masking-Key: 0 or 4 bytes。 所有从客户端发往服务端的数据帧都已经与一个包含在这一帧中的32 bit的掩码进行过了运算
	它是用于对定义在同一节中定义的帧负载数据Payload data字段中的包含Extension data和Application data的数据进行添加掩码

websocket协议中的心跳帧

  • 心跳Ping帧包含的操作码是0x9。如果终端收到了一个心跳Ping帧,那么终端必须发送一个心跳Pong 帧作为回应,除非已经收到了一个关闭帧。终端应该尽快回复Pong帧。
    • 终端可能会在建立连接后与连接关闭前中间的任意时间发送Ping帧。
  • 心跳Pong帧包含的操作码是0xA。作为回应发送的Pong帧必须完整携带Ping帧中传递过来的“应用数据”字段。
    • Pong帧可以被主动发送。这会作为一个单项的心跳。预期外的Pong包的响应没有规定

websocket的安全性

  • 通过检查Origin头字段,但是宿主可以发送假的Origin头字段来欺骗服务端
  • 对客户端发送给服务端的所有数据添加掩码(一串二进制,对目标字段进行位或运算),这样的话远端的脚本(攻击者)就不能够控制发送的数据如何出现在线路上,因此就不能够构造一条被中介误解的 HTPT请求
    • 客户端必须为每一帧选择一个新的掩码值,使用一个不能够被应用预测到的算法来进行传递数据
    • 从客户端发送给服务端的频道数据需要添加掩码值,服务端发送给客户端的数据不需要添加掩码
  • 当从客户端开始传递第一帧时,这个帧的有效载荷(应用程序提供的数据)就不能够被客户端应用程序修改
  • 应该对帧的大小和组装过后的包的总大小有一定的限制。
  • WebSocket 协议实现必须支持 TLS,并且应该在与对端进行数据传输时使用它。

websocket使用

//"ws"代表非加密连接,"wss"代表加密连接(通过TLS)
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
//默认的"ws"端口是80,而默认的"wss"端口是443

const ws = new WebSocket(“ws://localhost:8080);

属性:

  • ws.binaryType 使用二进制的数据类型连接。
  • ws.bufferedAmount 只读,未发送至服务器的字节数。
  • ws.extensions 只读,服务器选择的扩展。
  • ws.protocol 是个只读属性,用于返回服务器端选中的子协议的名字;这是一个在创建WebSocket 对象时,在参数protocols中指定的字符串,当没有已建立的链接时为空串。
  • ws.readyState
0 (WebSocket.CONNECTING)
正在链接中
1 (WebSocket.OPEN)
已经链接并且可以通讯
2 (WebSocket.CLOSING)
连接正在关闭
3 (WebSocket.CLOSED)
连接已关闭或者没有链接成功

方法:

  • ws.close([code[, reason]])
    关闭当前链接。
  • ws.send(data)
    对要传输的数据进行排队。

监听:

  • ws.onclose
    用于指定连接关闭后的回调函数。
  • ws.onerror
    用于指定连接失败后的回调函数。
  • ws.onmessage
    用于指定当从服务器接受到信息时的回调函数。
  • ws.onopen
    用于指定连接成功后的回调函数。

WebSocket心跳检测和断线重连

  • 心跳检测:WebSocket在连接关闭的情况下会触发onclose事件,在链接异常的情况下会触发onerror事件。而在弱网条件下,onclose事件触发的灵敏度却不高,往往已经断网很久了才触发onclose事件,前端又去进行重连操作,对实时界面的展示不友好。

    • 通过setTimeout做心跳检测(本示例使用这种方式)
      • 通过setTimeout在启动时和每次收到消息时,启动定时器向后台发送约定消息,并在定时器中再启动一个断开连接的定时器,当超时未清除时,断开连接
      • 当收到消息时,清楚掉上面两个定时器,并重新开启
    • 通过setInterval做心跳检测
      • 通过setInterval定时不断向后台发送消息,并在定时器外开启一个断开连接的定时器,当超时未清除时,断开连接,在定时器外避免生成多个断开连接的定时器
      • 在收到消息时,清楚掉断开连接的定时器,并重新生成
  • 断线重连:关闭连接的时候调用创建连接的方法

import { mapActions, mapState } from 'vuex';
export default {
    name: 'Websocket',
    data() {
        return {
            // 是否正在重连
            lockReconnect: false,
            //websocket实例
            socket: null,
            reconnectTimeout: null,
            timeout: 10 * 1000,
            timer: null,
            serverTimer: null
        };
    },
    computed: {
        ...mapState(['userInfo']),
        wsuri() {
            return `${process.env.VUE_APP_WEBSOCKET_URI}${this.userInfo.tenantId};${this.userInfo.userId}`;
        }
    },
    async mounted() {
        await this.getUserInfo();
        //初始化websocket
        this.initWebSocket();
    },
    destroyed() {
        this.socket.close();
    },
    methods: {
        ...mapActions(['getUserInfo']),
        start(ws) {
            this.reset();
            //心跳检测:定时发送消息并通过定时器启动关闭连接操作,若回复了消息,则取消关闭连接的定时器
            this.timer = setTimeout(() => {
                // console.log('发送心跳,后端收到后,返回一个心跳消息')
                // onmessage拿到返回的心跳就说明连接正常
                ws.send('ping');
                this.serverTimer = setTimeout(() => {
                    // 如果超过一定时间还没响应(响应后触发重置),说明后端断开了
                    ws.close();
                }, this.timeout);
            }, this.timeout);
        },
        //重置心跳检测的定时器和关闭连接的定时器
        reset() {
            this.serverTimer && clearTimeout(this.serverTimer);
            this.timer && clearTimeout(this.timer);
        },
        reconnect() {
            console.log('尝试重连');
            //避免多次重连
            if (this.lockReconnect) {
                return;
            }
            this.lockReconnect = true;
            this.reconnectTimeout && clearTimeout(this.reconnectTimeout);
            this.reconnectTimeout = setTimeout(() => {
                this.initWebSocket();
                this.lockReconnect = false;
            }, 4 * 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);
            //连接成功启动心跳检测
            this.start(this.socket);
            this.websocketSend();
        },
        websocketOnError(e) {
            console.log('WebSocket连接发生错误', e);
            this.reconnect();
        },
        websocketOnMessage(e) {
            if (e.data === 'pong') {
                // 每次消息获取成功,重置心跳并重新开启
                this.start(this.socket);
            }
        },
        websocketClose(e) {
            console.log('connection closed (' + e.code + ')');
            this.reconnect();
        },
        websocketSend() {
            this.socket.send('ping');
        }
    }
};

最后

以上就是花痴发卡为你收集整理的websocket的全部内容,希望文章能够帮你解决websocket所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部