通过上一个系列Appium Android Bootstrap源码分析我们了解到了appium在安卓目标机器上是如何通过bootstrap这个服务来接收appium从pc端发送过来的命令,并最终使用uiautomator框架进行处理的。大家还没有这方面的背景知识的话建议先去看一下,以下列出来方便大家参考:
- 《Appium Android Bootstrap源码分析之简介》
- 《Appium Android Bootstrap源码分析之控件AndroidElement》
- 《Appium Android Bootstrap源码分析之命令解析执行》
- 《Appium Android Bootstrap源码分析之启动运行》
那么我们知道了目标机器端的处理后,我们理所当然需要搞清楚bootstrap客户端,也就是Appium Server是如何工作的,这个就是这个系列文章的初衷。
Appium Server其实拥有两个主要的功能:
- 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令
- 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把这些命令发送给目标安卓机器的bootstrap来驱动uiatuomator来做事情
开始之前先声明一下,因为appium server是基于当今热本的nodejs编写的,而我本人并不是写javascript出身的,只是在写这篇文章的时候花了几个小时去了解了下javascript的语法,但是我相信语言是相同的,去看懂这些代码还是没有太大问题的。但,万一当中真有误导大家的地方,还敬请大家指出来,以免祸害读者...
1.运行参数准备
- 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();
- var ap = require('argparse').ArgumentParser
- // 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;
- };
- 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服务器监听的ip地址,没有指定的话默认就监听本机
- port :指定http服务器监听的端口,没有指定的话默认监听4723端口
- bootstrap-port :指定要连接上安卓目标机器端的socket监听端口,默认4724
2. 创建Express HTTP服务器
Appium支持两种方式启动,一种是在提供--shell的情况下提供交互式编辑器的启动方式,这个就好比你直接在命令行输入node,然后弹出命令行交互输入界面让你一行行的输入调试运行;另外一种就是我们正常的启动方式而不需要用户的交互,这个也就是我们今天关注的重点:
- if (process.argv[2] && process.argv[2].trim() === "--shell") {
- startRepl();
- } else {
- appium.run(args, function () { /* console.log('Rock and roll.'.grey); */ });
- }
- var args = parser().parseArgs();
- logFactory.init(args);
- var appium = require('../lib/server/main.js');
- module.exports.run = main;
- appium.run(args, function () { /* console.log('Rock and roll.'.grey); */ });
就相当于调用了'main.js'的:
- main(args, function () { /* console.log('Rock and roll.'.grey); */ });
- var main = function (args, readyCb, doneCb) {
- ...
- var rest = express()
- , server = http.createServer(rest);
- ...
- }
只是这个http服务器跟普通的服务器唯一的差别是createServer方法的参数,从一个回调函数变成了一个Epress对象的实例。它使用了express框架对http模块进行再包装的,这样它就可以很方便的使用express的功能和方法来快速建立http服务,比如:
- 通过 express的get,post等快速设置路由。用于指定不同的访问路径所对应的回调函数,这叫做“路由”(routing),这个也是为什么说express是符合RestFul风格的框架的原因之一了
- 使用express的use方法来设置中间件等。至于什么是中间件,简单说,中间件(middleware)就是处理HTTP请求的函数,用来完成各种特定的任务,比如检查用户是否登录、分析数据、以及其他在需要最终将数据发送给用户之前完成的任务。它最大的特点就是,一个中间件处理完,再传递给下一个中间件。
比如上面创建http服务器后所做的动作就是设置一堆中间件来完成特定的任务来处理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'}));
- ...
- }
- 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);
- };
- };
- 先创建一个domain
- 然后把http的request和response增加到这个domain里面
- 然后鉴定相应的事件发生,比如发生error的时候就打印相应的日记
- 然后调用下一个中间件来进行下一个任务处理
- var main = function (args, readyCb, doneCb) {
- ...
- // Instantiate the appium instance
- var appiumServer = appium(args);
- // Hook up REST http interface
- appiumServer.attachTo(rest);
- ...
- }
- var http = require('http')
- , express = require('express')
- ...
- , appium = require('../appium.js')
- 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;
- };
- Appium.prototype.attachTo = function (rest) {
- this.rest = rest;
- };
- var main = function (args, readyCb, doneCb) {
- ...
- routing(appiumServer);
- ...
- }
- var main = function (args, readyCb, doneCb) {
- ...
- function (cb) {
- startListening(server, args, parser, appiumVer, appiumRev, appiumServer, cb);
- }
- ...
- }
- server:基于express实例创建的http服务器实例
- args:参数
- parser:参数解析器
- appiumVer: 在‘'../../package.json'‘文件中指定的appium版本号
- appiumRev:通过上面提及的进行服务器基本配置时解析出来的版本修正号
- appiumServer: 刚才创建的appium服务器实例,里面包含了一个express实例,这个实例和第一个参数server用来创建http服务器的express实例时一样的
3. 启动http服务器监听
到了这里,整个基于Express的http服务器已经准备妥当,只差一个go命令了,这个go命令就是我们这里的启动监听方法:- 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);
- }
- });
- server.listen(args.port, args.address, function () {
- ...
- args.port :就是第一节提起的http服务器的监听端口,默认4723
- args.adress :就是第一节提及的http服务器监听地址,默认本地
- function :一系列回调函数来进行错误处理等