通过上一个系列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进行通信,敬请大家期待本人的下一篇文章。

 
作者 自主博客 微信服务号及扫描码 CSDN
天地会珠海分舵 http://techgogogo.com 服务号:TechGoGoGo扫描码: http://blog.csdn.net/zhubaitian

Appium Server 源码分析之启动运行Express http服务器的更多相关文章

  1. Appium Server源码分析之作为Bootstrap客户端

    Appium Server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把 ...

  2. Appium Android Bootstrap源码分析之启动运行

    通过前面的两篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>和<Appium Android Bootstrap源码分析之命令解析 ...

  3. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  4. Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://bl ...

  5. 转载-FileZilla Server源码分析(1)

    FileZilla Server源码分析(1) 分类: VC 2012-03-27 17:32 2363人阅读 评论(0) 收藏 举报 serversocketftp服务器usersockets工作 ...

  6. u-boot 源码分析(1) 启动过程分析

    u-boot 源码分析(1) 启动过程分析 文章目录 u-boot 源码分析(1) 启动过程分析 前言 配置 源码结构 api arch board common cmd drivers fs Kbu ...

  7. v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码

    本篇关键词:内核重定位.MMU.SVC栈.热启动.内核映射表 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 鸿蒙内核源码分析(汇编基础) | ...

  8. Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.c ...

  9. 安卓MonkeyRunner源码分析之启动

    在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的.但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解. 从几个月前开 ...

随机推荐

  1. PoolBoy

    PoolBoy  source code : https://github.com/devinus/poolboy Checkout ready({checkout, Block, Timeout}, ...

  2. 【转】Android 收集已发布程序的崩溃信息

    我们写程序的时候都希望能写出一个没有任何Bug的程序,期望在任何情况下都不会发生程序崩溃.不过理想是丰满的,现实是骨感的.没有一个程序员能保证自己写的程序绝对不会出现异常崩溃.特别是针对用户数达到几十 ...

  3. Spring : 征服数据库 (两)

    本节介绍Spring和ORM集成框架.尽管Hibernate在开源ORM 社区很受欢迎.但是,本文将MyBatis案例解说.也MyBatis和Hibernate好坏是没有意义的,主要看实际需求,有兴趣 ...

  4. 使用Java中间MessageDigest该文本MD5加密(Java中间MD5样品加密算法演示)

    原文地址:http://www.wenboxz.com 版权声明:本文博客原创文章,博客,未经同意,不得转载.

  5. MapXtreme DJ最短路径算法 全路径搜索算法

    包括最短路径,全路径搜索算法演示程序请在http://pan.baidu.com/s/1jG9gKMM#dir/path=%2F%E4%BA%A7%E5%93%81%2FDemos 找 ShortWa ...

  6. svn常见错误汇总

    comment中的换行.把换行去掉就可以了

  7. ZOJ 3822 可能性DP

    http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3822 本场比赛之前,我记得.见WALK概率路DP称号.那么它应该是可以考虑 ...

  8. Oracle 多表关联更新

    drop table course; create table course ( id integer, teacherNo integer, teacherDesc ), teacherName ) ...

  9. Java数据结构与算法(20) - ch08树

    树的主要算法有插入,查找,显示,遍历,删除,其中显示和删除略微复杂. package chap08.tree; import java.io.BufferedReader; import java.i ...

  10. PHP课程十大 PHP图像处理功能和实现的验证码

    假如你喜欢这个博客,访问这个博客地址:http://blog.csdn.net/junzaivip 总结: gd绘图库: 数学函数 PHP图片处理函数 图片处理函数使用场景 1.验证码 2.缩放 3. ...