STF系列之一--STF 实时显示设备截图功能源码分析

当我使用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实时显示设备截图流程

image流程.png

将这个过程分为 从设备实时传输图片二进制文件至前端,以及前端渲染图片两个部分。

实时传输设备图片二进制文件源码分析

STF实时传输设备图片二进制文件是来自如下文件:

screenshot.png

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的程序执行流程

simpleDemo.png

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时的逻辑,这段代码调用顺序如下所示

startScreenshot.png
其中主要函数:
- 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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容