Bootstrap

WebSocket(看这一篇就够了)

在这里插入图片描述

WebSocket 基本概念

什么是WebSocket?

WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。

WebSocket 的其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
在这里插入图片描述

为什么需要 WebSocket?

我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不具备服务器推送能力。

举例来说,我们想了解查询今天的实时数据,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用’轮询’:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

在 WebSocket 协议出现以前,创建一个和服务端进双通道通信的 web 应用,需要依赖HTTP协议,进行不停的轮询,这会导致一些问题:

  • 服务端被迫维持来自每个客户端的大量不同的连接
  • 大量的轮询请求会造成高开销,比如会带上多余的header,造成了无用的数据传输。

http协议本身是没有持久通信能力的,但是我们在实际的应用中,是很需要这种能力的,所以,为了解决这些问题,WebSocket协议由此而生,于2011年被IETF定为标准RFC6455,并被RFC7936所补充规范。

并且在HTML5标准中增加了有关WebSocket协议的相关api,所以只要实现了HTML5标准的客户端,就可以与支持WebSocket协议的服务器进行全双工的持久通信了。

与 HTTP 协议的区别

与 HTTP 协议相比,WebSocket 具有以下优点:

  1. 更高的实时性能:WebSocket 允许服务器和客户端之间实时双向通信,从而提高了实时通信场景中的性能。
  2. 更少的网络开销: HTTP 请求和响应之间需要额外的数据传输,而 WebSocket 通过在同一个连接上双向通信,减少了网络开销。
  3. 更灵活的通信方式:HTTP 请求和响应通常是一一对应的,而 WebSocket 允许服务器和客户端之间以多种方式进行通信,例如消息 Push、事件推送等。
  4. 更简洁的 API:WebSocket 提供了简洁的 API,使得客户端开发人员可以更轻松地进行实时通信。

当然肯定有缺点的:

  1. 不支持无连接: WebSocket 是一种持久化的协议,这意味着连接不会在一次请求之后立即断开。这是有利的,因为它消除了建立连接的开销,但是也可能导致一些资源泄漏的问题。
  2. 不支持广泛: WebSocket 是 HTML5 中的一种标准协议,虽然现代浏览器都支持,但是一些旧的浏览器可能不支持 WebSocket。
  3. 需要特殊的服务器支持: WebSocket 需要服务端支持,只有特定的服务器才能够实现 WebSocket 协议。这可能会增加系统的复杂性和部署的难度。
  4. 数据流不兼容: WebSocket 的数据流格式与 HTTP 不同,这意味着在不同的网络环境下,WebSocket 的表现可能会有所不同。
    在这里插入图片描述

WebSocket协议的原理

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

首先,WebSocket 是一个持久化的协议,相对于 HTTP 这种非持久的协议来说。简单的举个例子吧,用目前应用比较广泛的 PHP 生命周期来解释。

HTTP 的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。

在 HTTP1.1 中进行了改进,使得有一个 keep-alive,也就是说,在一个 HTTP 连接中,可以发送多个 Request,接收多个 Response。但是请记住 Request = Response, 在 HTTP 中永远是这样,也就是说一个 Request 只能有一个 Response。而且这个 Response 也是被动的,不能主动发起。

首先 WebSocket 是基于 HTTP 协议的,或者说借用了 HTTP 协议来完成一部分握手。

首先我们来看个典型的 WebSocket 握手

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

熟悉 HTTP 的童鞋可能发现了,这段类似 HTTP 协议的握手请求中,多了这么几个东西。

Upgrade: websocket
Connection: Upgrade

这个就是 WebSocket 的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的请求要用 WebSocket 协议,快点帮我找到对应的助理处理~而不是那个老土的 HTTP。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠我,我要验证你是不是真的是 WebSocket 助理。

然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同 URL 下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~

最后, Sec-WebSocket-Version 是告诉服务器所使用的 WebSocket Draft (协议版本),在最初的时候,WebSocket 协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么 Firefox 和 Chrome 用的不是一个版本之类的,当初 WebSocket 协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用同一个版本:服务员,我要的是13岁的噢→_→然后服务器会返回下列东西,表示已经接受到请求, 成功建立 WebSocket 啦!

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

