webSocket通信

最近在项目中用到了websocket通信,之前就了解过它的功能,第一次真正使用这个大名鼎鼎的通信协议,激动的心 ,颤抖的手,,,快来会会这个老朋友。

先来复习一波。

“WebSocket”是一种基于 TCP 的全双工网络通信协议。

1.为什么要有 WebSocket

HTTP/2 针对的是“队头阻塞”,传输效率低下的问题,而 WebSocket 针对的是“请求 - 应答”通信模式。

“请求 - 应答”是一种“半双工”的通信模式,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性,但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域。

2.WebSocket 的特点

2.1 全双工

WebSocket 是一个真正“全双工”的通信协议,客户端和服务器都可以随时向对方发送数据。

一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。

2.2 二进制帧结构

WebSocket 虽然有“帧”,但却没有像 HTTP/2 那样定义“流”,也就不存在“多路复用”“优先级”等复杂的特性,而它自身就是“全双工”的,也就不需要“服务器推送”。

2.3WebSocket 的握手

WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段和两个认证用头字段:

  • “Connection: Upgrade”,表示要求协议“升级”;
  • “Upgrade: websocket”,表示要“升级”成 WebSocket 协议。
  • Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;
  • Sec-WebSocket-Version:协议的版本号,当前必须是 13。

服务器收到 HTTP 请求报文,看到上面的四个字段,就知道这不是一个普通的 GET 请求,而是 WebSocket 的升级请求,于是就不走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用 WebSocket 协议通信。
如下图:
使用webSocket通信协议实现后端消息推送 - 图1

3.WebSocket 与 HTTP/2 的异同点

同:

  • 都可以从 HTTP/1 升级,都采用二进制帧结构。

异:

  • HTTP/2是请求与响应的模式,在WebSocket是“全双工”,没有请求响应的概念,服务器也可以主动向客户端发起请求,收发的都是数据帧。
  • websocket里面有帧的概念,却没有http2.0里的虚拟流的概念,也不存在优先级、多路复用。
  • websocket的出现本质上还是为了解决http的半双工的问题,变成全双工,服务器和客户端可以随意通行的问题。

工作场景遇到过用户订阅股票的股价,股价波动时实时推送给海量订阅的用户,面试场景被问到两次,一 千万粉丝的明星发布动态如何推送给粉丝 二 海量用户的主播直播如何推送弹幕 当时回答消息队列,其实web socket才是比较好的方案。 WebSocket适合实时通信交互的场景,和消息队列其实是两个领域,不冲突,可以互相结合使用。

4.实际应用

基于webSocket通信的库主要有 socket.ioSockJS,这里使用的是 SockJS。

引入模块

需要在项目中引入sockjs-clientstomjs这两个模块。

  1. import SockJS from "sockjs-client";
  2. import Stomp from "stompjs";
  3. import store from "./store";
  4. import router from "@/router";
  5. import { Notification } from "element-ui";

请求参数配置
  1. const sockJS = new SockJS(`//${process.env.VUE_APP_WS_URL}/ws`); // 请求地址
  2. stomp = Stomp.over(sockJS); // 用来定义消息语义.
  3. stomp.heartbeat.outgoing = 10000; // 客户端向服务端发送心跳包
  4. stomp.heartbeat.incoming = 0; //服务端 不向客户端发送心跳包

这个心跳包就是用来检测连接情况,之所以采用客户端向服务器发送而不是服务端向客户端发送,主要是考虑到服务器的压力,当用户很多的时候,比如淘宝等,那么就需要服务器维护一个很长的tcp连接,向每个用户都发送心跳包,这样服务器就压力很大。所以采用客户端发送心跳包。

向服务器发起websocket连接
  1. stomp.connect(
  2. {},
  3. () => {
  4. // 订阅事件类型 发送关于的什么的通知
  5. stomp.subscribe(`/token/01_${store.state.user.name}/event`, (res) => {
  6. // 服务器推送的数据
  7. const content = JSON.parse(res.body);
  8. const { data } = content;
  9. const { result } = data;
  10. const { title, msg, id, type } = data;
  11. // 客户端要提示的消息配置
  12. const options = {
  13. title,
  14. message: msg,
  15. onClick: () => {
  16. // 跳转 并刷新详情页
  17. // 金融产品状态变更
  18. if (
  19. type === "product" &&
  20. router.currentRoute.path !== "/my-financial-products/detail"
  21. ) {
  22. router.push({
  23. path: "/my-financial-products/detail",
  24. query: { id },
  25. });
  26. }
  27. // 融资申请管理
  28. if (
  29. type === "order" &&
  30. router.currentRoute.path !==
  31. "/financing-application-management/application-detail"
  32. ) {
  33. router.push({
  34. path: "/financing-application-management/application-detail",
  35. query: { id },
  36. });
  37. }
  38. },
  39. };
  40. // 存到store ,如果当前在详情页,就通过watch监听变化,触发详情页刷新
  41. store.commit(
  42. "app/SET_ORDER_DETAIL_TOGGLE",
  43. !store.state.app.orderStatus
  44. );
  45. if (result === "pass") {
  46. options.type = "success";
  47. Notification(options);
  48. } else if (result === "fail") {
  49. options.type = "error";
  50. Notification(options);
  51. } else if (result === "default") {
  52. options.type = "info";
  53. Notification(options);
  54. }
  55. });
  56. },
  57. // 若出现超时未连接成功的情况就会重新连接一次
  58. connect
  59. );

