websocket是什么?怎么用?

一、什么是websocket?

定义:

webSocket是一种在单个TCP连接上进行全双工通信的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,属于服务器推送技术的一种。

在Websocket API中,浏览器和服务器只需要完成一次握手,两者之间可以创建持久性的连接,并进行双向数据传输。websocket本质上是一种计算机网络应用层的协议,用来弥补http协议在持久通信能力上的不足。

特点:

1.建立在TCP协议之上,服务器端的实现比较容易

为什么选择TCP?

可靠性:TCP提供可靠的传输,确保数据包按顺序到达且无丢失,而且TCP具有错误恢复机制,可重传丢失的包和校验数据完整性;

面向连接的通信:Websocket设计为长连接,TCP的面向连接(通过三次握手建立连接,四次挥手关闭连接)特性支持长期稳定的通信通道;TCP的全双工模式为websocket的双向通信提供底层支持。

与HTTP握手兼容:Websocket握手阶段使用HTTP协议(通过Upgrade头切换协议),而HTTP本身基于TCP,使得Websocket无缝融入现有的Web生态。

补充说明:为何不选择UDP?

虽然UDP具有低延迟和无连接特性,但其不可靠性(丢包、乱序)不适合WebSocket的典型使用场景。若基于UDP,WebSocket需自行处理重传、排序等问题,增加复杂性,违背其设计初衷(简化实时通信)。

2.与HTTP协议有着良好的兼容性。默认端口也是80(非加密)和443(加密),并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器。

当浏览器向服务器发起websocket连接时,会先发送http请求(带有Upgrade头)进行握手,服务器响应101 Switching Protocols状态码,表示同意将连接升级为Websocket协议,之后双方可以基于Websocket进行通信,而这个过程对于浏览器和中间代理服务器来说,就像处理普通的HTTP请求一样,只是在握手成功后才切换到Websocket协议进行数据传输。

3.没有同源限制,客户端可以与任意服务器通信

4.websocket的数据帧结构相对简单,它只包含一些必要的头部信息和数据内容,不像HTTP协议那样有大量的头部字段和复杂的消息格式。这使得在数据传输过程中,websocket能减少数据冗余,减低传输开销,提高通信效率

5.websocket可以发送文本,也能直接传输二进制数据,如图片、音频、视频等

6. WebSocket 使用 ws****作为协议标识符来标识非加密的 WebSocket 连接,而对于加密的 WebSocket 连接则使用 wss

复制代码
ws://example.com:8080/ws
wss://secure.example.com/ws

网络协议连接方式http/https/ws/wss比较

HTTP协议

客户端基于TCP/IP协议与服务器建立连接;连接建立后,客户端发送HTTP请求,服务器接收请求,以请求URL确定处理方式并向客户端返回响应内容。

HTTPS协议:

  • 客户端和服务器先通过TCP协议建立连接,确定双方能可靠传输数据。
  • TLS/SSL握手
  • 握手完成,建立安全通道,之后客户端和服务器间数据传输都经 TLS/SSL 加密,保证数据机密性、完整性 ,防止数据被窃取、篡改。

补充TLS/SSL握手过程

1.建立TCP连接后,客户端向服务器发送握手消息,其中包含它所支持的加密算法和以一个随机数。

2.服务器 收到消息后,服务器接收到客户端的信息后,从客户端提供的加密算法列表中选择一个它也支持的算法。将包含公共密钥的证书(包含服务器的公钥)及自身的随机数发送给客户端。(握手消息)

3.客户端验证证书(检查签发机构、证书是否过期),抽取公共密钥,生成 pre_master_secret 随机密码串,用服务器公钥加密后发送给服务器(只有持有对应私钥的服务器才能解密 pre_master_secret )

4.双方依据 pre_master_secret 及各自随机数独立计算出用于后续数据加密的加密密钥 ,使用消息认证码(MAC)计算 MAC 密钥

5.客户端和服务器分别使用计算得到的 MAC 密钥对握手消息计算 MAC 值,并发送给对方 ;对方收MAC值,使用相同的密钥对收到的握手消息计算MAC值,对比之后如果相同说明双方的MAC密钥一致,握手成功。