这里开始就是 HTTP 最后负责的区域了,告诉客户,我已经成功切换协议啦~

Upgrade: websocket
Connection: Upgrade

依然是固定的,告诉客户端即将升级的是 WebSocket 协议,而不是 mozillasocket,lurnarsocket 或者 shitsocket。

然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。服务器:好啦好啦,知道啦,给你看我的 ID CARD 来证明行了吧。

后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。至此,HTTP 已经完成它所有工作了,接下来就是完全按照 WebSocket 协议进行了。

WebSocket工作流程

  1. 握手阶段

WebSocket在建立连接时需要进行握手阶段。握手阶段包括以下几个步骤:

  • 客户端向服务端发送请求,请求建立WebSocket连接。请求中包含一个Sec-WebSocket-Key参数,用于生成WebSocket的随机密钥。
  • 服务端接收到请求后,生成一个随机密钥,并使用随机密钥生成一个新的Sec-WebSocket-Accept参数。
  • 客户端接收到服务端发送的新的Sec-WebSocket-Accept参数后,使用原来的随机密钥和新的Sec-WebSocket-Accept参数共同生成一个新的Sec-WebSocket-Key参数,用于加密数据传输。
  • 客户端将新的Sec-WebSocket-Key参数发送给服务端,服务端接收到后,使用该参数加密数据传输。
  1. 数据传输阶段

建立连接后,客户端和服务端就可以通过WebSocket进行实时双向通信。数据传输阶段包括以下几个步骤:

  • 客户端向服务端发送数据,服务端收到数据后将其转发给其他客户端。
  • 服务端向客户端发送数据,客户端收到数据后进行处理。

双方如何进行相互传输数据的 具体的数据格式是怎么样的呢?WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。

发送方 -> 接收方:ping。

接收方 -> 发送方:pong。

ping 、pong 的操作,对应的是 WebSocket 的两个控制帧

  1. 关闭阶段

当不再需要WebSocket连接时,需要进行关闭阶段。关闭阶段包括以下几个步骤:

  • 客户端向服务端发送关闭请求,请求中包含一个WebSocket的随机密钥。
  • 服务端接收到关闭请求后,向客户端发送关闭响应,关闭响应中包含服务端生成的随机密钥。
  • 客户端收到关闭响应后,关闭WebSocket连接。

总的来说,WebSocket通过握手阶段、数据传输阶段和关闭阶段实现了服务器和客户端之间的实时双向通信。

WebSocket 数据帧结构和控制帧结构。

  1. 数据帧结构WebSocket 数据帧主要包括两个部分:帧头和有效载荷。以下是 WebSocket 数据帧结构的简要介绍:
  • 帧头:帧头包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,fin 表示数据帧的结束标志,rsv1、rsv2、rsv3 表示保留字段,opcode 表示数据帧的类型,masked 表示是否进行掩码处理,payload_length 表示有效载荷的长度。
  • 有效载荷:有效载荷是数据帧中实际的数据部分,它由客户端和服务端进行数据传输。
  1. 控制帧结构除了数据帧之外,WebSocket 协议还包括一些控制帧,主要包括 Ping、Pong 和 Close 帧。以下是 WebSocket 控制帧结构的简要介绍:
  • Ping 帧:Ping 帧用于测试客户端和服务端之间的连接状态,客户端向服务端发送 Ping 帧,服务端收到后需要向客户端发送 Pong 帧进行响应。
  • Pong 帧:Pong 帧用于响应客户端的 Ping 帧,它用于测试客户端和服务端之间的连接状态。
  • Close 帧:Close 帧用于关闭客户端和服务端之间的连接,它包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,opcode 的值为 8,表示 Close 帧。

JavaScript 中 WebSocket 对象的属性和方法,以及如何创建和连接 WebSocket。

