1. 程式人生 > >UiAutomator系列——Appium Server 原始碼分析之啟動執行Express http伺服器(010)

UiAutomator系列——Appium Server 原始碼分析之啟動執行Express http伺服器(010)

通過上一個系列Appium Android Bootstrap原始碼分析我們瞭解到了appium在安卓目標機器上是如何通過bootstrap這個服務來接收appium從pc端傳送過來的命令,並最終使用uiautomator框架進行處理的。大家還沒有這方面的背景知識的話建議先去看一下,以下列出來方便大家參考:

那麼我們知道了目標機器端的處理後,我們理所當然需要搞清楚bootstrap客戶端,也就是Appium Server是如何工作的,這個就是這個系列文章的初衷。

Appium Server其實擁有兩個主要的功能:

  • 它是個http伺服器,它專門接收從客戶端通過基於http的REST協議傳送過來的命令
  • 他是bootstrap客戶端:它接收到客戶端的命令後,需要想辦法把這些命令傳送給目標安卓機器的bootstrap來驅動uiatuomator來做事情
我們今天描述的就是第一點。大家先看下我以前畫的一個appium架構圖好有個基本概念:Appium Server大概是在哪個位置進行工作的
同時我們也先看下Appium Server的原始碼佈局,後有一個基本的程式碼結構概念:


開始之前先宣告一下,因為appium server是基於當今熱本的nodejs編寫的,而我本人並不是寫javascript出身的,只是在寫這篇文章的時候花了幾個小時去了解了下javascript的語法,但是我相信語言是相同的,去看懂這些程式碼還是沒有太大問題的。但,萬一當中真有誤導大家的地方,還敬請大家指出來,以免禍害讀者...

1.執行引數準備

Appium 伺服器啟動的入口就在bin下面的appium.js這個檔案裡面.在一開始的時候這個javascript就會先去匯入必須的模組然後對啟動引數進行初始化:
[javascript] view plaincopy
  1. var net = require('net')  
  2.   , repl = require('repl')  
  3.   , logFactory = require('../lib/server/logger.js')  
  4.   , parser = require('../lib/server/parser.js');  
  5. require('colors');  
  6. var
     args = parser().parseArgs();  
引數的解析時在‘../lib/server/parser.js'裡面的,檔案一開始就指定使用了nodejs提供的專門對引數進行解析的argparse模組的 ArgumentPaser類,具體這個類時怎麼用的大家自己google就好了: [javascript] view plaincopy
  1. var ap = require('argparse').ArgumentParser  
然後該javascript指令碼就會例項化這個ArgumentParser來啟動對引數的解析: [javascript] view plaincopy
  1. // Setup all the command line argument parsing
  2. module.exports = function () {  
  3.   var parser = new ap({  
  4.     version: pkgObj.version,  
  5.     addHelp: true,  
  6.     description: 'A webdriver-compatible server for use with native and hybrid iOS and Android applications.'
  7.   });  
  8.   _.each(args, function (arg) {  
  9.     parser.addArgument(arg[0], arg[1]);  
  10.   });  
  11.   parser.rawArgs = args;  
  12.   return parser;  
  13. };  
ArgumentPaser會對已經定義好的每一個args進行分析,如果有提供對應引數設定的就進行設定,沒有的話就會提供預設值,這裡我們提幾個比較重要的引數作為例子: [javascript] view plaincopy
  1. var args = [  
  2.     ...  
  3.   [['-a''--address'], {  
  4.     defaultValue: '0.0.0.0'
  5.   , required: false
  6.   , example: "0.0.0.0"
  7.   , help: 'IP Address to listen on'
  8.   }],  
  9.     ...  
  10.   [['-p''--port'], {  
  11.     defaultValue: 4723  
  12.   , required: false
  13.   , type: 'int'
  14.   , example: "4723"
  15.   , help: 'port to listen on'
  16.   }],  
  17.     ...  
  18.   [['-bp''--bootstrap-port'], {  
  19.     defaultValue: 4724  
  20.   , dest: 'bootstrapPort'
  21.   , required: false
  22.   , type: 'int'
  23.   , example: "4724"
  24.   , help: '(Android-only) port to use on device to talk to Appium'
  25.   }],  
  26.     ...  
  27. ];  
  • address:指定http伺服器監聽的ip地址,沒有指定的話預設就監聽本機
  • port:指定http伺服器監聽的埠,沒有指定的話預設監聽4723埠
  • bootstrap-port:指定要連線上安卓目標機器端的socket監聽埠,預設4724