6.此时,双方可以开始使用协商好的密钥进行安全的数据传输,因为双方的密钥一致,并且能够验证消息的完整性和真实性,所以可以保证数据在传输过程中不被篡改,且确实来自合法的通信对方

WS:

  • 客户端向服务器发送特殊 HTTP 请求(含 WebSocket 相关头部字段,如 Upgrade: websocket ,表示要升级协议),请求建立 WebSocket 连接。
  • 服务器接收请求,若支持 WebSocket 协议,回复 101 Switching Protocols 状态码,确认协议升级,双方切换到 WebSocket 协议通信。
  • 握手成功,在 TCP 连接基础上建立持久连接,后续双方可随时双向通信,无需重复建立连接。
  • 连接建立后,客户端和服务器可互相发送文本或二进制数据,实现实时通信,如聊天消息发送、实时数据推送。

WSS:

  • 同 HTTPS,先通过 TCP 协议建立基础连接。
  • 与 HTTPS 的 TLS/SSL 握手类似,客户端和服务器协商加密算法、交换密钥等,建立安全通道。
  • TLS/SSL 握手成功,基于已建立的安全通道,进行 WebSocket 握手,过程同 WS 协议的握手 ,建立 WebSocket 连接。
  • 通过已建立的加密 WebSocket 连接,客户端和服务器进行双向数据传输,数据在传输中被加密,保障安全。

二、为什么需要WebSocket

HTTP协议有一个缺陷:**通信只能由客户端发起,不具备服务器推送能力。**这种单向请求的特点,客户端获取服务器消息只能使用轮询:每隔一段时间,客户端发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室,客户端与服务器交互频繁,如果使用HTTP协议,轮询方式导致效率低,非常浪费资源。

  • 短轮询:客户端定时向服务器发送 HTTP 请求询问是否有新消息。比如每 5 秒发一次请求,不管有无新消息都得发。若服务器没新消息,这次请求就白白消耗了网络带宽和服务器资源;频繁请求还会增加客户端设备的功耗和性能开销。
  • 长轮询:客户端向服务器发送 HTTP 请求来获取消息。如果服务器当时没有新消息可供发送,它不会像传统的 HTTP 请求那样立即返回一个没有消息的响应,而是将这个请求保持挂起状态,也就是让连接一直处于打开状态。服务器会等待,直到有新消息产生或者达到了预先设定的超时时间,才会向客户端发送响应。这样做的目的是为了让客户端能够尽可能及时地获取到新消息,而不需要像短轮询那样频繁地发送请求去询问是否有新消息。

在websocket协议出现前,基于HTTP协议实现Web应用与服务端双通道通信需不停轮询,引发服务端需维持大量客户端连接,以及轮询请求产生高开销(如携带多余header致无用的数据传输)等问题。

为了解决这些问题,websocket协议由此而生,于2011年被IETF定为标准RFC6455,并被RFC7936所补充规范。并且在HTML5标准中增加了有关WebSocket协议的相关api,所以只要实现了HTML5标准的客户端,就可以与支持WebSocket协议的服务器进行全双工的持久通信了。

三、WebSocket与HTTP的区别

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

联系:WebSocket在建立握手时,数据是通过HTTP传输的。但是握手之后,数据传输是通过ws协议的

根据以上图我们来说明一下其区别:

1.连接方式

HTTP:连接相对短暂,客户端向服务器发送请求,服务器响应请求后连接即结束。即使在 HTTP/1.1 及后续版本中支持持久连接,也主要是为了在同一连接上进行多个请求 / 响应,本质上还是基于请求 - 响应模式。

WebSocket:在客户端和服务器之间建立持久的双向连接,连接一旦建立,双方可随时主动发送数据,实现实时通信。

2.数据传输

HTTP:请求和响应数据格式较为复杂 ,包含大量的头部信息等,每次传输可能会携带很多冗余数据。数据传输是单向的,由客户端发起请求,服务器进行响应。

WebSocket:数据格式相对轻量 ,传输更加高效。而且支持全双工通信,客户端和服务器可以同时发送和接收数据,在实时性要求高的场景(如在线游戏、实时聊天等)中表现更优。

3.建立连接的方式

HTTP:客户端通过向服务器发送 HTTP 请求来建立连接,服务器根据请求的 URL、方法等信息进行相应的处理并返回响应。