WebSocket 对象的属性和方法:

  1. WebSocket 对象:WebSocket 对象表示一个新的 WebSocket 连接。
  2. WebSocket.onopen 事件处理程序:当 WebSocket 连接打开时触发。
  3. WebSocket.onmessage 事件处理程序:当接收到来自 WebSocket 的消息时触发。
  4. WebSocket.onerror 事件处理程序:当 WebSocket 发生错误时触发。
  5. WebSocket.onclose 事件处理程序:当 WebSocket 连接关闭时触发。
  6. WebSocket.send 方法:向 WebSocket 发送数据。
  7. WebSocket.close 方法:关闭 WebSocket 连接。

创建和连接 WebSocket:

  1. 创建 WebSocket 对象:
var socket = new WebSocket('ws://example.com');

其中,ws://example.com 是 WebSocket 的 URL,表示要连接的服务器。

  1. 连接 WebSocket:
    使用 WebSocket.onopen 事件处理程序检查 WebSocket 是否成功连接。
socket.onopen = function() {
    console.log('WebSocket connected');
};
  1. 接收来自 WebSocket 的消息:

使用 WebSocket.onmessage 事件处理程序接收来自 WebSocket 的消息。

socket.onmessage = function(event) {
    console.log('WebSocket message:', event.data);
};
  1. 向 WebSocket 发送消息:

使用 WebSocket.send 方法向 WebSocket 发送消息。

 
socket.send('Hello, WebSocket!');
  1. 关闭 WebSocket:

当需要关闭 WebSocket 时,使用 WebSocket.close 方法。

socket.close();

注意:在 WebSocket 连接成功打开和关闭时,会分别触发 WebSocket.onopenWebSocket.onclose 事件。在接收到来自 WebSocket 的消息时,会触发 WebSocket.onmessage 事件。当 WebSocket 发生错误时,会触发 WebSocket.onerror 事件。

webSocket简单示例

以下是一个简单的 WebSocket 编程示例,通过 WebSocket 向服务器发送数据,并接收服务器返回的数据:

  1. 首先,创建一个 HTML 文件,添加一个按钮和一个用于显示消息的文本框:
<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8">
   <title>WebSocket 示例</title>
</head>
<body>
   <button id="sendBtn">发送消息</button>
   <textarea id="messageBox" readonly></textarea>
   <script src="main.js"></script>
</body>
</html>
  1. 接下来,创建一个 JavaScript 文件(例如 main.js),并在其中编写以下代码:
// 获取按钮和文本框元素
const sendBtn = document.getElementById('sendBtn');
const messageBox = document.getElementById('messageBox');

// 创建 WebSocket 对象
const socket = new WebSocket('ws://echo.websocket.org'); // 使用一个 WebSocket 服务器进行测试

// 设置 WebSocket 连接打开时的回调函数
socket.onopen = function() {
   console.log('WebSocket 连接已打开');
};

// 设置 WebSocket 接收到消息时的回调函数
socket.onmessage = function(event) {
   console.log('WebSocket 接收到消息:', event.data);
   messageBox.value += event.data + '\n';
};

// 设置 WebSocket 发生错误时的回调函数
socket.onerror = function() {
   console.log('WebSocket 发生错误');
};

// 设置 WebSocket 连接关闭时的回调函数
socket.onclose = function() {
   console.log('WebSocket 连接已关闭');
};

// 点击按钮时发送消息
sendBtn.onclick = function() {
   const message = 'Hello, WebSocket!';
   socket.send(message);
   messageBox.value += '发送消息: ' + message + '\n';

};

webSocket应用场景

  • 即时聊天通信
  • 多玩家游戏
  • 在线协同编辑/编辑
  • 实时数据流的拉取与推送
  • 体育/游戏实况
  • 实时地图位置
  • 即时Web应用程序:即时Web应用程序使用一个Web套接字在客户端显示数据,这些数据由后端服务器连续发送。在WebSocket中,数据被连续推送/传输到已经打开的同一连接中,这就是为什么WebSocket更快并提高了应用程序性能的原因。例如在交易网站或比特币交易中,这是最不稳定的事情,它用于显示价格波动,数据被后端服务器使用Web套接字通道连续推送到客户端。
  • 游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在WebSocket游戏应用程序中非常有帮助。
  • 聊天应用程序:聊天应用程序仅使用WebSocket建立一次连接,便能在订阅户之间交换,发布和广播消息。它重复使用相同的WebSocket连接,用于发送和接收消息以及一对一的消息传输。