2. 建立Express HTTP伺服器

Appium支援兩種方式啟動,一種是在提供--shell的情況下提供互動式編輯器的啟動方式,這個就好比你直接在命令列輸入node,然後彈出命令列互動輸入介面讓你一行行的輸入除錯執行;另外一種就是我們正常的啟動方式而不需要使用者的互動,這個也就是我們今天關注的重點:

  1. if (process.argv[2] && process.argv[2].trim() === "--shell") {  
  2.   startRepl();  
  3. else {  
  4.   appium.run(args, function () { /* console.log('Rock and roll.'.grey); */ });  
  5. }  
這裡appium這個變數是從其他地方匯入了,我們回到指令碼較前位置: [javascript] view plaincopy
  1. var args = parser().parseArgs();  
  2. logFactory.init(args);  
  3. var appium  = require('../lib/server/main.js');  
可以看到,這個指令碼首先會呼叫parser的模組去分析使用者輸入的引數然後儲存起來(至於怎麼解析的就不去看了,無非是讀取每個引數然後儲存起來而已,大家看下本人前面分析的其他原始碼是怎麼獲得啟動引數的就清楚了),然後往下我們就可以看到appium這個變數是從'../lib/server/main.js'這個指令碼導進來的,所以我們就需要去到這個指令碼,瀏覽到指令碼最下面的一行: [javascript] view plaincopy
  1. module.exports.run = main;  
它是把main這個方法以run的名字匯出給其他模組使用了,所以回到了最上面的:
  1. appium.run(args, function () { /* console.log('Rock and roll.'.grey); */ });  

就相當於呼叫了'main.js'的: [javascript] view plaincopy
  1. main(args, function () { /* console.log('Rock and roll.'.grey); */ });  
我們往下看main這個方法,首先它會做一些基本的引數檢查,然後初始化了一個express例項(Express是目前最流行的基於Node.js的Web開發框架,提供各種模組,可以快速地搭建一個具有完整功能的網站,強烈建議不清楚其使用的童鞋先去看下牛人阮一峰的《Express框架》),然後如平常一樣建立一個http伺服器: [javascript] view plaincopy
  1. var main = function (args, readyCb, doneCb) {  
  2.     ...  
  3.   var rest = express()  
  4.     , server = http.createServer(rest);  
  5.     ...  
  6. }  

只是這個http伺服器跟普通的伺服器唯一的差別是createServer方法的引數,從一個回撥函式變成了一個Epress物件的例項。它使用了express框架對http模組進行再包裝的,這樣它就可以很方便的使用express的功能和方法來快速建立http服務,比如:

  • 通過 express的get,post等快速設定路由。用於指定不同的訪問路徑所對應的回撥函式,這叫做“路由”(routing),這個也是為什麼說express是符合RestFul風格的框架的原因之一了
  • 使用express的use方法來設定中介軟體等。至於什麼是中介軟體,簡單說,中介軟體(middleware)就是處理HTTP請求的函式,用來完成各種特定的任務,比如檢查使用者是否登入、分析資料、以及其他在需要最終將資料傳送給使用者之前完成的任務。它最大的特點就是,一箇中間件處理完,再傳遞給下一個中介軟體。

比如上面建立http伺服器後所做的動作就是設定一堆中介軟體來完成特定的任務來處理http請求的:

[javascript] view plaincopy
  1. var main = function (args, readyCb, doneCb) {  
  2.     ...  
  3.   rest.use(domainMiddleware());  
  4.   rest.use(morgan(function (tokens, req, res) {  
  5.     // morgan output is redirected straight to winston
  6.     logger.info(requestEndLoggingFormat(tokens, req, res),  
  7.       (res.jsonResp || '').grey);  
  8.   }));  
  9.   rest.use(favicon(path.join(__dirname, 'static/favicon.ico')));  
  10.   rest.use(express.static(path.join(__dirname, 'static')));  
  11.   rest.use(allowCrossDomain);  
  12.   rest.use(parserWrap);  
  13.   rest.use(bodyParser.urlencoded({extended: true}));  
  14.   // 8/18/14: body-parser requires that we supply the limit field to ensure the server can
  15.   // handle requests large enough for Appium's use cases. Neither Node nor HTTP spec defines a max
  16.   // request size, so any hard-coded request-size limit is arbitrary. Units are in bytes (ie "gb" == "GB",
  17.   // not "Gb"). Using 1GB because..., well because it's arbitrary and 1GB is sufficiently large for 99.99%
  18.   // of testing scenarios while still providing an upperbounds to reduce the odds of squirrelliness.
  19.   rest.use(bodyParser.json({limit: '1gb'}));  
  20.   ...  
  21. }  
我們這裡以第一個中介軟體為例子,看看它是怎麼通過domain這個模組來處理異常的(注意notejs是出名的單執行緒,非阻塞的框架,正常的try,catch是抓獲不了任何異常處理的,因為相應的程式碼不會等待如i/o操作等結果就立刻返回的,所以nodejs後來引入了domain這個模組來專門處理這種事情。其實我認為原理還是回撥,把http過來的nodejs提供的request,和response引數作為回撥函式的引數提供給回撥函式,然後一旦相應事件發生了就出發回調然後操作這兩個引數進行返回):
  1. module.exports.domainMiddleware = function () {  
  2.   return function (req, res, next) {  
  3.     var reqDomain = domain.create();  
  4.     reqDomain.add(req);  
  5.     reqDomain.add(res);  
  6.     res.on('close', function () {  
  7.       setTimeout(function () {  
  8.         reqDomain.dispose();  
  9.       }, 5000);  
  10.     });  
  11.     reqDomain.on('error', function (err) {  
  12.       logger.error('Unhandled error:', err.stack, getRequestContext(req));  
  13.     });  
  14.     reqDomain.run(next);  
  15.   };  
  16. };  
大家可以看到這個回撥中介軟體(函式):
  • 先建立一個domain
  • 然後把http的request和response增加到這個domain裡面
  • 然後鑑定相應的事件發生,比如發生error的時候就列印相應的日記
  • 然後呼叫下一個中介軟體來進行下一個任務處理
其他的中介軟體這裡我就不花時間一一去分析了,大家各自跟蹤下或者google應該就清楚是用來做什麼事情的了,因為我自己就是這麼幹的。 main函式在為http伺服器建立好中介軟體後,下一步就是去建立一個appium伺服器,注意這裡appium伺服器和http伺服器是不一樣的,http伺服器是用來監聽appium客戶端,也就是selenium,我們的指令碼傳送過來的http的rest請求的;appium伺服器除了擁有著這個http伺服器與客戶端通訊之外,還包含其他如和目標裝置端的bootstrap通訊等功能。 [javascript] view plaincopy
  1. var main = function (args, readyCb, doneCb) {  
  2.     ...  
  3.   // Instantiate the appium instance
  4.   var appiumServer = appium(args);  
  5.   // Hook up REST http interface
  6.   appiumServer.attachTo(rest);  
  7.     ...  
  8. }  
這裡會去呼叫appium建構函式例項化一個appium伺服器,然後把剛才建立的express物件rest給傳到該伺服器例項儲存起來。那麼這裡這個appium類又是從哪裡來的呢?我們返回到main.js的前面:
  1. var http = require('http')  
  2.   , express = require('express')  
  3.   ...  
  4.   , appium = require('../appium.js')  
可以看到它是從上層目錄的appium.js匯出來的,我們進去看看它的建構函式: [javascript] view plaincopy
  1. var Appium = function (args) {  
  2.   this.args = _.clone(args);  
  3.   this.args.callbackAddress = this.args.callbackAddress || this.args.address;  
  4.   this.args.callbackPort = this.args.callbackPort || this.args.port;  
  5.   // we need to keep an unmodified copy of the args so that we can restore
  6.   // any server arguments between sessions to their default values
  7.   // (otherwise they might be overridden by session-level caps)
  8.   this.serverArgs = _.clone(this.args);  
  9.   this.rest = null;  
  10.   this.webSocket = null;  
  11.   this.deviceType = null;  
  12.   this.device = null;  
  13.   this.sessionId = null;  
  14.   this.desiredCapabilities = {};  
  15.   this.oldDesiredCapabilities = {};  
  16.   this.session = null;  
  17.   this.preLaunched = false;  
  18.   this.sessionOverride = this.args.sessionOverride;  
  19.   this.resetting = false;  
  20.   this.defCommandTimeoutMs = this.args.defaultCommandTimeout * 1000;  
  21.   this.commandTimeoutMs = this.defCommandTimeoutMs;  
  22.   this.commandTimeout = null;  
  23. };  
可以看到初始化的時候this.rest這個成員變數是設定成null的,所以剛提到的main中的最後一步就是呼叫這個appium.js中的attachTo方法把express例項rest給設定到appium伺服器物件裡面的:
  1. Appium.prototype.attachTo = function (rest) {  
  2.   this.rest = rest;  
  3. };  
例項化appium 伺服器後,下一步就是要設定好從client端過來的請求的資料路由了,這個下一篇文章討論Appium Server如何跟bootstrap通訊時會另外進行討論,因為它涉及到如何將客戶端的請求傳送給bootstrap進行處理。 [javascript] view plaincopy
  1. var main = function (args, readyCb, doneCb) {  
  2.     ...  
  3.   routing(appiumServer);  
  4.     ...  
  5. }  
設定好路由後,main往後就會對伺服器做一些基本配置,然後呼叫helpers.js的startListening方法來開啟http伺服器的監聽工作,大家要注意到現在為止http伺服器server時建立起來了,但是還沒有真正開始監聽接受連線和資料的的工作的: [javascript] view plaincopy
  1. var main = function (args, readyCb, doneCb) {  
  2.     ...  
  3.      function (cb) {  
  4.       startListening(server, args, parser, appiumVer, appiumRev, appiumServer, cb);  
  5.     }  
  6.     ...  
  7. }  
注意它傳入的幾個重要引數:
  • server:基於express例項建立的http伺服器例項
  • args:引數
  • parser:引數解析器
  • appiumVer: 在‘'../../package.json'‘檔案中指定的appium版本號
  • appiumRev:通過上面提及的進行伺服器基本配置時解析出來的版本修正號
  • appiumServer: 剛才建立的appium伺服器例項,裡面包含了一個express例項,這個例項和第一個引數server用來建立http伺服器的express例項時一樣的

3. 啟動http伺服器監聽

到了這裡,整個基於Express的http伺服器已經準備妥當,只差一個go命令了,這個go命令就是我們這裡的啟動監聽方法: [javascript] view plaincopy
  1. module.exports.startListening = function (server, args, parser, appiumVer, appiumRev, appiumServer, cb) {  
  2.   var alreadyReturned = false;  
  3.   server.listen(args.port, args.address, function () {  
  4.     var welcome = "Welcome to Appium v" + appiumVer;  
  5.     if (appiumRev) {  
  6.       welcome += " (REV " + appiumRev + ")";  
  7.     }  
  8.     logger.info(welcome);  
  9.     var logMessage = "Appium REST http interface listener started on " +  
  10.                      args.address + ":" + args.port;  
  11.     logger.info(logMessage);  
  12.     startAlertSocket(server, appiumServer);  
  13.     if (args.nodeconfig !== null) {  
  14.       gridRegister.registerNode(args.nodeconfig, args.address, args.port);  
  15.     }  
  16.     var showArgs = getNonDefaultArgs(parser, args);  
  17.     if (_.size(showArgs)) {  
  18.       logger.debug("Non-default server args: " + JSON.stringify(showArgs));  
  19.     }  
  20.     var deprecatedArgs = getDeprecatedArgs(parser, args);  
  21.     if (_.size(deprecatedArgs)) {  
  22.       logger.warn("Deprecated server args: " + JSON.stringify(deprecatedArgs));  
  23.     }  
  24.     logger.info('Console LogLevel: ' + logger.transports.console.level);  
  25.     if (logger.transports.file) {  
  26.       logger.info('File LogLevel: ' + logger.transports.file.level);  
  27.     }  
  28.   });  
這個方法看上去很長,其實很多都是傳給監聽方法的回撥函式的後期引數檢查和資訊列印以及錯誤處理,關鍵的就是最前面的啟動http監聽的方法: [javascript] view plaincopy
  1. server.listen(args.port, args.address, function () {  
  2.         ...  
這裡的server就是上面提及的基於express框架搭建的Http Server例項,傳入的引數:
  • args.port:就是第一節提起的http伺服器的監聽埠,預設4723
  • args.adress:就是第一節提及的http伺服器監聽地址,預設本地
  • function:一系列回撥函式來進行錯誤處理等

4. 小結

這篇文章主要描述了appium server是如何建立一個基於express框架的http伺服器,然後啟動相應的監聽方法來獲得從appium client端傳送過來的資料,至於獲取到資料後如何與目標安卓裝置的bootstrap進行通訊,敬請大家期待本人的下一篇文章。本人更多的文章請參考我的部落格:http://blog.csdn.net/zhubaitian

Item

Description

Warning

Author

天地會珠海分舵

轉載請註明出處!

更多精彩文章請檢視本人部落格!