本文作者: 王明月
场景:实现运营人员在一个页面同时审核多个直播的诉求。
思路:需求本身是聊天室的特点,即服务向浏览器推送数据,浏览器展现,所以决定使用 WebSocket。
为什么是WebSocket 呢?有没有其他方法实现呢?它们的优缺点又是什么。
首先 我们来了解一下 TCP连接 和 HTTP 协议 。
在不支持 WebSocket 协议的情况下,很多时候我们会使用如下方法来实时获取消息:
1. ajax 轮询
实现方法:每隔一段时间发送一次请求获取新信息 缺点:被动;每次都要新建立 HTTP连接,耗费资源
2. long pull
实现方法:不收到回复一直保持连接,关闭后请求关闭,后续请求需要再重新建立连接。 阻塞性:被动;耗费资源
针对上面的情况,WebSocket 应运而生。它是怎样解决上面的问题的呢?WebSocket 和 HTTP 他们的关系可以用下图来展示,他们之间有交集关系,但是却完全不同。
WebSocket 整个连接过程是这样的:
- 首先 WebSocket 借用 HTTP 实现握手:
Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,da;q=0.7,zh-TW;q=0.6 Cache-Control: no-cache Connection: Upgrade Host: xxx.com Origin: xxx.com Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: FjXHlYXn8caX+nvzj/oCWA== Sec-WebSocket-Version: 13 Upgrade: websocket User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
其中
Upgrade: websocket Connection: Upgrade
就是 WebSocket 的核心,服务器收到后知道发起的是 WebSocket 协议,而不是 HTTP 协议。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器 会验证是不是 WebSocket 的处理。
Sec-WebSocket-Protocol: 用户定义的字符串,用来区分服务。
Sec-WebSocket-Version: 指定 协议版本 Websocket Draft,最初 协议还没有统一,不同浏览器会有不同问题,现在已经定下来了。
Status Code: 响应的状态码101,表示切换了协议,说明利用http建立传输层的TCP连接,之后便与http协议无关了
- 后续我们收到浏览器的回复信息,表明已经建立了 WebSocket。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
这里 浏览器已经知道 成功从 HTTP 协议 切换到 WebSocket 协议,因为 WebSocket 是 基于 TCP 连接的,且浏览器识别到是 WebSocket 协议,所以可以实现长久连接。
多直播应用场景中的具体问题
在开发过程中,我们的整体思路如下:
我们的核心需求是 同一个页面 分 8个窗口来展示直播视频画面,这个过程中,我们遇到了如下问题:
- 视频的格式需要额外的解析
- 直播数最大为 6,其余视频画面 loading
- 播放时间较长后,页面出现卡顿以及崩溃
- 确保 连接长期有效
以下是我们找到的行之有效的方法:
- 问题 1 :直播格式为 flv,需要解析格式。当前, video 元素支持三种视频格式: MP4, WebM, 和 Ogg:
浏览器 | MP4 | WebM | Ogg |
---|---|---|---|
Internet Explorer | YES | NO | NO |
Chrome | YES | YES | YES |
Firefox | YES | YES | YES |
Safari | YES | NO | NO |
Opera | YES(从 Opera 25 起) | YES | YES |
因此需要插件来解析 flv 格式的视频,这里我们使用了 blibli/flv.js 来解析。git 地址: github.com/bilibili/fl… 。 具体使用 如下:
if (flvjs.isSupported()) {
this.flvPlayer = flvjs.createPlayer({
type: "flv",
url: this.src
}, {
enableStashBuffer: false,
autoCleanupSourceBuffer: true,
autoCleanupMaxBackwardDuration: 1,
autoCleanupMinBackwardDuration: 1
});
this.flvPlayer.attachMediaElement(this.videoPlayer);
this.flvPlayer.load(); // 加载
this.flvPlayer.play();
}
- 问题 2:最多播放 6个 视频画面,多余的一直处于 loading 状态。这就涉及到 浏览器的 请求并发数问题。 浏览器有并发请求数目限制,并发请求数目限制是针对同一域名的,即一时间针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。 所以超过6个后,新加载的直播视频都是 loading 状态。
所以 针对这个问题,我们的解决方案如下:
① 给定一组域名
② 同组域名指向同一个源
③ 接口中随机返回域名
本次的开发中,我们采用了两组域名,对直播流进行加载,就可以满足我们 对于 8个视频的需求了。
由上面的背景,我们可以来了解一下一些主流浏览器对HTTP 1.1和HTTP 1.0的最大并发连接数目,参考如下表格:
- 问题 3:直播一旦建立连接后,后续不会停止推流。 它的具体表现如下: ① 加载过的直播会持续推流,无法停止加载。
在控制台中,我们发现 直播建立连接后,开始持续推流。当我们卸载 包裹直播的组件,一开始建立的连接仍然在持续推流,除非卸载掉整个页面。这个问题会造成页面的占用的内存很大,导致页面的卡顿甚至崩溃。
针对这个问题,我们想到了一个方法:将 直播组件用 iframe 包裹。这样在卸载组件的时候,相当于卸载了一个iframe, 问题就可以解决。
② 播放较长时间后,占用内存过大,出现卡顿现象。为此,我们添加了定时刷新直播视频的功能,相当于定时 清掉之前直播推流的缓存。代码如下:
// 父组件 每个10min 发出刷新指令
this.refreshAllVideosTimer = setInterval(() => {
window.EventBus.emit("refreshLiveVideo");
}, this.refreshTime);
// 子组件监听 父组件的 刷新指令,并判断自己插入的时间是否符合超过 20min,满足条件可刷新
componentDidMount() {
window.EventBus.on("refreshLiveVideo", () => {
const newTime = Date.now();
if (newTime - this.initialTime > refreshTime) {
this.refresh();
this.initialTime = newTime;
}
});
}
在这里我们使用了 EventBus 。假想如果我们针对单个直播卡片进行是否刷新,首先我们的外层需要维持一个 复杂的对象,来记录每个卡片的进入时间 以及 播放时长,每个卡片还有增删的功能,这就导致 这个记录时间的对象 非常难维护。使用EventBus, 只需要父组件定时发出刷新指令,卡片内部去记录自己的时长记忆是否满足刷新条件,自己进行响应。这样就可以极大的缓解持续的推流 占用非常多的资源问题。
- 问题4 如何保障 WebSocket 长期有效 WebSocket 的心跳机制定义:在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。
如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连。 在我们开发的过程中,定义浏览器发出 "ping" 三次没有收到回复,即为断开,进行重连。
具体代码 实现如下:
// 心跳
heartCheck() {
this.clearHeartInterval();
this.pingInterval = setInterval(() => {
this.pingPong = "ping";
this.status = this.ws.readyState;
if (this.status === OPEN) {
this.sendMessage(JSON.stringify(this.heartParam));
} else if ([CLOSING, CLOSED].includes(this.status)) {
this.onClose();
}
}, 5000);
this.pongInterval = setInterval(() => {
if (this.pingPong === "pong") {
this.heartCount = 3;
return;
}
if (this.heartCount > 0) {
this.heartCount -= 1;
} else {
this.reconnect();
}
}, 6000);
}
// 重连
reconnect = () => {
this.connect("reconnect");
}
// 异常情况关闭监听
onClose() {
this.ws.onclose = () => {
this.clearHeartInterval();
console.log("链接已关闭");
if (this.status === "serverClose") {
message.error("连接已断开");
} else {
this.closeInterval = setInterval(() => { this.reconnect(); }, 5000);
}
};
}
以上就是 单个页面实现多直播同时播放审核的开发思路预计遇到的问题,如有建议或者问题,可随时沟通 ♪(・ω・)ノ