不能使用WebSocket的场景

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

注意:如果仅加载一次数据,则RESTful Web服务足以从服务器获取数据。

WebSocket 错误处理

WebSocket 的错误处理

  1. WebSocket is not supported:当浏览器不支持 WebSocket 时,会出现此错误。解决方法是在浏览器兼容性列表中检查是否支持 WebSocket。
  2. WebSocket connection closed:当 WebSocket 连接被关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  3. WebSocket error:当 WebSocket 发生错误时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
  4. WebSocket timeout:当 WebSocket 连接超时时,会出现此错误。解决方法是在 WebSocket.ontimeout 事件处理程序中进行错误处理。
  5. WebSocket handshake error:当 WebSocket 握手失败时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
  6. WebSocket closed by server:当 WebSocket 连接被服务器关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  7. WebSocket closed by protocol:当 WebSocket 连接被协议错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  8. WebSocket closed by network:当 WebSocket 连接被网络错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  9. WebSocket closed by server:当 WebSocket 连接被服务器错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。

通过为 WebSocket 对象的 oncloseonerrorontimeout 事件添加处理程序,可以及时捕获和处理 WebSocket 错误,从而确保程序的稳定性和可靠性。

利用单例模式创建完整的wesocket连接

class webSocketClass {
    constructor(thatVue) {
      this.lockReconnect = false;
      this.localUrl = process.env.NODE_ENV === 'production' ? 你的websocket生产地址' : '测试地址';
      this.globalCallback = null;
      this.userClose = false;
      this.createWebSocket();
      this.webSocketState = false
      this.thatVue = thatVue
    }
  
    createWebSocket() {
      let that = this;
      // console.log('开始创建websocket新的实例', new Date().toLocaleString())
      if( typeof(WebSocket) != "function" ) {
        alert("您的浏览器不支持Websocket通信协议,请更换浏览器为Chrome或者Firefox再次使用!")
      }
      try {
        that.ws = new WebSocket(that.localUrl);
        that.initEventHandle();
        that.startHeartBeat()
      } catch (e) {
        that.reconnect();
      }
    }

    //初始化
    initEventHandle() {
      let that = this;
      // //连接成功建立后响应
      that.ws.onopen = function() {
        console.log("连接成功");
      }; 
      //连接关闭后响应
      that.ws.onclose = function() {
        // console.log('websocket连接断开', new Date().toLocaleString())
        if (!that.userClose) {
          that.reconnect(); //重连
        }
      };
      that.ws.onerror = function() {
        // console.log('websocket连接发生错误', new Date().toLocaleString())
        if (!that.userClose) {
          that.reconnect(); //重连
        }
      };
      that.ws.onmessage = function(event) {
        that.getWebSocketMsg(that.globalCallback);
        // console.log('socket server return '+ event.data);
      };
    }
    startHeartBeat () {
      // console.log('心跳开始建立', new Date().toLocaleString())
      setTimeout(() => {
          let params = {
            request: 'ping',
          }
          this.webSocketSendMsg(JSON.stringify(params))
          this.waitingServer()
      }, 30000)
    }
    //延时等待服务端响应,通过webSocketState判断是否连线成功
    waitingServer () {
      this.webSocketState = false//在线状态
      setTimeout(() => {
          if(this.webSocketState) {
              this.startHeartBeat()
              return
          }
          // console.log('心跳无响应,已断线', new Date().toLocaleString())
          try {
            this.closeSocket()
          } catch(e) {
            console.log('连接已关闭,无需关闭', new Date().toLocaleString())
          }
          this.reconnect()
          //重连操作
      }, 5000)
    }
    reconnect() {
      let that = this;
      if (that.lockReconnect) return;
      that.lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多
      setTimeout(function() {
        that.createWebSocket();
        that.thatVue.openSuccess(that) //重连之后做一些事情
        that.thatVue.getSocketMsg(that)
        that.lockReconnect = false;
      }, 15000);
    }
  
