Nohost 实现原理 —— 远程办公下的开发测试协同提效方案

312次阅读  |  发布于2年以前

在《[Whistle 实现原理 —— 从 0 开始实现一个抓包工具] 》一文中给大家详细介绍了如何实现一个抓包调试工具 Whsitle。事实上 Whistle 不仅可以供开发人员本地开发调试,也可以部署到公共服务器给产品运营、测试等非开发人员用来访问测试环境、远程抓包调试。但 Whistle 是单进程服务,无法满足多人多项目同时使用,所以基于 Whistle 开发了多进程多用户的远程代理服务 -- Nohost,其主要功能:

Github 仓库: https://github.com/Tencent/nohost 有关 Whistle 的实现原理参见:[Whistle 实现原理 —— 从 0 开始实现一个抓包工具] 。

  1. 团队每个成员或业务模块分配一个 Whistle 实例。
  2. 每个 Whistle 实例可以设置多个独立的环境。
  3. 每个环境可以配置任意 Whistle 规则。
  4. 用户可以通过页面选择需要访问的环境。

更详细的场景和功能描述请参考:《[Nohost 开源啦!] 》。

Nohost 基本架构

有点类似 Node 的 Cluster 模块,其核心思想是:将多进程多问题转成单进程 Whistle 的问题,即给每个团队成员或业务模块分配一个 Whistle 进程,并通过一个 Master 进程来管理各个 Whistle 进程以及分发请求。

交互流程图及实现原理

1 . 访问接入 Nohost 的页面,Nohost 会在页面左下角注入一个小圆点(环境切换按钮)(具体原理后面讲)。

如何接入 Nohost 参见: https://nohost.pro/docs/quickstart

2 . 点击小圆点弹出环境选择框,点击选择环境后,页面会先将选择的 账号/环境 发送到 Nohost 的 Master 进程

3 . Master 进程会先获取 clientId(如何获取参见后面的 Master 进程实现原理),将获取的 clientId 与选择的 账号/环境 存到 Master 进程的内存 LRU Cache 里面。

4 . 页面等待设置环境的接口响应或超时后自动刷新页面,这时页面所有请求也会经过 Master 进程,Master 进程通过 3 的方式获取每个请求的 clientId,并根据 clientId 获取之前选择的 账号/环境 ,再将请求转到账号对应的 Whistle 进程及执行对应环境的规则(具体实现参见后面的 Master 进程实现原理)。

这样每个人或业务只需关注自己的 Whistle 进程,功能上也就跟本地的 Whistle 差不多,也可以直接使用所有 Whistle 插件。

Master 进程实现原理

最后看下 Master 进程的实现,Master 进程可以主要有三个功能:

  1. 注入小圆点(环境切换按钮)。
  2. 记录用户环境选择状态。
  3. Whistle 进程管理与请求分发。

启动(关闭)指定 Whistle 进程并将请求转发到该进程及执行对应环境规则

注入小圆点(环境切换按钮)

小圆点的注入不是简单的往页面追加脚本,还涉及到:

  1. 解析 HTTPS 请求(不然 HTTPS 请求无法注入任何内容)。
  2. 只对 HTML 页面注入小圆点(还需要排除一些返回 json 数据,但类型写成 HTML 的接口)。Nohost 采用在 Master 里面内置一个 Whistle 进程处理上述问题:

const startWhistle = require('whistle')

其中:

  1. 接收请求、解析 HTTPS 请求原理参见:[Whistle 实现原理]
  2. 注入小圆点,主要是由内置插件 whistle.nohost 实现的,分三部分内容:

这里引入了一个插件 whistle.nohost,该插件除了上述注入小圆点功能以外,还有如下的功能。

记录用户环境选择状态

点击选择环境,页面会发送请求 /.whistle-path.5b6af7b9884e1165/whistle.nohost/cgi-bin/select?name=imweb&envId=test&time=1637026271911 ,其中:

  1. /.whistle-path.5b6af7b9884e1165 是一个特殊路径,Whistle 自动拦截该路径并作为内部请求。
  2. name:账号(分组)名称。
  3. envId:环境名称的编码,encodeURIComponent(envName)

该请求会转到 Nohost 的内置的 whistle.nohost 插件进程的 uiServer,该插件会通过以下方式获取 clientId

  1. 先尝试从请求头 x-whistle-client-id 获取 clientId
  2. 如果没有请求头 x-whistle-client-id,则分别通过请求头 x-forwarded-forreq.socket.remoteAddress 获取 clientIp 作为 clientId

将获取的 clientIdname/env 存到插件进程的 LRU Cache:lru.set(clientId, name/env)

Whistle 进程管理与请求分发

对 Worker 里面的 Whistle 进程的管理也是通过 whistle.nohost 插件实现的:

  1. 插件通过实现 rulesServertunnelRulesServer 设置转发规则。
  2. 插件用上面的方式获取 clientId 并根据 clientId 获取用户选择的账号和环境。
  3. 如果没有选择任何环境,则返回空字符串,请求自动转到现网。
  4. 如果有选择环境,则通过 pfork 启动一个 Whistle 进程,并自动分配一个随机端口。
  5. 启动 Whistle 成功后获取端口,设置规则 * internal-proxy://127.0.0.1:xxxxx 让 Master 的 Whistle 进程将请求转到该端口。

internal-proxy 是 Whistle 代理直接转发协议,它可以通过用 http 的方式转发 https 请求

Nohost 插件会自动根据请求需求拉起对应进程,且如果该进程有超过 6 分钟没有请求,则会自动关闭。可以通过 Whistle 提供的方法检测进程是否有请求详见: https://github.com/Tencent/nohost/blob/master/lib/plugins/whistle.nohost/lib/whistle.js#L126 。

插件的完整实现代码参见: https://github.com/Tencent/nohost/blob/master/lib/plugins/whistle.nohost/lib/rulesServer.js

参考资料

  1. Github 仓库: https://github.com/Tencent/nohost
  2. 详细文档: https://nohost.pro/

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8