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

STF系列之一--STF 实时显示设备截图功能源码分析_第1张图片
image流程.png

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

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

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

STF系列之一--STF 实时显示设备截图功能源码分析_第2张图片
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的程序执行流程

STF系列之一--STF 实时显示设备截图功能源码分析_第3张图片
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时的逻辑,这段代码调用顺序如下所示

STF系列之一--STF 实时显示设备截图功能源码分析_第4张图片
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。

你可能感兴趣的:(STF系列之一--STF 实时显示设备截图功能源码分析)