WebSocket:连接建立需要通过 HTTP 协议进行握手。客户端发送带有特殊请求头的 HTTP 请求到服务器,服务器响应表示同意升级连接为 WebSocket 连接,连接建立后就可以使用 WebSocket 协议进行通信,不再依赖 HTTP 协议的请求 - 响应模式。

4.协议特点

HTTP:是无状态协议,服务器不保留客户端的状态信息,每个请求都是独立的,这使得服务器的实现相对简单,但在处理一些需要状态管理的业务逻辑时,需要额外的机制(如会话管理、Cookie 等)来维护状态。

websocket:连接建立后,双方可以在同一连接上持续进行数据交互,具有状态性,更适合处理需要保持上下文信息的业务场景。

5.应用场景:

HTTP:适用于获取静态资源(如网页、图片、文件等)以及进行简单的信息查询和提交等场景,例如浏览普通的新闻网站、提交表单数据等。

WebSocket:主要用于需要实时交互和双向通信的场景,如在线聊天、实时监控、股票行情推送、多人协作编辑等,能为用户提供更流畅、实时的体验。

四、websocket协议的原理

与http协议一样,websocket协议也需要通过已建立的TCP连接来传输数据,具体实现上是通过http协议建立通道,然后在此基础上用真正的websocket协议进行通信。

1.先建立TCP连接

2.握手过程

客户端发起请求

javascript 复制代码
//get是http请求方法,表示从服务器的/chart路径请求目标资源;HTTP/1.1表示使用的HTTP协议版本
GET /chat HTTP/1.1
//指定请求目标服务器的主机名和端口号(若未明确指定端口,对于 HTTP 默认 80 端口,HTTPS 默认 443 端口)
Host: server.example.com
//告诉服务器客户端希望将当前的HTTP连接升级为Websocket连接
Upgrade: websocket
//配合 "Upgrade" 字段使用,表明客户端希望服务器在处理完本次请求后,将连接升级为 "Upgrade" 字段指定的协议(此处为 WebSocket)。它表示连接的属性要进行改变,从 HTTP 协议切换到 WebSocket 协议。
Connection: Upgrade
//由客户端随机生成的Base64编码的密钥。服务器接收到密钥后,使用特定算法与固定字符串拼接后进行哈希计算,再将结果进行Base64编码返回给客户端
//客户端收到响应密钥后使用与服务器端相同的算法对其处理,客户端将自己计算出的结果与服务器返回的字段比较;相对则通过,否则拒绝连接;
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
//客户端向服务器表明自己支持的子协议列表。"chat" 和 "superchat" 就是这里列举的两个子协议,服务器可以从中选择一个双方都支持的子协议进行后续通信。如果服务器不支持任何一个,它可以选择不接受这个 WebSocket 连接,或者忽略该字段使用默认的 WebSocket 通信方式。
Sec-WebSocket-Protocol: chat, superchat
//用于指定客户端使用的 WebSocket 协议版本。服务器会检查该版本号,如果服务器支持这个版本,就可以继续进行连接升级流程;如果不支持,服务器可能会返回错误信息,拒绝升级连接。
Sec-WebSocket-Version: 13
// /发起这个 WebSocket 连接请求的网页所在的域
Origin: http://example.com

然后服务器返回以下字段,表示已经接收到请求,成功建立Websocket

javascript 复制代码
//响应行,101状态码表示服务器同意将连接升级为新的协议。
HTTP/1.1 101 Switching Protocols
//确认将连接升级为 WebSocket 协议。
Upgrade: websocket
//确认改变连接的协议。
Connection: Upgrade
//服务器根据客户端的Sec-WebSocket-Key计算得到的 Base64 编码的响应密钥,用于验证连接的合法性。
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
//服务器选择的子协议。
Sec-WebSocket-Protocol: chat

3.数据传输阶段

