Appium Server 源码分析之启动运行Express http服务器

通过上一个系列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就会先去导入必须的模块然后对启动参数进行初始化:
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();
参数的解析时在‘../lib/server/parser.js'里面的,文件一开始就指定使用了nodejs提供的专门对参数进行解析的argparse模块的 ArgumentPaser类,具体这个类时怎么用的大家自己google就好了:
var ap = require('argparse').ArgumentParser
然后该javascript脚本就会实例化这个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;
};
ArgumentPaser会对已经定义好的每一个args进行分析,如果有提供对应参数设置的就进行设置,没有的话就会提供默认值,这里我们提几个比较重要的参数作为例子:
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); */ });
}
这里appium这个变量是从其他地方导入了,我们回到脚本较前位置:

var args = parser().parseArgs();
logFactory.init(args);

var appium  = require('../lib/server/main.js');
可以看到,这个脚本首先会调用parser的模块去分析用户输入的参数然后保存起来(至于怎么解析的就不去看了,无非是读取每个参数然后保存起来而已,大家看下本人前面分析的其他源码是怎么获得启动参数的就清楚了),然后往下我们就可以看到appium这个变量是从'../lib/server/main.js'这个脚本导进来的,所以我们就需要去到这个脚本,浏览到脚本最下面的一行:

module.exports.run = main;
它是把main这个方法以run的名字导出给其他模块使用了,所以回到了最上面的:

  appium.run(args, function () { /* console.log('Rock and roll.'.grey); */ });

就相当于调用了'main.js'的:

  main(args, function () { /* console.log('Rock and roll.'.grey); */ });
我们往下看main这个方法,首先它会做一些基本的参数检查,然后初始化了一个express实例(Express是目前最流行的基于Node.js的Web开发框架,提供各种模块,可以快速地搭建一个具有完整功能的网站,强烈建议不清楚其使用的童鞋先去看下牛人阮一峰的《 Express框架》),然后如平常一样创建一个http服务器:

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'}));
  ...
}
我们这里以第一个中间件为例子,看看它是怎么通过domain这个模块来处理异常的(注意notejs是出名的单线程,非阻塞的框架,正常的try,catch是抓获不了任何异常处理的,因为相应的代码不会等待如i/o操作等结果就立刻返回的,所以nodejs后来引入了domain这个模块来专门处理这种事情。其实我认为原理还是回调,把http过来的nodejs提供的request,和response参数作为回调函数的参数提供给回调函数,然后一旦相应事件发生了就出发回调然后操作这两个参数进行返回):

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的时候就打印相应的日记
  • 然后调用下一个中间件来进行下一个任务处理
其他的中间件这里我就不花时间一一去分析了,大家各自跟踪下或者google应该就清楚是用来做什么事情的了,因为我自己就是这么干的。
main函数在为http服务器建立好中间件后,下一步就是去创建一个appium服务器,注意这里appium服务器和http服务器是不一样的,http服务器是用来监听appium客户端,也就是selenium,我们的脚本发送过来的http的rest请求的;appium服务器除了拥有着这个http服务器与客户端通信之外,还包含其他如和目标设备端的bootstrap通信等功能。
var main = function (args, readyCb, doneCb) {
    ...
  // Instantiate the appium instance
  var appiumServer = appium(args);
  // Hook up REST http interface
  appiumServer.attachTo(rest);
    ...
}
这里会去调用appium构造函数实例化一个appium服务器,然后把刚才创建的express对象rest给传到该服务器实例保存起来。那么这里这个appium类又是从哪里来的呢?我们返回到main.js的前面:
var http = require('http')
  , express = require('express')
  ...
  , appium = require('../appium.js')
可以看到它是从上层目录的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;
};
可以看到初始化的时候this.rest这个成员变量是设置成null的,所以刚提到的main中的最后一步就是调用这个appium.js中的attachTo方法把express实例rest给设置到appium服务器对象里面的:
Appium.prototype.attachTo = function (rest) {
  this.rest = rest;
};
实例化appium 服务器后,下一步就是要设置好从client端过来的请求的数据路由了,这个下一篇文章讨论Appium Server如何跟bootstrap通信时会另外进行讨论,因为它涉及到如何将客户端的请求发送给bootstrap进行处理。
var main = function (args, readyCb, doneCb) {
    ...
  routing(appiumServer);
    ...
}
设置好路由后,main往后就会对服务器做一些基本配置,然后调用helpers.js的startListening方法来开启http服务器的监听工作,大家要注意到现在为止http服务器server时创建起来了,但是还没有真正开始监听接受连接和数据的的工作的:
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);
    }
  });
这个方法看上去很长,其实很多都是传给监听方法的回调函数的后期参数检查和信息打印以及错误处理,关键的就是最前面的启动http监听的方法:
server.listen(args.port, args.address, function () {
        ...
这里的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


 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian



  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值