這篇文章是之前在testerhome發(fā)表過啊奄,自認(rèn)為寫得很不錯辩蛋,現(xiàn)在搬來博客凑术。
上一篇文章分析STF如何同步設(shè)備截圖的挽霉,這一篇分析用戶在前端頁面touch設(shè)備A時防嗡,STF如何將touch動作同步在設(shè)備A?本文將從前端,服務(wù)端侠坎,手機 三個方向說明蚁趁。
touch流程
touch的整個流程如下所示:
其中triproxy/index.js 從pull到pub用虛線標(biāo)識出來原因是,中間省略了一系列過程实胸。這些過程會在服務(wù)端部分詳細說明他嫡。
前端
===
當(dāng)用戶touch設(shè)備時番官,前端如何捕捉用戶動作?捕捉到用戶動作之后钢属,是如何將數(shù)據(jù)傳輸至服務(wù)端徘熔?
捕捉用戶動作-- screen-directive.js
這里說明一下,下文中所有設(shè)備詳情頁指的是http://localhost:7100/#!/control/${serial}
頁面,
設(shè)備詳情頁中看到設(shè)備截圖的那個部分淆党,如下圖所示
是由screen-directive.js 文件管理的酷师。screen-directive.js中除了之前說的渲染圖片二進制文件功能,還可以捕捉用戶點擊染乌,滑動“設(shè)備屏幕”等事件窒升,并調(diào)用ControlService.js 文件中對應(yīng)的函數(shù)。代碼如下所示
element.on('mousedown', mouseDownListener) //當(dāng)用戶觸發(fā)mousedown事件后慕匠,調(diào)用mouseDownListener函數(shù)
function mouseDownListener(){
...
...
control.touchDown(nextSeq(), 0, scaled.xP, scaled.yP, pressure)
...
$document.bind('mouseup', mouseUpListener)
}
當(dāng)用戶在前端點擊設(shè)備上某個位置時饱须,screen-directive.js捕捉了touchDown動作,調(diào)用的是mouseDownListener函數(shù), mouseDownListener會通過$scope.control
(即代碼中的control)調(diào)用ControlService.js的中touchDown函數(shù)台谊,向服務(wù)端發(fā)送消息--點擊該臺設(shè)備的(x,y)位置蓉媳。
至于為什么在control-panes-controller.js中定義的$scope.control
, 能在screen-directive.js使用?這就屬于angular 1.x知識锅铅,感興趣的同學(xué)可以自己去了解酪呻,這里不贅述。
傳輸數(shù)據(jù)至服務(wù)端 -- control-panes-controller.js
該js文件完成$scope.device
,和$scope.control
的初始化
...
function getDevice(serial) {
DeviceService.get(serial, $scope)
.then(function(device) {
return GroupService.invite(device)
})
.then(function(device) {
$scope.device = device
$scope.control = ControlService.create(device, device.channel)
// TODO: Change title, flickers too much on Chrome
// $rootScope.pageTitle = device.name
SettingsService.set('lastUsedDevice', serial)
return device
})
.catch(function() {
$timeout(function() {
$location.path('/')
})
})
}
...
其中:
$scope.control = ControlService.create(device, device.channel)
我們來分析ControlService.js文件
function sendOneWay(action, data) {
socket.emit(action, channel, data)
}
...
...
this.touchDown = function(seq, contact, x, y, pressure) {
sendOneWay('input.touchDown', {
seq: seq
, contact: contact
, x: x
, y: y
, pressure: pressure
})
}
...
ControlService.js中封裝了一系列操作設(shè)備的函數(shù):如: this.touchDown盐须。但這些操作設(shè)備函數(shù)都調(diào)用了sendOneWay方法玩荠。sendOneWay方法調(diào)用了socket.emit(),而這個socket來自socket-service.js文件贼邓。這個文件主要內(nèi)容如下所示
var io = require('socket.io')
var websocketUrl = AppState.config.websocketUrl || ''
var socket = io(websocketUrl, {
reconnection: false, transports: ['websocket']
})
...
這段代碼創(chuàng)建了websocket client(websocket 在之前的文章已經(jīng)描述過了阶冈,這里不再贅述)。ControlService.js文件使用這個socket向服務(wù)端發(fā)送消息塑径。
總結(jié)寫一下女坑,this.touchDown
函數(shù),通過socket.emit
向服務(wù)端發(fā)送'input.touchDown'消息统舀,并傳輸該設(shè)備的channel和按壓點的x,y坐標(biāo)匆骗。
需要注意的是,設(shè)備A詳情頁和設(shè)備B詳情頁,var websocketUrl = AppState.config.websocketUrl || ''
不同(端口不同)誉简。
服務(wù)端
準(zhǔn)備工作
請先仔細閱讀zeromq官方文檔碉就,zeromq其他參考資料。著重看push/pull,pub/sub部分闷串。
服務(wù)端流程
服務(wù)端是如何接收前端發(fā)送的數(shù)據(jù)瓮钥,并將數(shù)據(jù)傳輸至對應(yīng)手機呢?這里以單個設(shè)備舉例。
服務(wù)端處理信息流轉(zhuǎn)有三個文件,websocket/index.js
, triproxy/index.js
, processor/index.js
骏庸。最后接收數(shù)據(jù)和minitouch通信是來自device的sub.js,touch/index.js
年叮。
在執(zhí)行stf local
命令時具被,發(fā)現(xiàn)STF啟動了兩個triproxy的實例app001 和 dev001
INF/util:procutil 40934 [*] Forking "/Users/sheranjun/Code/stf/lib/cli.js triproxy app001 --bind-pub tcp://127.0.0.1:7111 --bind-dealer tcp://127.0.0.1:7112 --bind-pull tcp://127.0.0.1:7113"
INF/util:procutil 40934 [*] Forking "/Users/sheranjun/Code/stf/lib/cli.js triproxy dev001 --bind-pub tcp://127.0.0.1:7114 --bind-dealer tcp://127.0.0.1:7115 --bind-pull tcp://127.0.0.1:7116"
其中app001是處理從前端UI push的消息;dev001 是處理從手機push的消息只损。這兩個實例一起完成了前端UI和手機的雙向流通一姿。當(dāng)然本文只分析從前端UI發(fā)送消息到手機接收消息單方向的流程
websocket/index.js
index.js 做了兩件事,啟動websocket服務(wù)端跃惫,等待接收websocket 客戶端消息叮叹;啟動 app001 push,發(fā)送消息至 app001 pull
- websocket server 初始化
var socketio = require('socket.io')
var io = socketio.listen(server, {
serveClient: false
, transports: ['websocket']
})
....
server.listen(options.port)
log.info('Listening on port %d', options.port)
設(shè)備A詳情頁和設(shè)備B詳情頁啟動的是不同的websocket server.
- app001 push 初始化
var push = zmqutil.socket('push')
log.info("endopoints is " + options.endpoints)
log.info("endopoints is push " + options.endpoints.push)
Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
push.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to push endpoint', err)
lifecycle.fatal()
})
其中push.connect(record.url)
, record.url
是app001 pull 服務(wù)的URL。是唯一的爆存,所有的設(shè)備詳情頁使用的是同一個app001 pull 服務(wù)的URL蛉顽。
- 什么時候push呢?
io.on('connection', function(socket) {
socket.on('input.touchDown', function(channel, data){
log.info("touch down from websocket")
push.send([
channel
, wireutil.envelope(new wire.TouchDownMessage(
data.seq
, data.contact
, data.x
, data.y
, data.pressure
))
])
})
}
這段語句先较,將websocket和push聯(lián)系起來携冤,先來看看前端發(fā)送的input.touchDown
消息,傳輸?shù)臄?shù)據(jù)
function sendOneWay(action, data) {
socket.emit(action, channel, data)
}
...
...
this.touchDown = function(seq, contact, x, y, pressure) {
sendOneWay('input.touchDown', {
seq: seq
, contact: contact
, x: x
, y: y
, pressure: pressure
})
}
...
兩端代碼聯(lián)系起來闲勺,前端發(fā)送input.touchDown
消息曾棕,后端成功接收input.touchDown
消息之后,調(diào)用 app001 的push端菜循,將channel和封裝了data(封裝方式這里不重要翘地,不討論),發(fā)送至app 001 的pull端。
同時可以知道癌幕,盡管設(shè)備A和設(shè)備B詳情頁使用的不同的websocket衙耕,但它們使用的push端相同。
triproxy/index.js
triproxy中的index.js勺远,主要是新建pull端臭杰,dealer端,以及pub端谚中。代碼如下所示
function proxy(to) {
return function() {
to.send([].slice.call(arguments))
}
}
// App/device output
var pub = zmqutil.socket('pub')
pub.bindSync(options.endpoints.pub)
log.info('PUB socket bound on', options.endpoints.pub)
// Coordinator input/output
var dealer = zmqutil.socket('dealer')
dealer.bindSync(options.endpoints.dealer)
dealer.on('message', proxy(pub))
log.info('DEALER socket bound on', options.endpoints.dealer)
// App/device input
var pull = zmqutil.socket('pull')
pull.bindSync(options.endpoints.pull)
pull.on('message', proxy(dealer))
log.info('PULL socket bound on', options.endpoints.pull)
在啟動STF時渴杆,新建了兩個triproxy:app001,dev001。這里拿app001 triproxy來舉例宪塔,
-
pull.on('message', proxy(dealer))
,意味著pull端接受來自websocket/index.js push端的channel/data后磁奖,將channel/data通過 dealer 服務(wù)端發(fā)送出去。 -
dealer.on('message', proxy(pub))
,意思是 dealer服務(wù)端接收到channel/data后某筐,將channel/data通過pub 端發(fā)送出去比搭。
前面提到過,STF新建了兩個triproxy:app001 和dev 001南誊。所以身诺,websocket/index.js 中的channel/data通過 app001 push到了app001的pull端蜜托。app001pull端,將channel/data從app001 dealer 服務(wù)端發(fā)送出去霉赡,那channel/data又在哪里被接收了呢橄务?
processor/index.js
processor中的index.js,主要是為了app001 dealer client 和dev001 dealear client之間的消息轉(zhuǎn)發(fā)穴亏。代碼如下
// App side
var appDealer = zmqutil.socket('dealer')
Promise.map(options.endpoints.appDealer, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('App dealer connected to "%s"', record.url)
appDealer.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to app dealer endpoint', err)
lifecycle.fatal()
})
// Device side
var devDealer = zmqutil.socket('dealer')
appDealer.on('message', function(channel, data) {
devDealer.send([channel, data])
})
Promise.map(options.endpoints.devDealer, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Device dealer connected to "%s"', record.url)
devDealer.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to dev dealer endpoint', err)
lifecycle.fatal()
})
processor中的index.js 中新建了兩個dealer client:appDealer client 和 devDealer client蜂挪。分別連接triproxy中,app001 dealer 服務(wù)端和dev001 dealer 服務(wù)端嗓化。其中
appDealer.on('message', function(channel, data) {
devDealer.send([channel, data])
})
appDealer client 接收到來自 app001 dealer 服務(wù)端的channel/data之后棠涮,將channel/data傳遞給devDealer client,devDealer client發(fā)送至 dev001 dealer服務(wù)端刺覆。再聯(lián)系triproxy/index.js
一小節(jié)的流程严肪,dev001 的dealer服務(wù)端接收到channel/data后,將channel/data通過dev001的pub端發(fā)送出去谦屑。
至此服務(wù)端channel/data流轉(zhuǎn)前半截就將完了诬垂。接下來將dev001的pub端的channel/data流向哪里了?
lib/units/device 下sub.js,solo.js,以及touch/index.js
每一個手機都會創(chuàng)造一個device對象伦仍。device對象中包含很多功能结窘。和本篇文章相關(guān)的是兩個功能,一是dev001 sub端并訂閱固定channel充蓝。這個功能由sub.js和solo.js兩個文件完成隧枫;二是創(chuàng)造一個touch對象,STF用它來向手機發(fā)送指令谓苟,這個功能由touch/index.js文件完成官脓。如下圖所示:
至于device對象何時創(chuàng)造?如何被創(chuàng)建?這個流程和lib/cli.js涝焙,/lib/provider/index.js文件有關(guān)卑笨。關(guān)鍵詞是fork
以及.command('device <serial>')
,有興趣自己研究。
-
sub.js
為每個手機創(chuàng)建連接到dev001 pub端的sub端仑撞。代碼:
var sub = zmqutil.socket('sub') return Promise.map(options.endpoints.sub, function(endpoint) { return srv.resolve(endpoint).then(function(records) { return srv.attempt(records, function(record) { log.info('Receiving input from "%s"', record.url) sub.connect(record.url) return Promise.resolve(true) }) }) }) .then(function() { // Establish always-on channels [wireutil.global].forEach(function(channel) { log.info('Subscribing to permanent channel "%s"', channel) sub.subscribe(channel) }) }) .return(sub)
這里的
sub.connect(record.url)
,record.url
是dev001 pub端赤兴,這一點可以通過stf啟動日志驗證。STF日志中dev001 pub端口地址如下所示:
INF/util:procutil 47526 [*] Forking "/Users/****/Code/stf/lib/cli.js triproxy dev001 --bind-pub tcp://127.0.0.1:7114 --bind-dealer tcp://127.0.0.1:7115 --bind-pull tcp://127.0.0.1:7116"
而STF日志中sub.js打印出的日志:
INF/device:support:sub 48054 [63a5b447] Receiving input from "tcp://127.0.0.1:7114"
-
solo.js
solo.js隧哮,為sub.js中創(chuàng)建的sub端桶良,訂閱該手機的固定頻道。代碼如下
function makeChannelId() { var hash = crypto.createHash('sha1') hash.update(options.serial) return hash.digest('base64') } var channel = makeChannelId() log.info('Subscribing to permanent channel "%s"', channel) sub.subscribe(channel)
solo.js通過手機的serial創(chuàng)建channel沮翔≡煞可能會有人問,怎么確定dev001 pub端發(fā)送的channel(本質(zhì)上來自前端手機A詳情頁傳輸?shù)腸hannel),和該手機在solo.js中訂閱的channel是同一個呢疲牵?這里簡單解釋一下承二。前端傳遞過來的channel,也來自于后端纲爸。前端獲取channel的代碼在device-service.js文件中亥鸠,代碼如下所示:
deviceService.load = function(serial) { return $http.get('/api/v1/devices/' + serial) .then(function(response) { return response.data.device }) }
請求的api,最終執(zhí)行的后端函數(shù)如下所示:
function getDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value var fields = req.swagger.params.fields.value dbapi.loadDevice(serial) .then(function(device) { ... }
其中
dbapi.loadDevice
是操作數(shù)據(jù)庫函數(shù)缩焦,而對device對象的一系列數(shù)據(jù)庫操作中,只有setDeviceReady
函數(shù)是更新了device的channel责静,所以dbapi.loadDevice
中channel數(shù)據(jù)來自于setDeviceReady
函數(shù)袁滥。dbapi.setDeviceReady = function(serial, channel) { return db.run(r.table('devices').get(serial).update({ channel: channel , ready: true , owner: null , reverseForwards: [] })) }
而
dbapi.setDeviceReady
函數(shù)又在哪里被調(diào)用呢?是的灾螃,在processor/index.js题翻。devDealer.on('message', wirerouter() .on(wire.DeviceReadyMessage, function(channel, message, data) { dbapi.setDeviceReady(message.serial, message.channel) .then(function() { devDealer.send([ message.channel , wireutil.envelope(new wire.ProbeMessage()) ]) appDealer.send([channel, data]) }) })
而processor/index.js接收的channel/message/data,來自solo.js腰鬼。至于solo.js的push嵌赠,如何將channel/message/data傳輸至processor/index.js的devDealer client中,按照上一小節(jié)的triproxy.js的流程圖熄赡,對照STF日志姜挺,自己分析。這里不再贅述彼硫。
return { channel: channel , poke: function() { push.send([ wireutil.global , wireutil.envelope(new wire.DeviceReadyMessage( options.serial , channel )) ]) } }
所以炊豪,前端的channel,也是來自于solo.js,而且channel和serial有一一對應(yīng)的關(guān)系。這就解決了來自前端手機A詳情頁的數(shù)據(jù)一定會發(fā)送到后端手機A的對應(yīng)的channel上拧篮。
-
touch/index.js
touch/index.js
中做了兩件事:一是啟動手機的minitouch服務(wù)词渤,而是訂閱dev001 pub端的數(shù)據(jù),并將固定頻道channel數(shù)據(jù)傳輸至手機串绩。每一個手機都會有一個touch/index.js-
啟動手機minitouch服務(wù)
- 啟動手機上的minitouch服務(wù)
TouchConsumer.prototype._startService = function() { log.info('Launching screen service') return minitouch.run() .timeout(10000) }
- 將手機端啟動的minitouch tcp 服務(wù)端端口映射到pc端缺虐。連接到該pc端口,返回socket礁凡。該功能由
adb.openLocal
函數(shù)完成高氮,代碼如下所示
TouchConsumer.prototype._connectService = function() { function tryConnect(times, delay) { return adb.openLocal(options.serial, 'localabstract:minitouch') .timeout(10000) .then(function(out) { return out }) .catch(function(err) { if (/closed/.test(err.message) && times > 1) { return Promise.delay(delay) .then(function() { return tryConnect(times - 1, delay * 2) }) } return Promise.reject(err) }) } log.info('Connecting to minitouch service') // SH-03G can be very slow to start sometimes. Make sure we try long // enough. return tryConnect(7, 100) }
有興趣的,可以研究一下
adb.openLocal
函數(shù)顷牌,這里不再詳細討論該函數(shù)流程-
訂閱dev001 pub端固定channel
touch/index.js
通過引用sub.js和solo.js,使用訂閱固定頻道的dev001 sub纫溃,等待來自dev001 pub端數(shù)據(jù),代碼如下所示:router .on(wire.TouchDownMessage, function(channel, message) { log.info("touch down from toucb") queue.push(message.seq, function() { touchConsumer.touchDown(message) }) })
而router來自router.js中韧掩,router.js引用了sub.js
module.exports = syrup.serial() .dependency(require('./sub')) .define(function(options, sub, channels) { var log = logger.createLogger('device:support:router') var router = wirerouter() sub.on('message', router.handler()) return router })
其中dev001 push端在channelA上發(fā)送數(shù)據(jù)后紊浩,訂閱了channelA的dev001 sub端接收該數(shù)據(jù)。并調(diào)用
touchConsumer.touchDown(message)
。而touchDown函數(shù)的相關(guān)代碼如下所示:TouchConsumer.prototype.touchDown = function(point) { log.info("touch down from touch index") this._queueWrite(function() { return this._write(util.format( 'd %s %s %s %s\n' , point.contact , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX) , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY) , Math.floor((point.pressure || 0.5) * this.banner.maxPressure) )) }) } TouchConsumer.prototype._write = function(chunk) { this.socket.stream.write(chunk) }
touchDown
函數(shù)中的this._write
調(diào)用的this.socket
來自this._connectService()
返回值坊谁。this.socket
代表著連接手機minitouch tcp服務(wù)端的tcp 客戶端费彼。touchDown
函數(shù)接收touchDown消息后,向手機發(fā)送以'd'開頭的字符串命令口芍。
到此箍铲,服務(wù)端消息的流程已經(jīng)全部解析完成。再看看最開始畫的流程圖鬓椭,是不是清晰很多呢颠猴?
-
手機
手機上主要是啟動了minitouch的tcp服務(wù),接收STF服務(wù)端操作手機指令小染。并根據(jù)指令翘瓮,操作手機它的代碼如下所示
-
start_server函數(shù),啟動tcp服務(wù)
static int start_server(char* sockname) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { perror("creating socket"); return fd; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(&addr.sun_path[1], sockname, strlen(sockname)); if (bind(fd, (struct sockaddr*) &addr, sizeof(sa_family_t) + strlen(sockname) + 1) < 0) { perror("binding socket"); close(fd); return -1; } listen(fd, 1); return fd; }
fd
既是創(chuàng)造TCP 服務(wù)端 -
等到STF 服務(wù)端發(fā)送的指令裤翩,根據(jù)指令操作設(shè)備资盅,這里以touchDown指令,舉例說明
int client_fd = accept(server_fd, (struct sockaddr *) &client_addr, &client_addr_length); ..... while (io_length < sizeof(io_buffer) && read(client_fd, &io_buffer[io_length], 1) == 1) { if (io_buffer[io_length++] == '\n') { break; } } ... switch (io_buffer[0]) { case 'c': // COMMIT commit(&state); break; case 'r': // RESET touch_panic_reset_all(&state); break; case 'd': // TOUCH DOWN contact = strtol(cursor, &cursor, 10); x = strtol(cursor, &cursor, 10); y = strtol(cursor, &cursor, 10); pressure = strtol(cursor, &cursor, 10); touch_down(&state, contact, x, y, pressure); break; ..... }
其中
client_fd
接收來自STF服務(wù)端的指令踊赠,io_buffer
存儲STF服務(wù)端消息呵扛,并根據(jù)第一字符判斷操作類型。若第一個字符為d筐带,調(diào)用touch_down
函數(shù)今穿。
后記
touch動作源碼分析,整理出來算是一個大的工程了伦籍。涉及的核心知識點是zeromq荣赶,這里只分析了前端操作是如何同步到設(shè)備,逆向流程并沒有涉及鸽斟,有興趣的可以自己了解拔创。
由于項目需要改造STF,閱讀了STF的源碼富蓄,但項目改造STF重點在STF的前端和STF認(rèn)證剩燥。這兩塊沒有涉及多少STF整體架構(gòu)設(shè)計,更多的是angular 1.x框架的知識立倍。對這兩塊有興趣的灭红,歡迎討論。