握手成功后,客户端和服务器之间建立websocket连接,可以进行双向传输数据,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):表示这是否是消息的最后一个数据帧。如果为 1,则表示是最后一个数据帧;如果为 0,则表示还有后续数据帧。
  • RSV1、RSV2、RSV3(各 1 bit):保留位,用于扩展 WebSocket 协议,默认值为 0。
  • opcode(4 bits):表示数据帧的类型,常见的类型有:
    • 0x0:表示延续帧,用于分割大消息。
    • 0x1:表示文本帧,数据为 UTF-8 编码的文本。
    • 0x2:表示二进制帧,数据为二进制数据。
    • 0x8:表示关闭帧,用于关闭 WebSocket 连接。
    • 0x9:表示 Ping 帧,用于检测连接是否存活。
    • 0xA:表示 Pong 帧,是对 Ping 帧的响应。
  • MASK(1 bit):表示数据是否进行了掩码处理。客户端发送的数据帧必须进行掩码处理,服务器发送的数据帧不能进行掩码处理。
  • Payload length(7 bits、7+16 bits 或 7+64 bits):表示数据帧的有效负载长度。如果值在 0 - 125 之间,则表示实际的负载长度;如果值为 126,则接下来的 16 位表示负载长度;如果值为 127,则接下来的 64 位表示负载长度。
  • Masking-key(0 或 32 bits) :如果MASK为 1,则包含一个 32 位的掩码密钥,用于对数据进行掩码处理。
  • Payload Data:实际传输的数据。

4.连接关闭阶段:

当客户端或服务器需要关闭 WebSocket 连接时,会发送一个关闭帧(opcode 为0x8)。关闭帧可以包含一个状态码和一个可选的关闭原因。对方收到关闭帧后,会发送一个确认关闭帧进行响应,然后双方关闭连接。以下是一个示例关闭帧的状态码和含义:

  • 1000:表示正常关闭。
  • 1001:表示端点离开,例如服务器关闭或浏览器关闭页面。
  • 1002:表示由于协议错误而关闭。
  • 1003:表示由于不支持的数据类型而关闭。
  • 1005:表示没有状态码的关闭。
  • 1006:表示异常关闭,没有发送或接收关闭帧。

五、websocket的优缺点

优点:

  • WebSocket协议一旦建议后,互相沟通所消耗的请求头是很小的
  • 服务器可以向客户端推送消息了

缺点:

  • 少部分浏览器不支持,浏览器支持的程度与方式有区别(IE10)

六、websocket应用场景

  • 即时聊天通信:用户在连天客户端(如网页聊天窗口)输入消息后,客户端将消息通过已经建立的websocket连接发送给服务器。服务器收到消息后,根据消息的目标(单聊时的特定用户、群聊时的群组),再通过 WebSocket 将消息推送给相应的接收方客户端。
  • 多玩家游戏:以在线多人对战游戏为例,玩家操作游戏角色(如移动、攻击等)时,客户端将操作指令通过 WebSocket 发送给游戏服务器。服务器实时更新游戏世界状态(如角色位置、生命值变化),再通过 WebSocket 把最新状态推送给所有参与游戏的玩家客户端,客户端据此更新游戏画面。
  • 在线协同编辑:多个用户同时打开在线文档编辑页面,客户端与服务器建立 WebSocket 连接。当某个用户进行编辑操作(如输入文字、修改格式),操作数据会立即通过 WebSocket 发送给服务器。服务器整合这些操作,更新文档状态后,再通过 WebSocket 将新的文档内容推送给其他正在编辑的用户客户端,实现文档内容实时同步。
  • 实时地图位置:以共享出行软件为例,用户开启软件后,手机的定位系统获取用户位置信息,通过客户端的 WebSocket 连接将位置数据发送给服务器。服务器实时更新用户在地图上的位置,并通过 WebSocket 把附近其他用户(如拼车乘客、司机)的位置信息推送给该用户客户端,客户端在地图上显示出各方位置。

不能使用websocket的场景:

如果我们需要通过网络传输的任何实时更新或连续数据流,则可以使用WebSocket。如果我们要获取旧数据,或者只想获取一次数据供应用程序使用,则应该使用HTTP协议,不需要很频繁或仅获取一次的数据可以通过简单的HTTP请求查询,因此在这种情况下最好不要使用WebSocket

七、websocket心跳机制和断线重连

  • onopen:在 WebSocket 连接成功建立之后调用。
  • onmessage:当客户端接收到服务器发送的消息时调用。
  • onclose:在 WebSocket 连接关闭时调用。
  • onerror:在 WebSocket 连接出现错误时调用。