通过subscribe订阅要推送的消息类型,根据推送回来的数据在页面显示对应的提示。

完整代码:

  1. import SockJS from "sockjs-client";
  2. import Stomp from "stompjs";
  3. import store from "./store";
  4. import router from "@/router";
  5. import { Notification } from "element-ui";
  6. let stomp;
  7. export default function connect() {
  8. if (!stomp || !stomp.connected) {
  9. const sockJS = new SockJS(`//${process.env.VUE_APP_WS_URL}/ws`); // 请求地址
  10. stomp = Stomp.over(sockJS); // 格式化消息
  11. stomp.heartbeat.outgoing = 10000; // 客户端向服务端发送心跳包
  12. stomp.heartbeat.incoming = 0; //服务端 不向客户端发送心跳包
  13. // 向服务器发起websocket连接
  14. stomp.connect(
  15. {},
  16. () => {
  17. // 订阅事件类型 发送关于的什么的通知
  18. stomp.subscribe(`/token/01_${store.state.user.name}/event`, (res) => {
  19. // 服务器推送的数据
  20. const content = JSON.parse(res.body);
  21. const { data } = content;
  22. const { result } = data;
  23. const { title, msg, id, type } = data;
  24. // 客户端要提示的消息配置
  25. const options = {
  26. title,
  27. message: msg,
  28. onClick: () => {
  29. // 跳转 并刷新详情页
  30. // 金融产品状态变更
  31. if (
  32. type === "product" &&
  33. router.currentRoute.path !== "/my-financial-products/detail"
  34. ) {
  35. router.push({
  36. path: "/my-financial-products/detail",
  37. query: { id },
  38. });
  39. }
  40. // 融资申请管理
  41. if (
  42. type === "order" &&
  43. router.currentRoute.path !==
  44. "/financing-application-management/application-detail"
  45. ) {
  46. router.push({
  47. path: "/financing-application-management/application-detail",
  48. query: { id },
  49. });
  50. }
  51. },
  52. };
  53. // 存到store ,如果当前在详情页,就通过watch监听变化,触发详情页刷新
  54. store.commit(
  55. "app/SET_ORDER_DETAIL_TOGGLE",
  56. !store.state.app.orderStatus
  57. );
  58. // store.commit(`app/SET_HINT_TOGGLE`, !store.state.app.hintToggle);
  59. // if (
  60. // router.currentRoute.path !== "/entside/personalcenter/systeminfo"
  61. // ) {
  62. // store.commit("app/SET_HINT_DISPLAY", true);
  63. // }
  64. if (result === "pass") {
  65. options.type = "success";
  66. Notification(options);
  67. } else if (result === "fail") {
  68. options.type = "error";
  69. Notification(options);
  70. } else if (result === "default") {
  71. options.type = "info";
  72. Notification(options);
  73. }
  74. });
  75. },
  76. // 若出现超时未连接成功的情况就会重新连接一次
  77. connect
  78. );
  79. }
  80. return stomp;
  81. }

关于为什么要用SockJS与stomp

1、SockJS

SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的、跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟、全双工、跨域通信通道。你可能会问,我为什么不直接用原生的WebSocket而要使用SockJS呢?这得益于SockJS的一大特性,一些浏览器中缺少对WebSocket的支持,因此回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。SockJS提供了浏览器兼容性,优先使用原生的WebSocket,如果某个浏览器不支持WebSocket,SockJS会自动降级为轮询。

2、stomjs

直接使用WebSocket ,返回的是将字节流转化为文本/二进制消息,不是语义化的消息,因此可以在 WebSocket 之上使用STOMP协议,来为浏览器 和 server间的通信增加适当的消息语义。

STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议,WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义。与HTTP不同,WebSocket是处在TCP上非常薄的一层,会将字节流转化为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此可以在 WebSocket 之上使用STOMP协议,来为浏览器 和 server间的通信增加适当的消息语义。

STOMP与WebSocket 的关系:

HTTP协议解决了web浏览器发起请求以及web服务器响应请求的细节,假设HTTP协议不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情

直接使用WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用,因为没有高层协议,就需要我们定义应用间发送消息的语义,还需要确保连接的两端都能遵循这些语义;

同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义.