通過上一個系列Appium Android Bootstrap源碼分析我們了解到了appium在安卓目標(biāo)機器上是如何通過bootstrap這個服務(wù)來接收appium從pc端發(fā)送過來的命令,并最終使用uiautomator框架進行處理的。大家還沒有這方面的背景知識的話建議先去看一下,以下列出來方便大家參考:
成都創(chuàng)新互聯(lián)服務(wù)項目包括公安網(wǎng)站建設(shè)、公安網(wǎng)站制作、公安網(wǎng)頁制作以及公安網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,公安網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到公安省份的部分城市,未來相信會繼續(xù)擴大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!《Appium Android Bootstrap源碼分析之簡介》《Appium Android Bootstrap源碼分析之控件AndroidElement》《Appium Android Bootstrap源碼分析之命令解析執(zhí)行》《Appium Android Bootstrap源碼分析之啟動運行》
那么我們知道了目標(biāo)機器端的處理后,我們理所當(dāng)然需要搞清楚bootstrap客戶端,也就是Appium Server是如何工作的,這個就是這個系列文章的初衷。
Appium Server其實擁有兩個主要的功能:
它是個http服務(wù)器,它專門接收從客戶端通過基于http的REST協(xié)議發(fā)送過來的命令他是bootstrap客戶端:它接收到客戶端的命令后,需要想辦法把這些命令發(fā)送給目標(biāo)安卓機器的bootstrap來驅(qū)動uiatuomator來做事情
我們今天描述的就是第一點。大家先看下我以前畫的一個appium架構(gòu)圖好有個基本概念:Appium Server大概是在哪個位置進行工作的
同時我們也先看下Appium Server的源碼布局,后有一個基本的代碼結(jié)構(gòu)概念:
開始之前先聲明一下,因為appium server是基于當(dāng)今熱本的nodejs編寫的,而我本人并不是寫javascript出身的,只是在寫這篇文章的時候花了幾個小時去了解了下javascript的語法,但是我相信語言是相同的,去看懂這些代碼還是沒有太大問題的。但,萬一當(dāng)中真有誤導(dǎo)大家的地方,還敬請大家指出來,以免禍害讀者...
1.運行參數(shù)準(zhǔn)備
Appium 服務(wù)器啟動的入口就在bin下面的appium.js這個文件里面.在一開始的時候這個javascript就會先去導(dǎo)入必須的模塊然后對啟動參數(shù)進行初始化:
var net = require(\'net\') , repl = require(\'repl\') , logFactory = require(\'../lib/server/logger.js\') , parser = require(\'../lib/server/parser.js\'); require(\'colors\'); var args = parser().parseArgs();參數(shù)的解析時在‘../lib/server/parser.js\'里面的,文件一開始就指定使用了nodejs提供的專門對參數(shù)進行解析的argparse模塊的 ArgumentPaser類,具體這個類時怎么用的大家自己google就好了:
var ap = require(\'argparse\').ArgumentParser然后該javascript腳本就會實例化這個ArgumentParser來啟動對參數(shù)的解析:
// Setup all the command line argument parsing module.exports = function () { var parser = new ap({ version: pkgObj.version, addHelp: true, description: \'A webdriver-compatible server for use with native and hybrid iOS and Android applications.\' }); _.each(args, function (arg) { parser.addArgument(arg[0], arg[1]); }); parser.rawArgs = args; return parser; };ArgumentPaser會對已經(jīng)定義好的每一個args進行分析,如果有提供對應(yīng)參數(shù)設(shè)置的就進行設(shè)置,沒有的話就會提供默認(rèn)值,這里我們提幾個比較重要的參數(shù)作為例子:
var args = [ ... [[\'-a\', \'--address\'], { defaultValue: \'0.0.0.0\' , required: false , example: "0.0.0.0" , help: \'IP Address to listen on\' }], ... [[\'-p\', \'--port\'], { defaultValue: 4723 , required: false , type: \'int\' , example: "4723" , help: \'port to listen on\' }], ... [[\'-bp\', \'--bootstrap-port\'], { defaultValue: 4724 , dest: \'bootstrapPort\' , required: false , type: \'int\' , example: "4724" , help: \'(Android-only) port to use on device to talk to Appium\' }], ... ]; address :指定http服務(wù)器監(jiān)聽的ip地址,沒有指定的話默認(rèn)就監(jiān)聽本機port :指定http服務(wù)器監(jiān)聽的端口,沒有指定的話默認(rèn)監(jiān)聽4723端口bootstrap-port :指定要連接上安卓目標(biāo)機器端的socket監(jiān)聽端口,默認(rèn)4724
2. 創(chuàng)建Express HTTP服務(wù)器
Appium支持兩種方式啟動,一種是在提供--shell的情況下提供交互式編輯器的啟動方式,這個就好比你直接在命令行輸入node,然后彈出命令行交互輸入界面讓你一行行的輸入調(diào)試運行;另外一種就是我們正常的啟動方式而不需要用戶的交互,這個也就是我們今天關(guān)注的重點:
if (process.argv[2] && process.argv[2].trim() === "--shell") { startRepl(); } else { appium.run(args, function () { /* console.log(\'Rock and roll.\'.grey); */ }); }這里appium這個變量是從其他地方導(dǎo)入了,我們回到腳本較前位置:
var args = parser().parseArgs(); logFactory.init(args); var appium = require(\'../lib/server/main.js\');可以看到,這個腳本首先會調(diào)用parser的模塊去分析用戶輸入的參數(shù)然后保存起來(至于怎么解析的就不去看了,無非是讀取每個參數(shù)然后保存起來而已,大家看下本人前面分析的其他源碼是怎么獲得啟動參數(shù)的就清楚了),然后往下我們就可以看到appium這個變量是從\'../lib/server/main.js\'這個腳本導(dǎo)進來的,所以我們就需要去到這個腳本,瀏覽到腳本最下面的一行:
module.exports.run = main;它是把main這個方法以run的名字導(dǎo)出給其他模塊使用了,所以回到了最上面的:
appium.run(args, function () { /* console.log(\'Rock and roll.\'.grey); */ });
就相當(dāng)于調(diào)用了\'main.js\'的:
main(args, function () { /* console.log(\'Rock and roll.\'.grey); */ });我們往下看main這個方法,首先它會做一些基本的參數(shù)檢查,然后初始化了一個express實例(Express是目前最流行的基于Node.js的Web開發(fā)框架,提供各種模塊,可以快速地搭建一個具有完整功能的網(wǎng)站,強烈建議不清楚其使用的童鞋先去看下牛人阮一峰的《Express框架》),然后如平常一樣創(chuàng)建一個http服務(wù)器:
var main = function (args, readyCb, doneCb) { ... var rest = express() , server = http.createServer(rest); ... }
只是這個http服務(wù)器跟普通的服務(wù)器唯一的差別是createServer方法的參數(shù),從一個回調(diào)函數(shù)變成了一個Epress對象的實例。它使用了express框架對http模塊進行再包裝的,這樣它就可以很方便的使用express的功能和方法來快速建立http服務(wù),比如:
通過 express的get,post等快速設(shè)置路由。用于指定不同的訪問路徑所對應(yīng)的回調(diào)函數(shù),這叫做“路由”(routing),這個也是為什么說express是符合RestFul風(fēng)格的框架的原因之一了使用express的use方法來設(shè)置中間件等。至于什么是中間件,簡單說,中間件(middleware)就是處理HTTP請求的函數(shù),用來完成各種特定的任務(wù),比如檢查用戶是否登錄、分析數(shù)據(jù)、以及其他在需要最終將數(shù)據(jù)發(fā)送給用戶之前完成的任務(wù)。它的特點就是,一個中間件處理完,再傳遞給下一個中間件。
比如上面創(chuàng)建http服務(wù)器后所做的動作就是設(shè)置一堆中間件來完成特定的任務(wù)來處理http請求的:
var main = function (args, readyCb, doneCb) { ... rest.use(domainMiddleware()); rest.use(morgan(function (tokens, req, res) { // morgan output is redirected straight to winston logger.info(requestEndLoggingFormat(tokens, req, res), (res.jsonResp || \'\').grey); })); rest.use(favicon(path.join(__dirname, \'static/favicon.ico\'))); rest.use(express.static(path.join(__dirname, \'static\'))); rest.use(allowCrossDomain); rest.use(parserWrap); rest.use(bodyParser.urlencoded({extended: true})); // 8/18/14: body-parser requires that we supply the limit field to ensure the server can // handle requests large enough for Appium\'s use cases. Neither Node nor HTTP spec defines a max // request size, so any hard-coded request-size limit is arbitrary. Units are in bytes (ie "gb" == "GB", // not "Gb"). Using 1GB because..., well because it\'s arbitrary and 1GB is sufficiently large for 99.99% // of testing scenarios while still providing an upperbounds to reduce the odds of squirrelliness. rest.use(bodyParser.json({limit: \'1gb\'})); ... }我們這里以第一個中間件為例子,看看它是怎么通過domain這個模塊來處理異常的(注意notejs是出名的單線程,非阻塞的框架,正常的try,catch是抓獲不了任何異常處理的,因為相應(yīng)的代碼不會等待如i/o操作等結(jié)果就立刻返回的,所以nodejs后來引入了domain這個模塊來專門處理這種事情。其實我認(rèn)為原理還是回調(diào),把http過來的nodejs提供的request,和response參數(shù)作為回調(diào)函數(shù)的參數(shù)提供給回調(diào)函數(shù),然后一旦相應(yīng)事件發(fā)生了就出發(fā)回調(diào)然后操作這兩個參數(shù)進行返回):
module.exports.domainMiddleware = function () { return function (req, res, next) { var reqDomain = domain.create(); reqDomain.add(req); reqDomain.add(res); res.on(\'close\', function () { setTimeout(function () { reqDomain.dispose(); }, 5000); }); reqDomain.on(\'error\', function (err) { logger.error(\'Unhandled error:\', err.stack, getRequestContext(req)); }); reqDomain.run(next); }; };大家可以看到這個回調(diào)中間件(函數(shù)):
先創(chuàng)建一個domain然后把http的request和response增加到這個domain里面然后鑒定相應(yīng)的事件發(fā)生,比如發(fā)生error的時候就打印相應(yīng)的日記然后調(diào)用下一個中間件來進行下一個任務(wù)處理
其他的中間件這里我就不花時間一一去分析了,大家各自跟蹤下或者google應(yīng)該就清楚是用來做什么事情的了,因為我自己就是這么干的。
main函數(shù)在為http服務(wù)器建立好中間件后,下一步就是去創(chuàng)建一個appium服務(wù)器,注意這里appium服務(wù)器和http服務(wù)器是不一樣的,http服務(wù)器是用來監(jiān)聽appium客戶端,也就是selenium,我們的腳本發(fā)送過來的http的rest請求的;appium服務(wù)器除了擁有著這個http服務(wù)器與客戶端通信之外,還包含其他如和目標(biāo)設(shè)備端的bootstrap通信等功能。
var main = function (args, readyCb, doneCb) { ... // Instantiate the appium instance var appiumServer = appium(args); // Hook up REST http interface appiumServer.attachTo(rest); ... }這里會去調(diào)用appium構(gòu)造函數(shù)實例化一個appium服務(wù)器,然后把剛才創(chuàng)建的express對象rest給傳到該服務(wù)器實例保存起來。那么這里這個appium類又是從哪里來的呢?我們返回到main.js的前面:
var http = require(\'http\') , express = require(\'express\') ... , appium = require(\'../appium.js\')可以看到它是從上層目錄的appium.js導(dǎo)出來的,我們進去看看它的構(gòu)造函數(shù):
var Appium = function (args) { this.args = _.clone(args); this.args.callbackAddress = this.args.callbackAddress || this.args.address; this.args.callbackPort = this.args.callbackPort || this.args.port; // we need to keep an unmodified copy of the args so that we can restore // any server arguments between sessions to their default values // (otherwise they might be overridden by session-level caps) this.serverArgs = _.clone(this.args); this.rest = null; this.webSocket = null; this.deviceType = null; this.device = null; this.sessionId = null; this.desiredCapabilities = {}; this.oldDesiredCapabilities = {}; this.session = null; this.preLaunched = false; this.sessionOverride = this.args.sessionOverride; this.resetting = false; this.defCommandTimeoutMs = this.args.defaultCommandTimeout * 1000; this.commandTimeoutMs = this.defCommandTimeoutMs; this.commandTimeout = null; };可以看到初始化的時候this.rest這個成員變量是設(shè)置成null的,所以剛提到的main中的最后一步就是調(diào)用這個appium.js中的attachTo方法把express實例rest給設(shè)置到appium服務(wù)器對象里面的:
Appium.prototype.attachTo = function (rest) { this.rest = rest; };實例化appium 服務(wù)器后,下一步就是要設(shè)置好從client端過來的請求的數(shù)據(jù)路由了,這個下一篇文章討論Appium Server如何跟bootstrap通信時會另外進行討論,因為它涉及到如何將客戶端的請求發(fā)送給bootstrap進行處理。
var main = function (args, readyCb, doneCb) { ... routing(appiumServer); ... }
設(shè)置好路由后,main往后就會對服務(wù)器做一些基本配置,然后調(diào)用helpers.js的startListening方法來開啟http服務(wù)器的監(jiān)聽工作,大家要注意到現(xiàn)在為止http服務(wù)器server時創(chuàng)建起來了,但是還沒有真正開始監(jiān)聽接受連接和數(shù)據(jù)的的工作的:
var main = function (args, readyCb, doneCb) { ... function (cb) { startListening(server, args, parser, appiumVer, appiumRev, appiumServer, cb); } ... }注意它傳入的幾個重要參數(shù):
server:基于express實例創(chuàng)建的http服務(wù)器實例args:參數(shù)parser:參數(shù)解析器appiumVer: 在‘\'../../package.json\'‘文件中指定的appium版本號appiumRev:通過上面提及的進行服務(wù)器基本配置時解析出來的版本修正號appiumServer: 剛才創(chuàng)建的appium服務(wù)器實例,里面包含了一個express實例,這個實例和第一個參數(shù)server用來創(chuàng)建http服務(wù)器的express實例時一樣的
3. 啟動http服務(wù)器監(jiān)聽 到了這里,整個基于Express的http服務(wù)器已經(jīng)準(zhǔn)備妥當(dāng),只差一個go命令了,這個go命令就是我們這里的啟動監(jiān)聽方法:
module.exports.startListening = function (server, args, parser, appiumVer, appiumRev, appiumServer, cb) { var alreadyReturned = false; server.listen(args.port, args.address, function () { var welcome = "Welcome to Appium v" + appiumVer; if (appiumRev) { welcome += " (REV " + appiumRev + ")"; } logger.info(welcome); var logMessage = "Appium REST http interface listener started on " + args.address + ":" + args.port; logger.info(logMessage); startAlertSocket(server, appiumServer); if (args.nodeconfig !== null) { gridRegister.registerNode(args.nodeconfig, args.address, args.port); } var showArgs = getNonDefaultArgs(parser, args); if (_.size(showArgs)) { logger.debug("Non-default server args: " + JSON.stringify(showArgs)); } var deprecatedArgs = getDeprecatedArgs(parser, args); if (_.size(deprecatedArgs)) { logger.warn("Deprecated server args: " + JSON.stringify(deprecatedArgs)); } logger.info(\'Console LogLevel: \' + logger.transports.console.level); if (logger.transports.file) { logger.info(\'File LogLevel: \' + logger.transports.file.level); } });這個方法看上去很長,其實很多都是傳給監(jiān)聽方法的回調(diào)函數(shù)的后期參數(shù)檢查和信息打印以及錯誤處理,關(guān)鍵的就是最前面的啟動http監(jiān)聽的方法:
server.listen(args.port, args.address, function () { ... 這里的server就是上面提及的基于express框架搭建的Http Server實例,傳入的參數(shù):
args.port :就是第一節(jié)提起的http服務(wù)器的監(jiān)聽端口,默認(rèn)4723args.adress :就是第一節(jié)提及的http服務(wù)器監(jiān)聽地址,默認(rèn)本地function :一系列回調(diào)函數(shù)來進行錯誤處理等
4. 小結(jié) 這篇文章主要描述了appium server是如何創(chuàng)建一個基于express框架的http服務(wù)器,然后啟動相應(yīng)的監(jiān)聽方法來獲得從appium client端發(fā)送過來的數(shù)據(jù)。