基于 websocket 的多端桥接平台
我们现在的业务是基于新闻客户端实现的,都要经过新闻客户端的环境,进行前后端数据上的交互。但是我们在调试过程中,非常的不方便。
通常使用的工具有:modheader, postman, fiddler 等,但这些工具都会存在的问题:
- 缺少客户端里相应的设备信息;
- 即使将 cookie 信息复制出来,也是存在过期的问题;
- 多个设备之间切换时不方便;
针对这些存在的问题和不足,我基于 websocket 双向通信的特点,并实现了“多端桥接管理平台”:通过在 PC 端上的操作,可以直接在新闻客户端内直接执行相应的命令,并将结果、cookie、设备信息等一起返回到 PC 端。
1. 要调试什么
我们主要要知道调试什么,最终回去到什么样子的结果:
- 调试接口,传入接口地址,即可获取对应的结果;并且可以同时调试多个设备;
- 调试 jsapi,输入对应的方法,则即可在新闻客户端中展示出效果。
在调试接口方面,其实我们有一种方法可以方便地进行调试,但有两个限制条件:Android系统
和测试版的客户端
,这样通过 Chrome 浏览器进行桥接。但这种方式,在 iOS 系统和正式版的客户端中,就失效了。
2. websocket 的特性
WebSocket 协议的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
3. 建立 socket 连接
为了满足我们在第 1 部分设置的调试目标,我们这里要实现的功能有:
- PC 端相当于房主,建立房间后,其他设备可以进入到该房间,一个设备只能进入到一个房间中;
- 客户端有
断线重连
的机制,当客户端断开连接后,可以尝试重连; - 服务端维护一个
心跳检测
的机制,当有新设备进入或者之前的设备退出时,要及时地更新当前房间中的设备列表;
3.1 如何创建房间
在浏览器上输入房间的标识,若浏览器与服务端成功建立起 websocket 连接后,则在浏览器端创建对应的二维码。用微信/手 Q 或者其他扫描二维码的设备进行扫描,即可通过提前设定的 scheme 协议,跳转到新闻客户端里对应的调试页面。
若客户端里也与服务端成功建立 websocket 连接后,则相当于进入房间成功,PC 端会出现一个对应的图标。
ws.open(serverId)
.then(() => {
// PC 端成功建立连接后
setStatus("linked"); // 更新页面的状态
// 生成二维码
qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => {
setCodeUrl(url);
});
})
.catch(e => {
// 建立连接失败
console.error(e);
Modal.error({ title: "当前服务器出现问题啦,正在抢修中" });
setStatus("unlink");
});
3.2 客户端的断线重现机制
在移动端中的页面有个特点,当屏幕黑屏后,或者因为其他的原因,客户端会自动断开 socket 连接。
为了方便进行调试,而不是每次在断开连接后,需要手动点击,或者重新进入页面。我在这里实现了一个简单的断线重连机制。websocket 连接断开时,会执行onclose
的回调,因此,我们可以在 onclose 事件中进行再次重连的机制。
同时,为了防止无限制的重连尝试,我在这里也进行了下限制,最多重连 3 次,3 次后还没有重新连接上,则停止连接;若重连成功,则将重连次数重置为 3。
断开连接时:
// 断开连接时
ws.onclose(() => {
timer = setTimeout(() => {
setStatus("unlink");
setCodeUrl("");
}, 500);
reconnectNum--;
// 限制重连的次数
if (reconnectNum >= 0) {
_open(); // 尝试重新连接
}
});
连接成功时:
ws.open(serverId).then(() => {
// PC 端成功建立连接后
+reconnectNum = 3;
+timer && clearTimeout(timer);
setStatus("linked"); // 更新页面的状态
// 生成二维码
qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => {
setCodeUrl(url);
});
});
3.3 心跳检测
就像我们在 QQ 群里聊天一样,哪个人在线要一目了然,若有人进入到聊天群,或者有人退出了,都要通知房主,并及时地更新群列表。
心跳检测主要有 2 种方式:客户端发起的心跳检测和服务端维护的心跳检测。我们稍微讲解下这两种:
- 客户端发起的心跳:每隔一段固定的时间,向服务器端发送一个 ping 数据,如果在正常的情况下,服务器会返回一个 pong 给客户端,如果客户端通过 onmessage 事件能监听到的话,说明请求正常。
- 服务端维护的心跳:每隔一段时间,检测所有连接的状态,若状态为断开时,则将其从列表中剔除。
我在这里使用的是服务端维护的心跳检测
,当房间里的设备数量发生变化时,则服务端向客户端推送最新的设备列表:
// 持续监测客户端的连接状态
// 若已断开连接,则将客户端清除
let aliveClients = new Map();
let lastAliveLength = new Map();
setInterval(() => {
let clients = {};
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) {
return ws.terminate();
}
const serverId = ws.serverId;
if (clients[serverId]) {
clients[serverId].push(ws);
} else {
clients[serverId] = [ws];
}
ws.isAlive = false;
ws.ping(() => {});
});
for (let serverId in clients) {
aliveClients.set(serverId, clients[serverId]);
const length = clients[serverId].length;
// 若当前serverId连接的设备数量发生变化,则发送消息
if (length !== lastAliveLength.get(serverId)) {
// 想当前所有serverId的设备发送消息
sendAll("devices", clients[serverId], serverId);
// 存储上次当前serverId的连接数
lastAliveLength.set(serverId, length);
}
}
const size = wss.clients.size;
console.log("connection num: ", size, new Date().toTimeString());
}, 2000);
4. 进行接口的调试
我们在第 3 节已经成功把 PC 端和新闻客户端连接起来了,那么怎么进行双端数据的通信?
4.1 接口的调试
我们在这里要传入 3 个字段:
- serverId: 即房间号,服务端要将信息广播给所有带有 serverId 的成员;
- type: 类型,这条指令是要做什么的;
- msg: 传入的参数;
在接口调试的过程中,则传入的参数是:
const params = {
type: "post", // 类型
msg: {
// 参数
url: "https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336"
}
};
当客户端正常完成接口的请求后,则将接口结果、cookie 和设备信息等返回到 PC 端:
// 请求的方法
const post = url => {
if (window.TencentNews && window.TencentNews.post) {
window.TencentNews.post(url, {}, window[id], { loginType: "qqorweixin" }, {});
} else if (window.TencentNews && window.TencentNews.postData) {
window.TencentNews.postData(url, '{"a":"b"}', id, "requestErrorCallback");
}
};
// 移动端向服务端发起的数据
ws.send({
type: "postCb", // 执行的结果
msg: {
method: "post",
result,
cookie: document.cookie,
appInfo
}
});
这样就能在前端展示出结果了,而且是真实的数据请求。
4.2 历史记录的存储
历史记录这块,我们周边的同学在试用的过程中,还是非常迫切需要的需求。要不然每次要测试之前的接口地址时,都需要重新输入或者粘贴,非常不方便。
我们把用户请求的 URL、返回的结果、cookie、设备信息等比较完整的信息存储到 boss 中,而本地只存储历史的 URL,当用户需要再次测试之前的接口时,点击一下即可。若需要查看之前调试的接口,可以去鹰眼上进行查看。
本地采用的是localStorage
的方式进行存储。还有更重要的是,我们也使用mobx
的响应式工具,能够在用户完成这次请求后,马上在侧边的历史记录里看到结果。
5. 新闻客户端内 jsapi 的调试
除了可以调试接口外,还可以进行一些新闻客户端内的 jsapi 调试。我们新闻客户端的 jsapi 有两种调用的方式:
// 直接调用
window.TencentNews.login("qqorweixin", isLogined => console.log(isLogined));
// invoke方式调用
window.TencentNews.invoke("login", "qqorweixin", isLogined => console.log(isLogined));
这里我选择了使用invoke
的方式来调用 jsapi。
PC 端发起 jsapi 的调用:
ws.send({
type: "call",
msg: {
method: method,
params: slice.call(arguments)
}
});
移动端在收到服务端发过来的请求后,进行 jsapi 的调用,并将执行的结果返回到 PC 端即可:
const handleNewsApi = async (msg: any): Promise<any> => {
await tencentReady();
const { method, params } = msg;
return new Promise(resolve => {
window.TencentNews.invoke(method, ...params, (result: any) => {
resolve({ method, result });
});
});
};
6. 总结
到这里,我的“基于 websocket 的多端桥接平台”基本上已经构建完毕了。不过还是有 2 个问题要简要的说明下。
6.1 为什么要手动输入 serverId
最开始想着用户创建房间时,由系统随机产生一个 uuid,但后来想,如果用户刷新页面了,这个 uuid 就会发生变化,导致无法连接到之前的 uuid,所以这里就换成了手动输入。
6.2 如何保证一个客户端的 socket 请求都进入到同一个进程中
当我们后台采用多个进程时,若用户的请求我们不做干预,会造成请求的随机访问,产生 400 的请求,毕竟最开始连接在 A 进程中,现在发起的请求到 B 进程中,B 进程不知道怎么处理了。
这里有多种方式可以进行处理:
方法 | 介绍 | 优点 | 缺点 |
---|---|---|---|
一致性 hash 算法 | 所有的主机和连接都分配到 0 ~ 2^32-1 的虚拟圆中 | 1. 适用在大规模的应用; 2. 某个主机或者进程挂掉后,影响小 |
实现比较复杂 |
nginx 分配 | 自带的 ip_hash 可实现负载均衡; 同一 ip 会被分配给固定的后端服务器 |
配置方便 | 可能会集中到某个进程中 |
我这里的平台是内部的调试平台,用户量不大,杀鸡焉用牛刀,而且我们只有一台机器,因此我们考虑的是同一个 IP 进入到同一个进程中。这里我借用里 nginx 中的 ip_hash 思想:当请求来到主进程后,我这里对 IP 进行加权计算后,然后按照进程的个数进行取模。
显然这种方式也有可能存在一个进程中 socket 连接过多的问题,不过在用户量不多的时候完全可以接受(针对这个问题我也考虑了别的方法,例如瀑布流的方式,每次给子进程分配连接的时候,都首先获取到连接数最少的那个进程,然后连接分配给这个进程,不过还要维护一个表,每次都要计算)。
6.4 多进程之间的通信
同一个房间里,当 PC 端的 socket 连接和多个移动端的连接不在同一个进程中时,就会存在跨进程的问题。一个极端的例子,每个 socket 连接都在不同的进程中,那么就要考虑如何通知其他的进程,需要给客户端发送请求了。
比较简单的方式利用我们的机制,每个 PC 端的用户就是房主,可以创建一个房间,移动设备就是房间中的成员,每个房间都是独立的,互不干扰。这样我们把房间里所有的 socket 连接,通过房间的标识,都放到同一个进程中,这样就没有跨进程的问题了。但这种方式存在的一个问题是:一个房间里的连接过多时,都需要这同一个进程来承担,而别的进程却闲着的。
还有可以使用 redis:利用 redis 的发布/订阅者模式,将当前进程中的房间标识和信息广播到其他的进程中,其他进程中有相同房间标识的 socket 连接,进行相应的操作。
欢迎我的公众号,多多交流:
基于 websocket 的多端桥接平台的更多相关文章
- 分享基于 websocket 网页端聊天室
博客地址:https://ainyi.com/67 有一个月没有写博客了,也是因为年前需求多.回家过春节的原因,现在返回北京的第二天,想想,应该也要分享技术专题的博客了!! 主题 基于 websock ...
- 基于node.js 的 websocket的移动端H5直播开发
这一篇介绍一下基于node.js 的 websocket的移动端H5直播开发, 下载文章底部的源码,我是用vscode打开, 首先在第一个终端运行 npm run http-server 这个指令是运 ...
- Socket.IO – 基于 WebSocket 构建跨浏览器的实时应用
Socket.IO 是一个功能非常强大的框架,能够帮助你构建基于 WebSocket 的跨浏览器的实时应用.支持主流浏览器,多种平台,多种传输模式,还可以集合 Exppress 框架构建各种功能复杂 ...
- 基于 WebSocket 构建跨浏览器的实时应用
Socket.IO – 基于 WebSocket 构建跨浏览器的实时应用 Socket.IO 是一个功能非常强大的框架,能够帮助你构建基于 WebSocket 的跨浏览器的实时应用.支持主流浏览器,多 ...
- 基于 WebSocket 的 MQTT 移动推送方案
WebSphere MQ Telemetry Transport 简介 WebSphere MQ Telemetry Transport (MQTT) 是一项异步消息传输协议,是 IBM 在分析了他们 ...
- 在线Online表单来了!JeecgBoot 2.1 版本发布——基于SpringBoot+AntDesign的快速开发平台
项目介绍 Jeecg-Boot 是一款基于SpringBoot+代码生成器的快速开发平台! 采用前后端分离架构:SpringBoot,Ant-Design-Vue,Mybatis,Shiro,JWT. ...
- 基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)
我们上一篇<基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(一)>主要讲解了如何搭建一个实时数据通讯服务器,客户端与服务端是如何通讯的,相信通过上一篇的讲解,再配 ...
- 高效简易开发基于websocket 的通讯应用
websocket的主要是为了解决在web上应用长连接进行灵活的通讯应用而产生,但websocket本身只是一个基础协议,对于消息上还不算灵活,毕竟websocket只提供文本和二进制流这种基础数据格 ...
- 基于AgileEAS.NET企业应用开发平台的分布式解决方案
开篇 分布式应用 AgileEAS.NET基于Microsoft .Net构件技术而构建,Microsoft .Net最吸引人的莫过于分布式应用技术,基已经提供了XML WebService. .Ne ...
随机推荐
- Centos7上pkg-config的安装
1.官网下载自己想要的版本,我这里下载的是目前最新版 https://www.freedesktop.org/wiki/Software/pkg-config/ 2.安装 tar xf XXX.tgz ...
- C++走向远洋——62(项目二1、类模板)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- git还原历史某一版本
返回上一版本 git reset --hard HEAD^ 常用的命令git refloggit reset --hard "填写版本号" 黄色的就是版本号 其他查看以前版本的命令 ...
- sql -- 获取商品分类的最新销售情况
表设计: 需求: 1.先找出各个分类中销售的最新日期 select prod_class,max(sales_date) as sn from prod_sales group by prod_cla ...
- LeetCode--链表2-双指针问题
LeetCode--链表2-双指针问题 思考问题: 判断一个链表是否有环 列举几种情况: graph LR A-->B B-->C C-->D D-->E E-->C g ...
- MySQL root密码重置问题
1:进入cmd,停止mysql服务:Net stop mysql (进入服务---->MySql----->停止) 到mysql的安装路径启动mysql,在bin目录下使用mysqld-n ...
- MySQL的字符集和乱码问题
1.字符集知识 #概述 .字符集是一套文字符号及其编码.比较规则的集合,第一个计算机字符串ASC2 .mysql数据库字符集包括字符集(character)和 校对规则,其中字符集使用来定义mysql ...
- 说一说 HTML 中的 script 标签
我们在 <Javascript简史>这遍文章中说过,「Javascript」这门语言是由 Netscape开发而来,当初开发的时候为了能让 「Javascript」这门语言能与 HTML ...
- 天坑,CSS之定位Position(六分之五)
Position定位 个人觉得position这个属性真的算是CSS的见面杀了.尤其是absolute,当年可是被虐的不轻.当然了,现在爱上了这个属性,谁用谁知道. position属性 positi ...
- Python - loguru日志库,高效输出控制台日志和日志记录
一.安装loguru loguru的PyPI地址为:https://pypi.org/project/loguru/ GitHub仓库地址为:https://github.com/Delgan/log ...