我们现在的业务是基于新闻客户端实现的,都要经过新闻客户端的环境,进行前后端数据上的交互。但是我们在调试过程中,非常的不方便。

通常使用的工具有:modheader, postman, fiddler 等,但这些工具都会存在的问题:

  1. 缺少客户端里相应的设备信息;
  2. 即使将 cookie 信息复制出来,也是存在过期的问题;
  3. 多个设备之间切换时不方便;

针对这些存在的问题和不足,我基于 websocket 双向通信的特点,并实现了“多端桥接管理平台”:通过在 PC 端上的操作,可以直接在新闻客户端内直接执行相应的命令,并将结果、cookie、设备信息等一起返回到 PC 端。

1. 要调试什么

我们主要要知道调试什么,最终回去到什么样子的结果:

  1. 调试接口,传入接口地址,即可获取对应的结果;并且可以同时调试多个设备;
  2. 调试 jsapi,输入对应的方法,则即可在新闻客户端中展示出效果。

在调试接口方面,其实我们有一种方法可以方便地进行调试,但有两个限制条件:Android系统测试版的客户端,这样通过 Chrome 浏览器进行桥接。但这种方式,在 iOS 系统和正式版的客户端中,就失效了。

2. websocket 的特性

WebSocket 协议的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

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

3. 建立 socket 连接

为了满足我们在第 1 部分设置的调试目标,我们这里要实现的功能有:

  1. PC 端相当于房主,建立房间后,其他设备可以进入到该房间,一个设备只能进入到一个房间中;
  2. 客户端有断线重连的机制,当客户端断开连接后,可以尝试重连;
  3. 服务端维护一个心跳检测的机制,当有新设备进入或者之前的设备退出时,要及时地更新当前房间中的设备列表;

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 的多端桥接平台的更多相关文章

  1. 分享基于 websocket 网页端聊天室

    博客地址:https://ainyi.com/67 有一个月没有写博客了,也是因为年前需求多.回家过春节的原因,现在返回北京的第二天,想想,应该也要分享技术专题的博客了!! 主题 基于 websock ...

  2. 基于node.js 的 websocket的移动端H5直播开发

    这一篇介绍一下基于node.js 的 websocket的移动端H5直播开发, 下载文章底部的源码,我是用vscode打开, 首先在第一个终端运行 npm run http-server 这个指令是运 ...

  3. Socket.IO – 基于 WebSocket 构建跨浏览器的实时应用

     Socket.IO 是一个功能非常强大的框架,能够帮助你构建基于 WebSocket 的跨浏览器的实时应用.支持主流浏览器,多种平台,多种传输模式,还可以集合 Exppress 框架构建各种功能复杂 ...

  4. 基于 WebSocket 构建跨浏览器的实时应用

    Socket.IO – 基于 WebSocket 构建跨浏览器的实时应用 Socket.IO 是一个功能非常强大的框架,能够帮助你构建基于 WebSocket 的跨浏览器的实时应用.支持主流浏览器,多 ...

  5. 基于 WebSocket 的 MQTT 移动推送方案

    WebSphere MQ Telemetry Transport 简介 WebSphere MQ Telemetry Transport (MQTT) 是一项异步消息传输协议,是 IBM 在分析了他们 ...

  6. 在线Online表单来了!JeecgBoot 2.1 版本发布——基于SpringBoot+AntDesign的快速开发平台

    项目介绍 Jeecg-Boot 是一款基于SpringBoot+代码生成器的快速开发平台! 采用前后端分离架构:SpringBoot,Ant-Design-Vue,Mybatis,Shiro,JWT. ...

  7. 基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)

    我们上一篇<基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(一)>主要讲解了如何搭建一个实时数据通讯服务器,客户端与服务端是如何通讯的,相信通过上一篇的讲解,再配 ...

  8. 高效简易开发基于websocket 的通讯应用

    websocket的主要是为了解决在web上应用长连接进行灵活的通讯应用而产生,但websocket本身只是一个基础协议,对于消息上还不算灵活,毕竟websocket只提供文本和二进制流这种基础数据格 ...

  9. 基于AgileEAS.NET企业应用开发平台的分布式解决方案

    开篇 分布式应用 AgileEAS.NET基于Microsoft .Net构件技术而构建,Microsoft .Net最吸引人的莫过于分布式应用技术,基已经提供了XML WebService. .Ne ...

随机推荐

  1. Proto3:Arena分配指南

    Arena分配是仅C++有的功能,在使用Protocol Buffer时,它可以帮助你优化你的内存使用,提高性能.在.proto文件中启用Arena分配会在生成的C++代码中添加处理Arena分配的额 ...

  2. 容易出错的JavaScript题目集锦

    容易出错的JavaScript题目集锦 1.typeof(null) 会得到什么?object,在JavaScript中null被认为是一个对象. 2.下列代码将输出控制台的是什么?为什么? 1234 ...

  3. doctrine queryBuilder

    为了能够方便的切换数据库,我们有必要使用doctrine的queryBuilder, 但是估计很多人都是不喜欢的(我也是),之前尝试用的时候,发现在doctrine定义的SELECT语法中并没有CON ...

  4. Html学习笔记(二) 简单标签

    标签的重点 标签的用途 标签在浏览器中的默认样式 <body>标签: 在网页上显示的内容 <p>标签: 添加段落 <hx>标签: 添加标题 标签一共有6个,h1.h ...

  5. 使用hexo,创建博客

    下载hexo工具 1 npm install hexo-cli -g 下载完成后可以在命令行下生成一个全局命令hexo搭建博客可用thinkjs 创建一个博客文件夹 1 hexo init 博客文件夹 ...

  6. 30s源码刨析系列之函数篇

    前言 由浅入深.逐个击破 30SecondsOfCode 中函数系列所有源码片段,带你领略源码之美. 本系列是对名库 30SecondsOfCode 的深入刨析. 本篇是其中的函数篇,可以在极短的时间 ...

  7. Python 十大语法

    前言 Python 是一种代表简单思想的语言,其语法相对简单,很容易上手.不过,如果就此小视 Python 语法的精妙和深邃,那就大错特错了.本文精心筛选了最能展现 Python 语法之精妙的十个知识 ...

  8. Ubuntu gnome 美化与个别组件安装

    1. 安装 gnome 工具 sudo apt install gnome-tweaks# 打开时中文直接选择 "优化" 即可 1.1. gnome 官网 # https://ex ...

  9. Appium自动化测试实践

    安装配置Appium环境 安装appium 安装appium有两种方式,一种是通过命令行终端方式,一种是Appium Desktop.这里推荐使用Appium Desktop方式,只需下载下来安装就可 ...

  10. React项目实战:react-redux-router基本原理

    React相关 React 是一个采用声明式,高效而且灵活的用来构建用户界面的框架. JSX 本质上来讲,JSX 只是为React.createElement(component, props, .. ...