客户端

1.如何与服务器进行通信:调用方法this.send({type:'消息类型',message:'消息内容'})

2.心跳机制:心跳机制是为了实时检测连接的状态,客户端每隔一段时间发送ping包,如果能够接收到服务器的pong响应,说明连接正常;否则说明连接出现问题:可能是断网了

怎么做心跳机制?

  • 设置心跳定时器heartbeatTimer,用于每隔一段事件发送一次心跳包;设置pingTimeout存储ping的超时定时器,如果规定时间内没有收到pong响应说明服务器有问题则主动断开ws连接
  • 封装startHeartbeat开启心跳,当连接成功建立时在onopen里开启心跳
javascript 复制代码
//开启心跳检测
startHeartbeat(){
    //心跳定时器
    this.heartbeatTimer=setInterval(()=>{
    //发送心跳包
    this.send({type:ping})
    //如果5s还没收到响应断开连接
    this.pingTimeout=setTimeout(()=>{
         if(this.ws.readyState==WebSocket.OPEN){
            this.ws.close()
          }
       },5000)
    },25000)
}
  • 封装stopHeartbeat停止心跳,在连接关闭的时候停止心跳
javascript 复制代码
//停止心跳
    stopHeartbeat() {
        console.log("停止心跳")
        clearInterval(this.heartbeatTimer);
        if (this.pingTimeout) {
            clearTimeout(this.pingTimeout);
        }
    }

3.重连机制:如果不是客户端主动断开ws连接,由于网络等原因被迫断开的连接调用重连机制

  • 设置重连定时器reconnectTimer,使用指数退避策略实施重连,随着重连失败次数增多重连延时时间延长,节省网络资源;记录重连尝试次数reconnectAttempts和最大重连尝试次数maxReconnectAttempts停止重连
  • 封装重连机制
javascript 复制代码
//计划重连
scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
        console.log('已达最大重连次数,停止重试')
        return
    }

    //指数退避策略
    const delay = Math.pow(2, this.reconnectAttempts) * 1000
    this.reconnectTimer = setTimeout(() => {
        console.log('尝试重连', this.reconnectAttempts)
        this.reconnectAttempts++
        this.connect()
    }, delay)
}

完整代码如下:

javascript 复制代码
//模拟客户端websocket
export class webSocketClient{
    constructor(){
        //websocket实例
        this.ws=null,
        //存储心跳定时器
        this.heartbeatTimer=null,
        //心跳响应超时定时器
        this.pingTimeout=null,
        //重连次数
        this.reconnectAttemps=0,
        //重连最大次数
        this.maxReconnectAttemps=5
        //重连定时器
        this.reconnectTimer=null
    }
    //初始化连接
    connect(){
        //这里假如一个用户只用一个ws连接
        if(this.ws) this.ws.close()
        //初始化一个ws连接
        this.ws=new WebSocket('握手的接口')
        //连接成功回调
        this.ws.onopen=()=>{
            console.log("ws连接成功")
            //初始化重连次数,开始心跳
            this.reconnectAttemps=0
            this.startHeartbeat()
        }
        //接收服务器消息的回调
        this.ws.onmessage=(event)=>{
            const data=JSON.parse(event.data)
            if(data.type=='pong'){
                console.log("心跳正常")
                if(this.pingTimeout){
                    clearTimeout(this.pingTimeout)
                    this.pingTimeout=null
                }
            }else{
                //处理业务消息
            }
        }

        this.ws.onerror=(error)=>{
            console.log('ws连接出错',error)
        }

        //连接关闭触发回调
        this.ws.onclose=(event)=>{
            console.log('ws连接关闭的原因',event.reason)
            //停止心跳检测
            this.stopHeartbeat()
            //如果是异常关闭,非主动关闭,则尝试重连
            if(!event.wasClean){
                console.log("重连")
                this.scheduleReconnect()
            }
        }
    }

    //开启心跳检测
    startHeartbeat(){
        //心跳定时器
        this.heartbeatTimer=setInterval(()=>{
            //发送心跳包
            this.send({type:ping})
            //如果5s还没收到响应断开连接
            this.pingTimeout=setTimeout(()=>{
                if(this.ws.readyState==WebSocket.OPEN){
                    this.ws.close()
                }
            },5000)
        },25000)
    }

