當我使用STF時,最震驚的是,它怎么做到設備和前端頁面設備模塊操作上的同步。之前看STF 框架之 minicap 工具, 知道作者開發自己的Android設備上快速截圖的工具,但是STF怎么將截圖以這么快的速度傳輸到前端頁面的呢?很好奇,所以有了這篇文章。
基礎準備
STF依賴技術
- STF的服務端基于node js,使用express框架
- STF的前端基于angular 1.x框架
閱讀STF源碼,除熟悉javascript 基礎語法,express框架需要知道一些基本概念。若想要改造STF前端,angular 1.x框架必須好好學一學。
Websocket協議
STF服務端和STF前端通信協議是Websocket,不是HTTP。Websocket是瀏覽器端新的傳輸協議,類似于socket。因為這個協議,STF能快速將截圖從服務端同步給前端。我們先了解這個協議。
-
STF為什么選擇Websocket協議
- 我們假設瀏覽器是A端,服務端是B端,手機是C端。STF需要保證C端的操作,能在A端立即反應。同時用戶在A端的點擊之類的事件也能立刻在C端同步。它不再是像HTTP協議一樣,單方向通信,而是雙向通信。正是因為這樣,STF使用的是Websocket協議,
- Websocket協議快。除了第一次建立握手鏈接時,使用的是http協議。接下來傳遞數據,使用的是socket協議。
-
基于Websocket協議一段小應用
了解Websocket協議是理解STF實時顯示設備截圖的基礎。-
服務端 server.js
var websocketServer = require('socket.io'); //socket.io實現了websocket協議,引用socket.io模塊,新建Websocket服務器 var io = new websocketServer(7001); //Websocket服務器監聽7100端口 io.on('connection', function (ws) { ws.emit('news', {hello: "world"}); //連接建立,服務端發送消息至前端,消息的標識碼是'news',客戶端通過這個標志碼可以接收{hello: "world"}數據 ws.on('fromClient', function (data) { console.log("this is fromClient" + data ) //服務端接收客戶端標志碼‘fromClient’的數據。 }) });
-
前端 home.js
var socket = io.connect('http://localhost:7001'); //服務端啟websocket服務在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),客戶端發送該event消息時,服務端立刻調用function。socket.on功能類似
- ws.emit(event, data)/ws.send(event, data), 服務端向客戶端發送data。socket.emit/socket.send功能類似
一個完整使用Websocket協議通信的例子如上所示。接下來分析STF如何實現實時顯示設備截圖功能。
實時顯示設備截圖功能源碼分析
STF實時顯示設備截圖流程
將這個過程分為 從設備實時傳輸圖片二進制文件至前端,以及前端渲染圖片兩個部分。
實時傳輸設備圖片二進制文件源碼分析
STF實時傳輸設備圖片二進制文件是來自如下文件:
stream.js做了兩件事:
- 從設備 tcp server 中接收圖片二進制文件
- 將圖片二進制文件發送至前端
不關心STF強大的截圖工具minicap,只需要明白圖片二進制文件如何從設備傳輸至前端。
1.簡單的實時傳輸圖片二進制文件到前端頁面的demo
STF官方文檔minicap的使用demo,這個demo實現了這樣一個功能:
安裝minicap工具在手機上,執行命令adb forward tcp:1717 localabstract:minicap
,此時將設備的TCP服務器端口映射到本機的1717端口。nodejs啟動代碼中app.js,發現手機上的截圖不停顯示在localhost:9002頁面上。這個demo是STF中傳輸設備圖片二進制文件到前端的基本雛形。分析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部分
-
創建Websocket服務器,用于和前端通信。
var WebSocketServer = require('ws').Server var server = http.createServer(app) var wss = new WebSocketServer({ server: server })
-
websocket連接建立成功。
wss.on('connection', function(ws){ .... })
-
關閉websocket
ws.on('close', function() { ... })
-
-
和設備建立TCP通信部分
-
創建tcp client,net模塊是用來創建TCP客戶端。這段代碼創建一個TCP客戶端,監聽端口1717
net = require('net') var stream = net.connect({ port: 1717 })
-
接收tcp server 發送圖片
stream.on('readable', tryRead)
當接收
readable
事件后,調用tryRead函數。tryRead除了處理圖片二進制文件的邏輯,最重要的是調用了websocket.send,也就是說從設備獲得圖片二進制文件之后,使用Websocket協議傳輸至前端。function tryRead() { .... //...處理圖片 ws.send(frameBody, { binary: true }) }
-
關閉tcp client
stream.end()
-
demo的程序執行流程
2.STF中實時傳輸設備截圖代碼分析
STF中stream.js 實現實時傳輸設備圖片二進制文件代碼,基本原理和上面的demo是一樣的。只不過因為STF管理多臺設備,代碼會有點差別。
-
三個對象。
-
FrameProducer
FrameProducer創建tcp client,解析來自tcp server的數據,獲得二進制文件(圖片)
-
ws
創建websocket服務器,和前端通信
-
broadcastSet
通過broadcastSet的wsFrameNotifier函數,使用ws,發送二進制文件(圖片)。
-
-
啟動實時截圖服務
[圖片上傳失敗...(image-242b68-1521094554165)]- 前端使用websocket傳遞message,當message為on時,調用broadcastSet.insert()函數。
- FrameProducer.start() 函數在狀態隊列中插入start狀態。
- FrameProducer._ensureState() 開始實時同步設備的圖片二進制文件到前端
-
實時同步設備的圖片二進制文件到前端
實時將設備的圖片二進制文件同步到前端,邏輯放在FrameProducer._ensureState函數中,代碼如下所示:
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時的邏輯,這段代碼調用順序如下所示
其中主要函數:
- FrameProducer._connectService: 使用adb命令將設備的minicap工具啟動的tcp server 端口映射到pc的端口A。創建tcp client,tcp client連接端口A,返回該tcp client
- FrameProducer._readFrames: 等待minicap發出`readable`事件。接收該事件,調用FrameProducer.emit等函數。
- FrameProducer.nextFrame: 解析并返回設備傳輸二進制文件(圖片),代碼邏輯類似于上面demo中tryRead()函數。
- Websocket.send: 發送FrameProducer.nextFrame函數產生的二進制文件(圖片)至前端
前端渲染圖片
前端接收到二進制文件,如何渲染圖片呢?這部分邏輯主要在${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: 接收來自服務端的圖片二進制文件,為它創造blob對象
- URL.createObjectURL: 為blob對象創建URL,可以像普通URL使用它
- 將URL賦值給img.src,圖片可以加載出來
單獨拎出來這段代碼。這種更新前端圖片的流程給我提供了新思路。
后記
STF實時顯示設備截圖功能涉及的知識點很多:Android,tcp通信,瀏覽器Websocket協議,blob對象等。只覺得寫這個工具的作者牛X。