當(dāng)我使用STF時(shí),最震驚的是,它怎么做到設(shè)備和前端頁面設(shè)備模塊操作上的同步掏导。之前看STF 框架之 minicap 工具, 知道作者開發(fā)自己的Android設(shè)備上快速截圖的工具,但是STF怎么將截圖以這么快的速度傳輸?shù)角岸隧撁娴哪乩炜猓亢芎闷媸鸦郏杂辛诉@篇文章。
基礎(chǔ)準(zhǔn)備
STF依賴技術(shù)
- STF的服務(wù)端基于node js渺蒿,使用express框架
- STF的前端基于angular 1.x框架
閱讀STF源碼痢士,除熟悉javascript 基礎(chǔ)語法,express框架需要知道一些基本概念茂装。若想要改造STF前端怠蹂,angular 1.x框架必須好好學(xué)一學(xué)。
Websocket協(xié)議
STF服務(wù)端和STF前端通信協(xié)議是Websocket少态,不是HTTP城侧。Websocket是瀏覽器端新的傳輸協(xié)議,類似于socket彼妻。因?yàn)檫@個(gè)協(xié)議嫌佑,STF能快速將截圖從服務(wù)端同步給前端。我們先了解這個(gè)協(xié)議侨歉。
-
STF為什么選擇Websocket協(xié)議
- 我們假設(shè)瀏覽器是A端屋摇,服務(wù)端是B端,手機(jī)是C端幽邓。STF需要保證C端的操作炮温,能在A端立即反應(yīng)。同時(shí)用戶在A端的點(diǎn)擊之類的事件也能立刻在C端同步牵舵。它不再是像HTTP協(xié)議一樣茅特,單方向通信,而是雙向通信棋枕。正是因?yàn)檫@樣白修,STF使用的是Websocket協(xié)議,
- Websocket協(xié)議快重斑。除了第一次建立握手鏈接時(shí)兵睛,使用的是http協(xié)議。接下來傳遞數(shù)據(jù)窥浪,使用的是socket協(xié)議祖很。
-
基于Websocket協(xié)議一段小應(yīng)用
了解Websocket協(xié)議是理解STF實(shí)時(shí)顯示設(shè)備截圖的基礎(chǔ)。-
服務(wù)端 server.js
var websocketServer = require('socket.io'); //socket.io實(shí)現(xiàn)了websocket協(xié)議漾脂,引用socket.io模塊假颇,新建Websocket服務(wù)器 var io = new websocketServer(7001); //Websocket服務(wù)器監(jiān)聽7100端口 io.on('connection', function (ws) { ws.emit('news', {hello: "world"}); //連接建立,服務(wù)端發(fā)送消息至前端,消息的標(biāo)識碼是'news',客戶端通過這個(gè)標(biāo)志碼可以接收{(diào)hello: "world"}數(shù)據(jù) ws.on('fromClient', function (data) { console.log("this is fromClient" + data ) //服務(wù)端接收客戶端標(biāo)志碼‘fromClient’的數(shù)據(jù)骨稿。 }) });
-
前端 home.js
var socket = io.connect('http://localhost:7001'); //服務(wù)端啟websocket服務(wù)在7100端口笨鸡,所以客戶端連接7100端口 socket.on('news', function (data) { console.log(data.hello) }); socket.emit('fromClient',{"message": "everything is ok"});
-
- 上述demo中姜钳,使用的socket.io模塊,這也是STF使用模塊形耗,說明如下:
- io.on('connection',function) , 與客戶端Websocket連接建立成功
- ws.on(event, function)哥桥,客戶端發(fā)送該event消息時(shí),服務(wù)端立刻調(diào)用function激涤。socket.on功能類似
- ws.emit(event, data)/ws.send(event, data), 服務(wù)端向客戶端發(fā)送data拟糕。socket.emit/socket.send功能類似
一個(gè)完整使用Websocket協(xié)議通信的例子如上所示。接下來分析STF如何實(shí)現(xiàn)實(shí)時(shí)顯示設(shè)備截圖功能倦踢。
實(shí)時(shí)顯示設(shè)備截圖功能源碼分析
STF實(shí)時(shí)顯示設(shè)備截圖流程
將這個(gè)過程分為 從設(shè)備實(shí)時(shí)傳輸圖片二進(jìn)制文件至前端送滞,以及前端渲染圖片兩個(gè)部分。
實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件源碼分析
STF實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件是來自如下文件:
stream.js做了兩件事:
- 從設(shè)備 tcp server 中接收圖片二進(jìn)制文件
- 將圖片二進(jìn)制文件發(fā)送至前端
不關(guān)心STF強(qiáng)大的截圖工具minicap辱挥,只需要明白圖片二進(jìn)制文件如何從設(shè)備傳輸至前端犁嗅。
1.簡單的實(shí)時(shí)傳輸圖片二進(jìn)制文件到前端頁面的demo
STF官方文檔minicap的使用demo,這個(gè)demo實(shí)現(xiàn)了這樣一個(gè)功能:
安裝minicap工具在手機(jī)上般贼,執(zhí)行命令adb forward tcp:1717 localabstract:minicap
愧哟,此時(shí)將設(shè)備的TCP服務(wù)器端口映射到本機(jī)的1717端口。nodejs啟動(dòng)代碼中app.js哼蛆,發(fā)現(xiàn)手機(jī)上的截圖不停顯示在localhost:9002頁面上蕊梧。這個(gè)demo是STF中傳輸設(shè)備圖片二進(jìn)制文件到前端的基本雛形。分析demo中app.js
var WebSocketServer = require('ws').Server
, http = require('http')
, express = require('express')
, path = require('path')
, net = require('net')
, app = express()
var PORT = process.env.PORT || 9002
app.use(express.static(path.join(__dirname, '/public')))
var server = http.createServer(app)
var wss = new WebSocketServer({ server: server })
wss.on('connection', function(ws) {
console.info('Got a client')
var stream = net.connect({
port: 1717
})
stream.on('error', function() {
console.error('Be sure to run `adb forward tcp:1717 localabstract:minicap`')
process.exit(1)
})
function tryRead() {
....
....
ws.send(frameBody, {
binary: true
})
}
stream.on('readable', tryRead)
ws.on('close', function() {
console.info('Lost a client')
stream.end()
})
server.listen(PORT)
上述代碼主要分為以下幾塊
-
和前端通信的websocket部分
-
創(chuàng)建Websocket服務(wù)器腮介,用于和前端通信肥矢。
var WebSocketServer = require('ws').Server var server = http.createServer(app) var wss = new WebSocketServer({ server: server })
-
websocket連接建立成功。
wss.on('connection', function(ws){ .... })
-
關(guān)閉websocket
ws.on('close', function() { ... })
-
-
和設(shè)備建立TCP通信部分
-
創(chuàng)建tcp client,net模塊是用來創(chuàng)建TCP客戶端叠洗。這段代碼創(chuàng)建一個(gè)TCP客戶端甘改,監(jiān)聽端口1717
net = require('net') var stream = net.connect({ port: 1717 })
-
接收tcp server 發(fā)送圖片
stream.on('readable', tryRead)
當(dāng)接收
readable
事件后,調(diào)用tryRead函數(shù)灭抑。tryRead除了處理圖片二進(jìn)制文件的邏輯十艾,最重要的是調(diào)用了websocket.send,也就是說從設(shè)備獲得圖片二進(jìn)制文件之后腾节,使用Websocket協(xié)議傳輸至前端忘嫉。function tryRead() { .... //...處理圖片 ws.send(frameBody, { binary: true }) }
-
關(guān)閉tcp client
stream.end()
-
demo的程序執(zhí)行流程
2.STF中實(shí)時(shí)傳輸設(shè)備截圖代碼分析
STF中stream.js 實(shí)現(xiàn)實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件代碼,基本原理和上面的demo是一樣的案腺。只不過因?yàn)镾TF管理多臺設(shè)備庆冕,代碼會(huì)有點(diǎn)差別。
-
三個(gè)對象劈榨。
-
FrameProducer
FrameProducer創(chuàng)建tcp client,解析來自tcp server的數(shù)據(jù)访递,獲得二進(jìn)制文件(圖片)
-
ws
創(chuàng)建websocket服務(wù)器,和前端通信
-
broadcastSet
通過broadcastSet的wsFrameNotifier函數(shù)同辣,使用ws拷姿,發(fā)送二進(jìn)制文件(圖片)惭载。
-
-
啟動(dòng)實(shí)時(shí)截圖服務(wù)
[圖片上傳失敗...(image-242b68-1521094554165)]- 前端使用websocket傳遞message,當(dāng)message為on時(shí)跌前,調(diào)用broadcastSet.insert()函數(shù)棕兼。
- FrameProducer.start() 函數(shù)在狀態(tài)隊(duì)列中插入start狀態(tài)陡舅。
- FrameProducer._ensureState() 開始實(shí)時(shí)同步設(shè)備的圖片二進(jìn)制文件到前端
-
實(shí)時(shí)同步設(shè)備的圖片二進(jìn)制文件到前端
實(shí)時(shí)將設(shè)備的圖片二進(jìn)制文件同步到前端抵乓,邏輯放在FrameProducer._ensureState函數(shù)中,代碼如下所示:
FrameProducer.prototype._ensureState = function() { ... ... switch (this.runningState) { case FrameProducer.STATE_STARTING: case FrameProducer.STATE_STOPPING: // Just wait. break case FrameProducer.STATE_STOPPED: if (this.desiredState.next() === FrameProducer.STATE_STARTED) { this.runningState = FrameProducer.STATE_STARTING this._startService().bind(this) .then(function(out) { this.output = new RiskyStream(out) .on('unexpectedEnd', this._outputEnded.bind(this)) return this._readOutput(this.output.stream) }) .then(function() { return this._waitForPid() }) .then(function() { return this._connectService() }) .then(function(socket) { this.parser = new FrameParser() this.socket = new RiskyStream(socket) .on('unexpectedEnd', this._socketEnded.bind(this)) return this._readBanner(this.socket.stream) }) .then(function(banner) { this.banner = banner return this._readFrames(this.socket.stream) }) .then(function() { this.runningState = FrameProducer.STATE_STARTED this.emit('start') }) .catch(Promise.CancellationError, function() { return this._stop() }) .catch(function(err) { return this._stop().finally(function() { this.failCounter.inc() this.emit('error', err) }) }) .finally(function() { this._ensureState() }) } else { setImmediate(this._ensureState.bind(this)) } break .... .... }
上面這段代碼主要看FrameProducer.STATE_STOPPED時(shí)的邏輯靶衍,這段代碼調(diào)用順序如下所示
其中主要函數(shù):
- FrameProducer._connectService: 使用adb命令將設(shè)備的minicap工具啟動(dòng)的tcp server 端口映射到pc的端口A灾炭。創(chuàng)建tcp client,tcp client連接端口A颅眶,返回該tcp client
- FrameProducer._readFrames: 等待minicap發(fā)出`readable`事件蜈出。接收該事件,調(diào)用FrameProducer.emit等函數(shù)涛酗。
- FrameProducer.nextFrame: 解析并返回設(shè)備傳輸二進(jìn)制文件(圖片)铡原,代碼邏輯類似于上面demo中tryRead()函數(shù)。
- Websocket.send: 發(fā)送FrameProducer.nextFrame函數(shù)產(chǎn)生的二進(jìn)制文件(圖片)至前端
前端渲染圖片
前端接收到二進(jìn)制文件商叹,如何渲染圖片呢燕刻?這部分邏輯主要在${STFhome}/res/app/components/stf/screen/screen-directive.js
文件中
var ws = new WebSocket(device.display.url)
ws.binaryType = 'blob'
ws.onmessage = (function() {
return function messageListener(message) {
if (message.data instanceof Blob) {
var blob = new Blob([message.data], {
type: 'image/jpeg'
})
...
...
var img = imagePool.next()
var url = URL.createObjectURL(blob)
img.src = url
}
}
})()
- new Blob: 接收來自服務(wù)端的圖片二進(jìn)制文件,為它創(chuàng)造blob對象
- URL.createObjectURL: 為blob對象創(chuàng)建URL,可以像普通URL使用它
- 將URL賦值給img.src剖笙,圖片可以加載出來
單獨(dú)拎出來這段代碼卵洗。這種更新前端圖片的流程給我提供了新思路。
后記
STF實(shí)時(shí)顯示設(shè)備截圖功能涉及的知識點(diǎn)很多:Android弥咪,tcp通信过蹂,瀏覽器Websocket協(xié)議,blob對象等聚至。只覺得寫這個(gè)工具的作者牛X酷勺。