    //停止心跳
    stopHeartbeat(){
        console.log('停止心跳')
        clearInterval(this.heartbeatTimer)
        if(this.pingTimeout){
            clearTimeout(this.pingTimeout)
        }
    }

    //send封装
    send(message){
        if(this.ws.readyState==WebSocket.OPEN){
            this.ws.send(JSON.stringify(message))
        }
    }

    //重连
    scheduleReconnect(){
        if(this.reconnectAttemps>=this.maxReconnectAttemps){
            console.log("以达到最大重连次数,停止重连")
            return
        }
        //指数退避策略
        const delay=Math.pow(2,this.reconnectAttemps)*1000
        this.reconnectTimer=setTimeout(()=>{
            this.reconnectAttemps++
            this.connect()
        },delay)
    }
}

//记得配置代理的时候ws:true

服务端:

1.当有新的客户端连接到服务器时触发连接对应的回调,wss.on('connection',()=>{})

2.验证客户端的携带的token是否合法,如果合法才做出相应的处理,否则关闭连接

  • 绑定用户连接userConnections,将用户id与ws连接相对应存储起来,后续如果要给客户端发送消息时,找到相对应的用户的连接,可以调用send方法给指定的用户id发送消息

验证原理

JSON Web Token 是一种用于在网络应用中安全传输信息的开放标准(RFC 7519)。一个 JWT 通常由三部分组成,用点(.)分隔,分别是头部(Header)、载荷(Payload)和签名(Signature)。验证 JWT 的核心就是验证签名的有效性,确保 JWT 在传输过程中没有被篡改。

验证过程

  1. 解析 JWT :首先,jwt.verify 方法会将传入的 JWT 字符串按照点(.)进行分割,得到头部、载荷和签名三部分。
  2. 验证签名 :使用相同的签名算法(在头部中指定)和密钥(JWT_SECRET)对头部和载荷进行签名计算,得到一个新的签名。然后将新计算的签名与 JWT 中的签名进行比较,如果两者相同,则说明签名有效,JWT 在传输过程中没有被篡改。
  3. 验证过期时间 :如果 JWT 的载荷中包含 exp(过期时间)字段,jwt.verify 方法会检查当前时间是否已经超过了 exp 指定的时间。如果超过了,则说明 JWT 已经过期,验证失败。
  4. 返回结果 :如果签名有效且没有过期,验证成功,回调函数的 err 参数为 nulldecoded 参数包含解码后的载荷信息;如果签名无效或者 JWT 已经过期,验证失败,回调函数的 err 参数包含相应的错误信息,decoded 参数为 undefined

验证合法的条件

验证合法需要满足以下两个条件:

  1. 签名有效:新计算的签名与 JWT 中的签名相同,说明 JWT 在传输过程中没有被篡改。
  2. 未过期 :如果 JWT 的载荷中包含 exp 字段,当前时间必须在 exp 指定的时间之前,说明 JWT 还在有效期内。

3.服务器主动检测心跳

上面的客户端我们自定义了一个心跳机制,服务端我们用ws.ping()触发websocket底层的心跳机制

这里使用了ws的isAlive属性记录客户端心跳状态,true表示客户端存活,false表示客户端离线了

javascript 复制代码
//全局心跳检测(服务器主动检测,如果客户端超时回应pong断开连接)
setInterval(()=>{
    wss.clients.forEach((ws)=>{
        if(ws.isAlive==false){
            console.log('心跳超时,关闭连接')
            return ws.terminate()//强制断开
        }
        ws.isAlive=false
        // 该方法是 WebSocket 协议的一部分,用于发送一个 ping 帧到客户端。
        // 当客户端收到 ping 帧后,协议层会自动返回一个 pong 帧作为响应。
        // 这个过程在 WebSocket 协议内部处理,不需要你在客户端手动监听或回复 pong。
        ws.ping()
    })
},30000)

完整代码如下:

javascript 复制代码
const http=require('http')
const WebSocket=require('ws')
const express=require('express')

//创建应用
const app=express()

