被多个姑娘拒绝是一种什么体验?很荣幸,我体会到了。这种心路历程,对于我这么一个大龄青年,一开始心酸,现在是坦然的接受,同时煮几碗心灵鸡汤,告诉自己不管这特么看上去是多么糟糕,你也应该积极拥抱这个世界。
STF 部署之后,如何让北京、上海的设备进行共享成为了一个必然的需求。
然而这其中有很多不确定性:
其实,我在之前的一篇文章[STF] 二次开发之批量安装
做过 STF 拓扑结构的介绍,其中介绍过 Provider 这个概念。
现在回顾一下:
provider 用来连接 adb,然后为每个设备启动一个 worker process。之后接受并发送从这些 processors 传过了的命令。
provider 也表示你的宿主机,这个服务你部署在哪台机器上就代表是哪台机器接入,也即设备连在哪台主机上。另外,注意几个关键词连接adb
,这说明 provider 只要给定 ADB 的 IP 地址和端口信息,他就能进行设备管理。然而,provider 这个服务有个问题,底层调用的是 adbkit 的 createClient 方法,这个只能连接一台主机。聪明的同学肯定立刻意识到,启动多个 provider 不就可以了么?没错,当前阶段正式采用这种方案。
那么,provider 服务应该在哪里呢?
关于答案,我认为两种都可以。理由是 STF 的模块(服务)架构相对比较独立,就想 deployment.md 文档中描述的一样,可分布式部署。然而,目前我采用的是方案一,在于相对简便。方案二的问题在于 windows 安装 STF 会有各种问题,不是很顺利。不符合快速迭代的原则,想让用户能用起来。
首先要知道 provider 是一个可以独立工作的模块,当然有依赖,具体依赖什么自行翻看deployment.md
文档。
接下来看看 provider 被启动时的代码,在lib/cli/local/index.js
的 266 行。
// provider
, procutil.fork(path.resolve(__dirname, '..'), [
'provider'
, '--name', argv.provider
, '--min-port', argv.providerMinPort
, '--max-port', argv.providerMaxPort
, '--connect-sub', argv.bindDevPub
, '--connect-push', argv.bindDevPull
, '--group-timeout', argv.groupTimeout
, '--public-ip', argv.publicIp
, '--storage-url'
, util.format('http://localhost:%d/', argv.port)
, '--adb-host', argv.adbHost
, '--adb-port', argv.adbPort
, '--vnc-initial-size', argv.vncInitialSize.join('x')
, '--mute-master', argv.muteMaster
]
.concat(argv.allowRemote ? ['--allow-remote'] : [])
.concat(argv.lockRotation ? ['--lock-rotation'] : [])
.concat(!argv.cleanup ? ['--no-cleanup'] : [])
.concat(argv.serial))
provider 启动时,会传递各个命令行参数,这些参数是支持分布式部署的外部接口。
挑几个我用到的说一下。
--name
:这个参数就是设备所在机器的名称,可以自己定义。如果部署在用户的机器上,默认获取的是用户的计算机名称。目前的方案是创建一个客户端,通过InetAddress
获取计算机名。--connect-sub
:这个是丙丁设备的 Pub 端口,用来接收数据。(可以通过 ps 命令获取 local provider 的参数信息,拷贝帖进去就行了。)--connect-push
:这个是设备的 Pull 端口发送数据。--public-ip
:这个就是主机的公共 IP 了,既然在服务上,那就是服务器的 ip 地址。--storage-url
:这个要注意,不能填http://localhost
,这个是在浏览器端进行跳转到服务端的。所以要填服务器的 ip 地址,http://192.168.1.8/
--adb-host
:这个是 adb 的主机,这里没有填主机,填的是127.0.0.1
,后面会说到,因为服务器不能访问办公及本机,需要网络穿透。不过,如果采用方案二,以我对 provider 的理解,我认为可以是真正主机的 ip 地址。--adb-port
:这个就是 adb 所在机器的 adb 端口了,当然这里面也是通过网络穿透,并非是实际的5037
端口--allow-remote
:很容易理解,是否允许远程接入。通过这些参数,能获得什么信息?
没错,只要知道个参数的地址,provider 就会自动将设备接入整个系统,它根据你传进的 sub, pub, adb-host, port 寻找相应的 zmq 及 adb-server。
所以,一台主机接入就启动一个 provider 服务,一个 provider 可以支持多个设备连接,每个 provider 为每个设备启一个 worker process。(不理解的,反复阅读文档吧,读书百遍,其义自现)
前文已述,因为服务器不能访问办公室机器,需要中间层做一个代理,即所谓的网络穿透,可以将访问通过代理连入办公机。
# frps.ini
[common]
bind_port = 7000
[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 5037
remote_port = 6668
type
:连接类型,标识 tcp,我们通过 tcp 与服务端通信。local_ip
:这是本机的 ip 地址。local_port
:实际需要监听的端口,因为 adb 的端口是 5037,所以我们监听这个端口。remote_port
:这个是 STF 服务端要调用的端口,通过这个端口连接,区分是机器的连入。
通过以上分析,思路基本就有了。通过 FRP 做为网络中转,客户端通过 FRP 连入服务器,服务器接受到请求后自动创建 provider。
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.TextArea?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<Text text="Configuration" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2" />
<Label text="Access Token:" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<TextField fx:id="accessTokenTextField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label text="API:" GridPane.columnIndex="0" GridPane.rowIndex="2" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="2" fx:id="apiTextField"/>
<Label text="Server Address:" GridPane.columnIndex="0" GridPane.rowIndex="3" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="3" fx:id="serverAddrTextField"/>
<Label text="Server Port:" GridPane.columnIndex="0" GridPane.rowIndex="4" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="4" fx:id="serverPortTextField"/>
<Label text="Type:" GridPane.columnIndex="0" GridPane.rowIndex="5" />
<TextField fx:id="typeTextField" GridPane.columnIndex="1" GridPane.rowIndex="5" text="tcp" editable="false" disable="true"/>
<Label text="Local IP:" GridPane.columnIndex="0" GridPane.rowIndex="6" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="6" fx:id="localIpTextField"/>
<Label text="Local ADB Port:" GridPane.columnIndex="0" GridPane.rowIndex="7" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="7" fx:id="localAdbPortTextField"/>
<Label text="Remote Port:" GridPane.columnIndex="0" GridPane.rowIndex="8" />
<TextField GridPane.columnIndex="1" GridPane.rowIndex="8" fx:id="remotePortTextField"/>
<HBox spacing="10" alignment="BOTTOM_RIGHT" GridPane.columnIndex="1" GridPane.rowIndex="9">
<Button text="#1 Save Configuration" onAction="#handleSaveConfigAction" />
<Button text="#2 Start Frp Client" onAction="#handleStartFrpClientAction" />
<Button text="#3 Connect to Server" onAction="#handleConnectAction" />
</HBox>
<Label text="Config Status: " GridPane.columnIndex="0" GridPane.rowIndex="10" />
<Text fx:id="configStatusText" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.halignment="RIGHT" GridPane.rowIndex="10" />
<Label text="Frp Status: " GridPane.columnIndex="0" GridPane.rowIndex="11" />
<Text fx:id="frpStatusText" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.halignment="RIGHT" GridPane.rowIndex="11" />
<Label text="Server Status: " GridPane.columnIndex="0" GridPane.rowIndex="12" />
<TextField fx:id="serverStatusTextField" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.halignment="RIGHT" GridPane.rowIndex="12" />
</GridPane>
package sample;
import frp.Common;
import frp.FRP;
import frp.Ssh;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.scene.text.Text;
import stf.STF;
import util.PlatformUtil;
import util.PropertyUtil;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable {
// UI Fields
@FXML
private TextField accessTokenTextField;
@FXML
private TextField apiTextField;
@FXML
private TextField serverAddrTextField;
@FXML
private TextField serverPortTextField;
@FXML
private TextField typeTextField;
@FXML
private TextField localIpTextField;
@FXML
private TextField localAdbPortTextField;
@FXML
private TextField remotePortTextField;
@FXML
private Text configStatusText;
@FXML
private Text frpStatusText;
@FXML
private TextField serverStatusTextField;
// Settings
private PropertyUtil propUtil;
private FRP frp;
private Common commonSettings;
private Ssh sshSettings;
// STF
private STF stf;
public Controller() {
}
public void handleConnectAction(ActionEvent actionEvent) {
try {
saveAccessToken();
saveApi();
String api = propUtil.getApi();
String json = "{\n"
+ "\"computerName\": \"${computerName}\",\n"
+ "\"userName\": \"${userName}\",\n"
+ "\"remotePort\": ${remotePort}\n"
+ "}";
json = json.replace("${computerName}", PlatformUtil.getComputerName() == null ? "unknown" : PlatformUtil.getComputerName())
.replace("${userName}", PlatformUtil.getUserName() == null ? "unknwon" : PlatformUtil.getUserName())
.replace("${remotePort}", sshSettings.getRemotePort());
String tmp = stf.connect(propUtil.getAccessToken(), api, json);
serverStatusTextField.setText(tmp);
} catch (Exception e) {
serverStatusTextField.setText(e.getMessage());
}
}
public void handleSaveConfigAction(ActionEvent actionEvent) {
String serverAddr = serverAddrTextField.getText();
String serverPort = serverPortTextField.getText();
String type = typeTextField.getText();
String localIp = localIpTextField.getText();
String localAdbPort = localAdbPortTextField.getText();
String remotePort = remotePortTextField.getText();
commonSettings.setServerAddr(serverAddr);
commonSettings.setServerPort(serverPort);
sshSettings.setType(type);
sshSettings.setLocalIp(localIp);
sshSettings.setLocalPort(localAdbPort);
sshSettings.setRemotePort(remotePort);
try {
saveAccessToken();
saveApi();
frp.save();
configStatusText.setText("Save config succeed!");
} catch (Exception e) {
configStatusText.setText("Save config failed!");
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
stf = new STF();
propUtil = new PropertyUtil();
accessTokenTextField.setText(propUtil.getAccessToken());
apiTextField.setText(propUtil.getApi());
frp = new FRP();
commonSettings = frp.getCommonSettings();
sshSettings = frp.getSshSettings();
serverAddrTextField.setText(commonSettings.getServerAddr());
serverPortTextField.setText(commonSettings.getServerPort());
localIpTextField.setText(sshSettings.getLocalIp());
localAdbPortTextField.setText(sshSettings.getLocalPort());
remotePortTextField.setText(sshSettings.getRemotePort());
}
public void handleStartFrpClientAction(ActionEvent actionEvent) {
try {
frp.connect();
frpStatusText.setText("FRP Client is running!");
} catch (Exception e) {
frpStatusText.setText("failed! " + e.getMessage());
}
}
private void saveApi() {
String api = apiTextField.getText().trim();
propUtil.setApi(api);
}
private void saveAccessToken() {
String accessToken = accessTokenTextField.getText().trim();
propUtil.setAccessToken(accessToken);
}
}
/providers:
x-swagger-router-controller: provider
post:
summary: Create Provider
description: Create one provider for each connection from client
operationId: createProvider
tags:
- providers
parameters:
- name: clientInfo
in: body
description: Provider for client to create
required: true
schema:
$ref: '#/definitions/CreateProviderPayload'
responses:
"200":
description: Provider Created
schema:
$ref: "#/definitions/ProviderResponse"
security:
- accessTokenAuth: []
新建一个 provider.js 控制器,创建如下代码。
var _ = require('lodash')
var Promise = require('bluebird')
var proc = require('child_process')
var procutil = require('../../../util/procutil')
var path = require('path')
var util = require('util')
var dbapi = require('../../../db/api')
var logger = require('../../../util/logger')
var log = logger.createLogger('api:controllers:provider')
module.exports = {
createProvider: createProvider
}
function createProvider(req, res) {
var computerName = req.body.computerName
var userName = req.body.userName
var remotePort = req.body.remotePort
dbapi.loadProvider(remotePort)
.then(function(provider) {
if(provider) {
return res.status(200).json({
code: 1
, message: 'Remote port already in use!'
, provider: provider
})
}
dbapi.saveProvider({
remotePort: remotePort
, computerName: computerName
, userName: userName
})
.then(function(provider) {
procutil.fork(path.resolve(__dirname, '..'), [
'provider'
, '--name', computerName
, '--min-port', '7400'
, '--max-port', '7700'
, '--connect-sub', 'tcp://127.0.0.1:7114'
, '--connect-push', 'tcp://127.0.0.1:7116'
, '--group-timeout', '900'
, '--public-ip', '192.168.177.15'
, '--storage-url'
, util.format('http://192.168.177.15:%d/', '80')
, '--adb-host', '127.0.0.1'
, '--adb-port', remotePort
, '--vnc-initial-size', '600x800'
, '--mute-master', 'never'
, '--allow-remote'
])
return res.status(200).json({
code: 0
, message: 'Established connection!'
, provider: provider
})
})
})
}
数据库的改造比较简单
, providers: {
primaryKey: 'remotePort'
}
dbapi.loadProvider = function(remotePort) {
return db.run(r.table('providers').get(remotePort))
}
dbapi.saveProvider = function(provider) {
return db.run(r.table('providers').insert({
remotePort: provider.remotePort
, computerName: provider.computerName
, userName: provider.userName
}))
}
http://xxx.xxx.xxx/api/v1/providers
就能创建 provider,支持办公机接入。Settings->Keys
里面。完
本文一开始说了两个方案,个人希望尝试一下方案二,这样就可以减轻服务端的压力,办公机器也撑得起这点进程开销。后续再改吧。
另,如果需要客户端完整代码的话,我会贴在评论区里面。