当我使用 STF 时,最震惊的是,它怎么做到设备和前端页面设备模块操作上的同步。之前看STF 框架之 minicap 工具, 知道作者开发自己的 Android 设备上快速截图的工具,但是 STF 怎么将截图以这么快的速度传输到前端页面的呢?很好奇,所以有了这篇文章。
阅读 STF 源码,除熟悉 javascript 基础语法,express 框架需要知道一些基本概念。若想要改造 STF 前端,angular 1.x 框架必须好好学一学。
STF 服务端和 STF 前端通信协议是 Websocket,不是 HTTP。Websocket 是浏览器端新的传输协议,类似于 socket。因为这个协议,STF 能快速将截图从服务端同步给前端。我们先了解这个协议。
STF 为什么选择 Websocket 协议
基于 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 使用模块,说明如下:
一个完整使用 Websocket 协议通信的例子如上所示。接下来分析 STF 如何实现实时显示设备截图功能。
将这个过程分为 从设备实时传输图片二进制文件至前端,以及前端渲染图片两个部分。
STF 实时传输设备图片二进制文件是来自如下文件:
stream.js 做了两件事:
不关心 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,发送二进制文件(图片)。
启动实时截图服务
实时同步设备的图片二进制文件到前端
实时将设备的图片二进制文件同步到前端,逻辑放在 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 时的逻辑,这段代码调用顺序如下所示
其中主要函数:
readable
事件。接收该事件,调用 FrameProducer.emit 等函数。前端接收到二进制文件,如何渲染图片呢?这部分逻辑主要在${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
}
}
})()
单独拎出来这段代码。这种更新前端图片的流程给我提供了新思路。
STF 实时显示设备截图功能涉及的知识点很多:Android,tcp 通信,浏览器 Websocket 协议,blob 对象等。只觉得写这个工具的作者牛 X。