const server=http.createServer(app)
///创建ws服务器实例
const wss=new WebSocket({noServer:true})

//握手阶段,将http连接升级为ws连接
//request是 http.IncomingMessage 实例,代表客户端的http请求,包按请求头headers,请求方法,url等信息
//socket是net.Socket 实例,代表与客户端之间底层的TCP套接字连接,你可以借助这个对象对底层的网络连接进行控制,比如读和写数据socket.write()向客户端发送数据
//head是 Buffer 类型的对象,包含了客户端发送的额外数据,这些数据是在升级请求中紧跟在 HTTP 头之后的部分。
server.on('upgrade',(request,socket,head)=>{
    //handleUpgrade 方法的作用是把普通的 HTTP 连接升级为 WebSocket 连接。升级成功后调用回调
    //回调函数中的 ws 是一个 ws.WebSocket 实例,代表与客户端建立的 WebSocket 连接
    wss.handleUpgrade(request,socket,head,(ws)=>{
        wss.emit('connection',ws,request)
    })
})

wss.on('connection',(ws,req)=>{
    console.log("处理客户端的ws连接")
    //验证用户身份
    const token=new URL(req.url,'baseURL').searchParams.get('token')
    jwt.verify(token,JWT_SECRET,(err,decoded)=>{
        if(err){
            console.log('无效token,关闭连接')
            ws.close(4003,'Athentication Failed')
            return
        }
        //这里可以绑定用户信息等逻辑操作

        //处理心跳,自定义isAlive将客户端的状态设置为存活
        ws.isAlive=true
        //监听pong消息,这里是响应服务器触发底层的心跳机制,客户端会自动返回pong
        ws.on('pong',()=>{
            ws.isAlive=true
        })

        //处理消息
        ws.on('message',(data)=>{
            try{
                const message=JSON.parse(data)
                //这里是响应客户端自定义的心跳机制
                if(message.type=='ping'){
                    ws.send(JSON.stringify({type:'pong'}))
                }else{
                    //处理业务消息
                }
            }catch(err){
                ws.send('{"type":"error","message":"消息格式错误"}')
            }
        })

    })
})

//全局心跳检测(服务器主动检测,如果客户端超时回应pong断开连接)
setInterval(()=>{
    wss.clients.forEach((ws)=>{
        if(ws.isAlive==false){
            console.log('心跳超时,关闭连接')
            return ws.terminate()//强制断开
        }
        ws.isAlive=false
        // 该方法是 WebSocket 协议的一部分,用于发送一个 ping 帧到客户端。
        // 当客户端收到 ping 帧后,协议层会自动返回一个 pong 帧作为响应。
        // 这个过程在 WebSocket 协议内部处理,不需要你在客户端手动监听或回复 pong。
        ws.ping()
    })
},30000)

参考:

一文吃透 WebSocket 原理 刚面试完,趁热赶紧整理因为项目中使用到了 WebSocket ,面试官在深挖项目经验 - 掘金

相关推荐
丨千纸鹤丨25 分钟前
高可用集群Keepalived
linux·服务器·网络
北极光SD-WAN组网2 小时前
工业互联网时代,如何通过混合SD-WAN提升煤炭行业智能化网络安全
网络·安全·web安全
charlie1145141912 小时前
快速入门Socket编程——封装一套便捷的Socket编程——导论
linux·网络·笔记·面试·网络编程·socket
东风西巷3 小时前
X-plore File Manager v4.34.02 修改版:安卓设备上的全能文件管理器
android·网络·软件需求
liulilittle3 小时前
C++ Proactor 与 Reactor 网络编程模式
开发语言·网络·c++·reactor·proactor
我很好我还能学4 小时前
【计算机网络 篇】TCP基本认识和TCP三次握手相关问题
运维·服务器·网络
苏州向日葵4 小时前
篇五 网络通信硬件之PHY,MAC, RJ45
网络·嵌入式硬件
程序员编程指南4 小时前
Qt 网络编程进阶:WebSocket 通信
c语言·网络·c++·qt·websocket
作业Y5 小时前
第1章第2章笔记
网络
宇宙核5 小时前
【内网穿透】使用FRP实现内网与公网Linux/Ubuntu服务器穿透&项目部署&多项目穿透方案
运维·服务器·网络