    webSocketSendMsg(msg) {
      this.ws.send(msg);
    }
  
    getWebSocketMsg(callback) {
      this.ws.onmessage = ev => {
        callback && callback(ev);
      };
    }
    onopenSuccess(callback) {
      this.ws.onopen = () => {
        // console.log("连接成功", new Date().toLocaleString())
        callback && callback()
      }
    }
    closeSocket() {
      let that = this;
      if (that.ws) {
        that.userClose = true;
        that.ws.close();
      }
    }
  }
  export default webSocketClass;

websocket 断线重连

心跳就是客户端定时的给服务端发送消息,证明客户端是在线的, 如果超过一定的时间没有发送则就是离线了。

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

如何解决断线问题

通过查阅资料了解到 nginx 代理的 websocket 转发,无消息连接会出现超时断开问题。网上资料提到解决方案两种,一种是修改nginx配置信息,第二种是websocket发送心跳包。

下面就来总结一下本次项目实践中解决的websocket的断线 和 重连 这两个问题解决方案。

主动触发包括主动断开连接,客户端主动发送消息给后端

  1. 主动断开连接
ws.close();
  1. 主动断开连接,根据需要使用,基本很少用到。主动发送消息
ws.send('hello world');

针对websocket断线我们来分析一下,断线的可能原因

原因一

websocket超时没有消息自动断开连接,应对措施:这时候我们就需要知道服务端设置的超时时长是多少,在小于超时时间内发送心跳包,有2中方案:一种是客户端主动发送上行心跳包,另一种方案是服务端主动发送下行心跳包。

下面主要讲一下客户端也就是前端如何实现心跳包:

首先了解一下心跳包机制

跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。

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

在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。

心跳检测步骤:

// 前端解决方案:心跳检测
var heartCheck = {
    timeout: 30000, //30秒发一次心跳
    timeoutObj: null,
    serverTimeoutObj: null,
    reset: function(){
        clearTimeout(this.timeoutObj);
        clearTimeout(this.serverTimeoutObj);
        return this;
    },
    start: function(){
        var self = this;
        this.timeoutObj = setTimeout(function(){
            //这里发送一个心跳,后端收到后,返回一个心跳消息,
            //onmessage拿到返回的心跳就说明连接正常
            ws.send('ping');
            console.log('ping!')

            self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了
                ws.close(); //如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
            }, self.timeout);
        }, this.timeout);
    }
}
  1. 客户端每隔一个时间间隔发生一个探测包给服务器
  2. 客户端发包时启动一个超时定时器
  3. 服务器端接收到检测包,应该回应一个包
  4. 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
  5. 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

原因二

websocket异常包括服务端出现中断,交互切屏等等客户端异常中断等等

当若服务端宕机了,客户端怎么做、服务端再次上线时怎么做?

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

针对这种异常的中断解决方案就是处理重连,下面我们给出的重连方案是使用js库处理:引入reconnecting-websocket.min.js,ws建立链接方法使用js库api方法:

var ws = new ReconnectingWebSocket(url);
// 断线重连:
reconnectSocket(){
    if ('ws' in window) {
        ws = new ReconnectingWebSocket(url);
    } else if ('MozWebSocket' in window) {
       ws = new MozWebSocket(url);
    } else {
      ws = new SockJS(url);
    }
}

断网监测支持使用js库:offline.min.js

onLineCheck(){
    Offline.check();
    console.log(Offline.state,'---Offline.state');
    console.log(this.socketStatus,'---this.socketStatus');

    if(!this.socketStatus){
        console.log('网络连接已断开!');
        if(Offline.state === 'up' && websocket.reconnectAttempts > websocket.maxReconnectInterval){
            window.location.reload();
        }
        reconnectSocket();
    }else{
        console.log('网络连接成功!');
        websocket.send('heartBeat');
    }
}

// 使用:在websocket断开链接时调用网络中断监测
websocket.onclose => () {
    onLineCheck();
};

以上方案,只是抛砖引玉,如果大家有更好的解决方案欢迎评论区分享交流。

java代码示例

添加Maven依赖
在pom.xml中添加WebSocket的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

