基于 websocket 的多端桥接平台

咱们如今的业务是基于新闻客户端实现的,都要通过新闻客户端的环境,进行先后端数据上的交互。可是咱们在调试过程当中,很是的不方便。javascript

一般使用的工具备:modheader, postman, fiddler 等,但这些工具都会存在的问题:html

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

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

1. 要调试什么

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

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

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

2. websocket 的特性

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

websocket与http的对比

其余特色包括:redis

  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 端会出现一个对应的图标。api

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 端便可:

websocket的客户端调试-蚊子的博客

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. 适用在大规模的应用;<br/>2. 某个主机或者进程挂掉后,影响小 实现比较复杂
nginx 分配 自带的 ip_hash 可实现负载均衡;<br/>同一 ip 会被分配给固定的后端服务器 配置方便 可能会集中到某个进程中

我这里的平台是内部的调试平台,用户量不大,杀鸡焉用牛刀,并且咱们只有一台机器,所以咱们考虑的是同一个 IP 进入到同一个进程中。这里我借用里 nginx 中的 ip_hash 思想:当请求来到主进程后,我这里对 IP 进行加权计算后,而后按照进程的个数进行取模。

显然这种方式也有可能存在一个进程中 socket 链接过多的问题,不过在用户量很少的时候彻底能够接受(针对这个问题我也考虑了别的方法,例如瀑布流的方式,每次给子进程分配链接的时候,都首先获取到链接数最少的那个进程,而后链接分配给这个进程,不过还要维护一个表,每次都要计算)。

6.4 多进程之间的通讯

同一个房间里,当 PC 端的 socket 链接和多个移动端的链接不在同一个进程中时,就会存在跨进程的问题。一个极端的例子,每一个 socket 链接都在不一样的进程中,那么就要考虑如何通知其余的进程,须要给客户端发送请求了。

比较简单的方式利用咱们的机制,每一个 PC 端的用户就是房主,能够建立一个房间,移动设备就是房间中的成员,每一个房间都是独立的,互不干扰。这样咱们把房间里全部的 socket 链接,经过房间的标识,都放到同一个进程中,这样就没有跨进程的问题了。但这种方式存在的一个问题是:一个房间里的链接过多时,都须要这同一个进程来承担,而别的进程却闲着的。

还有可使用 redis:利用 redis 的发布/订阅者模式,将当前进程中的房间标识和信息广播到其余的进程中,其余进程中有相同房间标识的 socket 链接,进行相应的操做。

欢迎个人公众号,多多交流: 蚊子的博客

原文出处:https://www.cnblogs.com/xumengxuan/p/12582184.html

相关文章
相关标签/搜索