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閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容