配置WebSocket
创建一个配置类,实现WebSocketMessageBrokerConfigurer接口

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // 配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单的内存消息代理,用于广播消息
        config.enableSimpleBroker("/topic", "/queue");
        // 设置应用程序前缀,用于点对点消息
        config.setApplicationDestinationPrefixes("/app");
    }

    // 注册WebSocket端点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册一个端点,并启用SockJS支持(兼容不支持WebSocket的浏览器)
        registry.addEndpoint("/ws").withSockJS();
    }
}

控制器

创建一个控制器,使用SimpMessagingTemplate来发送消息

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    // 处理来自客户端的点对点消息
    @MessageMapping("/sendToUser")
    @SendTo("/queue/user/{recipient}")
    public String sendToUser(@Payload String message, @Header("recipient") String recipient) {
        return message; // 返回的消息将自动发送到指定的用户队列
    }

    // 处理来自客户端的广播消息
    @MessageMapping("/broadcast")
    @SendTo("/topic/messages")
    public String broadcastMessage(@Payload String message) {
        return message; // 返回的消息将广播到所有订阅了/topic/messages的客户端
    }

    // 通过控制器发送消息(例如,用于测试或管理目的)
    // 注意:这个方法不是通过WebSocket消息映射触发的,而是直接通过HTTP请求调用的
    // 在实际应用中,你可能会有其他服务或组件来调用这个方法
    public void sendMessageToUser(String recipient, String message) {
        messagingTemplate.convertAndSend("/queue/user/" + recipient, message);
    }

    public void broadcastMessageToAll(String message) {
        messagingTemplate.convertAndSend("/topic/messages", message);
    }
}

注意:@SendTo注解用于指定消息的发送目标。在点对点消息中,我们使用了/queue/user/{recipient}作为目标,其中{recipient}是一个占位符,它的值将从消息头中获取。在广播消息中,我们使用了/topic/messages作为目标。

前端代码

在src/main/resources/templates目录下创建一个index.html文件,用于WebSocket客户端连接和接收消息。这里只展示关键部分,因为完整的HTML文件可能很长。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket STOMP Client</title>
    <!-- 引入SockJS和STOMP客户端库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/[email protected]/lib/stomp.min.js"></script>
</head>
<body>
    <h1>WebSocket STOMP Client Example</h1>
    <script>
        // WebSocket服务器的URL(请根据实际情况替换)
        const socketUrl = '/your-websocket-endpoint'; // 注意:这通常是SockJS的端点,而不是原生的WebSocket端点
        let stompClient = null;

        // 连接到WebSocket服务器并初始化STOMP客户端
        function connect() {
            const socket = new SockJS(socketUrl);
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) {
                console.log('Connected: ' + frame);
                // 连接成功后可以在这里订阅主题等
                // 例如:stompClient.subscribe('/topic/someDestination', function (message) { ... });
            }, function (error) {
                console.error('Error during connection: ' + error);
            });
        }

        // 发送点对点消息
        function sendToUser(recipient, message) {
            if (stompClient && stompClient.connected) {
                stompClient.send("/app/sendToUser", { 'recipient': recipient }, message);
            } else {
                console.error("STOMP client is not connected.");
            }
        }

        // 发送广播消息
        function broadcastMessage(message) {
            if (stompClient && stompClient.connected) {
                stompClient.send("/app/broadcast", {}, message);
            } else {
                console.error("STOMP client is not connected.");
            }
        }

        // 示例:连接后发送测试消息
        window.onload = function() {
            connect();
            // 发送点对点消息(假设有一个用户名为'user1')
            setTimeout(() => sendToUser('user1', 'Hello, user1! This is a point-to-point message.'), 5000);
            // 发送广播消息
            setTimeout(() => broadcastMessage('This is a broadcast message!'), 10000);
        };
    </script>
</body>
</html>

注意:在前端代码中,我们使用了stompClient.send方法来发送消息。第一个参数是目标端点(与控制器中的@MessageMapping注解匹配),第二个参数是消息头(用于传递额外的信息,如点对点消息的接收者),第三个参数是消息体。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力
在这里插入图片描述

;