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 协议通信。
如下图:
3.WebSocket 与 HTTP/2 的异同点
同:
- 都可以从 HTTP/1 升级,都采用二进制帧结构。
异:
- HTTP/2是请求与响应的模式,在WebSocket是“全双工”,没有请求响应的概念,服务器也可以主动向客户端发起请求,收发的都是数据帧。
- websocket里面有帧的概念,却没有http2.0里的虚拟流的概念,也不存在优先级、多路复用。
- websocket的出现本质上还是为了解决http的半双工的问题,变成全双工,服务器和客户端可以随意通行的问题。
工作场景遇到过用户订阅股票的股价,股价波动时实时推送给海量订阅的用户,面试场景被问到两次,一 千万粉丝的明星发布动态如何推送给粉丝 二 海量用户的主播直播如何推送弹幕 当时回答消息队列,其实web socket才是比较好的方案。 WebSocket适合实时通信交互的场景,和消息队列其实是两个领域,不冲突,可以互相结合使用。
4.实际应用
基于webSocket通信的库主要有 socket.io,SockJS,这里使用的是 SockJS。
引入模块
需要在项目中引入sockjs-client、stomjs这两个模块。
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import store from "./store";
import router from "@/router";
import { Notification } from "element-ui";
请求参数配置
const sockJS = new SockJS(`//${process.env.VUE_APP_WS_URL}/ws`); // 请求地址
stomp = Stomp.over(sockJS); // 用来定义消息语义.
stomp.heartbeat.outgoing = 10000; // 客户端向服务端发送心跳包
stomp.heartbeat.incoming = 0; //服务端 不向客户端发送心跳包
这个心跳包就是用来检测连接情况,之所以采用客户端向服务器发送而不是服务端向客户端发送,主要是考虑到服务器的压力,当用户很多的时候,比如淘宝等,那么就需要服务器维护一个很长的tcp连接,向每个用户都发送心跳包,这样服务器就压力很大。所以采用客户端发送心跳包。
向服务器发起websocket连接
stomp.connect(
{},
() => {
// 订阅事件类型 发送关于的什么的通知
stomp.subscribe(`/token/01_${store.state.user.name}/event`, (res) => {
// 服务器推送的数据
const content = JSON.parse(res.body);
const { data } = content;
const { result } = data;
const { title, msg, id, type } = data;
// 客户端要提示的消息配置
const options = {
title,
message: msg,
onClick: () => {
// 跳转 并刷新详情页
// 金融产品状态变更
if (
type === "product" &&
router.currentRoute.path !== "/my-financial-products/detail"
) {
router.push({
path: "/my-financial-products/detail",
query: { id },
});
}
// 融资申请管理
if (
type === "order" &&
router.currentRoute.path !==
"/financing-application-management/application-detail"
) {
router.push({
path: "/financing-application-management/application-detail",
query: { id },
});
}
},
};
// 存到store ,如果当前在详情页,就通过watch监听变化,触发详情页刷新
store.commit(
"app/SET_ORDER_DETAIL_TOGGLE",
!store.state.app.orderStatus
);
if (result === "pass") {
options.type = "success";
Notification(options);
} else if (result === "fail") {
options.type = "error";
Notification(options);
} else if (result === "default") {
options.type = "info";
Notification(options);
}
});
},
// 若出现超时未连接成功的情况就会重新连接一次
connect
);
通过subscribe订阅要推送的消息类型,根据推送回来的数据在页面显示对应的提示。
完整代码:
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import store from "./store";
import router from "@/router";
import { Notification } from "element-ui";
let stomp;
export default function connect() {
if (!stomp || !stomp.connected) {
const sockJS = new SockJS(`//${process.env.VUE_APP_WS_URL}/ws`); // 请求地址
stomp = Stomp.over(sockJS); // 格式化消息
stomp.heartbeat.outgoing = 10000; // 客户端向服务端发送心跳包
stomp.heartbeat.incoming = 0; //服务端 不向客户端发送心跳包
// 向服务器发起websocket连接
stomp.connect(
{},
() => {
// 订阅事件类型 发送关于的什么的通知
stomp.subscribe(`/token/01_${store.state.user.name}/event`, (res) => {
// 服务器推送的数据
const content = JSON.parse(res.body);
const { data } = content;
const { result } = data;
const { title, msg, id, type } = data;
// 客户端要提示的消息配置
const options = {
title,
message: msg,
onClick: () => {
// 跳转 并刷新详情页
// 金融产品状态变更
if (
type === "product" &&
router.currentRoute.path !== "/my-financial-products/detail"
) {
router.push({
path: "/my-financial-products/detail",
query: { id },
});
}
// 融资申请管理
if (
type === "order" &&
router.currentRoute.path !==
"/financing-application-management/application-detail"
) {
router.push({
path: "/financing-application-management/application-detail",
query: { id },
});
}
},
};
// 存到store ,如果当前在详情页,就通过watch监听变化,触发详情页刷新
store.commit(
"app/SET_ORDER_DETAIL_TOGGLE",
!store.state.app.orderStatus
);
// store.commit(`app/SET_HINT_TOGGLE`, !store.state.app.hintToggle);
// if (
// router.currentRoute.path !== "/entside/personalcenter/systeminfo"
// ) {
// store.commit("app/SET_HINT_DISPLAY", true);
// }
if (result === "pass") {
options.type = "success";
Notification(options);
} else if (result === "fail") {
options.type = "error";
Notification(options);
} else if (result === "default") {
options.type = "info";
Notification(options);
}
});
},
// 若出现超时未连接成功的情况就会重新连接一次
connect
);
}
return stomp;
}
关于为什么要用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之上提供了一个基于帧的线路格式层,用来定义消息语义.