通过上一个系列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. 每天的学习经验:SharePoint 2013 定义自己添加的产品清单。Callout菜单项、文档关注、SharePoint服务机端对象模型查询

    前言: 前一段时间一直都比較忙.没有什么时间进行总结,刚好节前项目上线.同一时候趁着放假能够好好的对之前遇到的一些问题进行总结. 主要内容有使用SharePoint服务端对象模型进行查询.为Share ...

  3. Linux核心regulator建筑和准备

    电源引入的物种 (百度百科)LDO这是low dropout regulator,这意味着低压差线性稳压器.它相比于传统的线性调节器.传统的线性稳压器.例如78xx系列芯片需要输入电压比输出电压高2v ...

  4. “NET网络”进行中,多管齐下的人才力挫“”粗俗

            随着互联网的迅猛发展,一些不太干净.低俗的甚至色情的内容不断浮现.不仅严重影响了我们的上网体验,也成为扰乱互联网正常秩序的罪魁祸首,部分不法内容甚至给网民造成了一定的財产损失.在这样的 ...

  5. Android采用canvas绘制各种图形

    canvas通俗的说就是一个帆布,我们可以用刷子paint,就此随机抽签显卡. 原理: 能够canvas视Surface替代或接口.图形绘制Surface向上.Canvas封装了全部的绘制调用. 通过 ...

  6. ExecutorService invokeAll 实例(转)

    10个班级,每个班级20名学生,在指定的时间内查询每个班级学生的集合. package cn.com.ld.study.thread; import java.util.ArrayList; impo ...

  7. Codeforces 439C Devu and Partitioning of the Array(模拟)

    题目链接:Codeforces 439C Devu and Partitioning of the Array 题目大意:给出n个数,要分成k份,每份有若干个数,可是仅仅须要关注该份的和为奇数还是偶数 ...

  8. 采用Visual Stuidio 2010 创建网站栏

    采用Visual Stuidio 2010 创建网站栏 Visual Stuidio 2010 该项目模板使创建网站栏/内容类型和列表变得非常方便. 1. 管理员身份打开Visual Stuidio ...

  9. Redis源代码-数据结构Adlist双端列表

    Redis的Adlist实现了数据结构中的双端链表,整个结构例如以下: 链表节点定义: typedef struct listNode { struct listNode *prev; struct ...

  10. Flex4 Alert PopupManager 演示样本

    Flex4中间PopupManager分类似模仿桌面用户界面弹出窗体,有些人还喜欢JS弹出屏幕操作,底层接口灰色禁用掉. 创建需要要喷射形式的文件,码如下面: <?xml version=&qu ...