当我使用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。