TCP UDP探索

最近开发新接口,为实现更高的性能,深入考察了下TCP,UDP通信协议。之前的开发当中都是基于HTTP的接口开发,简单易用,各个编程语言都支持;web服务器、中间件成熟,方便扩展;支持安全加密等等。与自定义Socket通信相比,HTTP仍然不够高效。

TCP协议,也叫传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。每建立一个TCP连接需要三次握手,关闭时又需要四次握手;传输过程当中,每发送一个包,都要接收一个确认回复,如果在一定时间内未收到,则重发,如果是同一块数据分包传输,则回复所需要的包序号;在发送和接收时都要计算校验和。由于有严格的数据包传输顺序和校验,因为接收到的数据将会是顺序。

UDP协议,也叫用户数据报协议(User Datagram Protocol)是一个简单的面向数据报的传输层协议。它只管数据包发送,并不没有TCP的连接控制、回复确认,并不知道对方是否接收成功。因为并不等待接收方回复,发送端可以连续发送数据,接收端接收到数据并不一定是顺序的。因此相比TCP,UDP的有更高的传输效率,更低的可靠性。如果需要可靠性,则需要发送确认包,类似TCP那样子。

通常HTTP协议基于TCP实现,保证收到的内容是完整、顺序的。在HTTP 1.0时,每一次请求都需要建立TCP连接,关闭TCP连接;在HTTP 1.1时,同一TCP连接则可以在活跃(Keep-Live)间期内复用。HTTP是无连接的,每次处理完成请求即断开连接,一方面可以节省服务器资源,另一方面却浪费连接时间。如果需要维持客户端的Keep-Alive状态,则又需要消耗更多服务器资源,需要权衡。

WebSocket是基于TCP 连接上进行全双工通讯的协议,允许服务端直接向浏览器发送消息,不再需要ajax轮询。它也是在TCP上面的实现,使用类似HTTP请求的握手协议,进行TCP通信切换。WebRTC,也叫Web即时通信(Web Real-Time Communication),则是一个支持浏览器之间进行实时语音对话或视频对话的API。它是建立于TCP/UDP之上的点对点通信,客户端基于上层RTCDataChannel进行通信而非底层TCP/UDP。

Google推出了基于UDP的QUIC协议(快速UDP网络连接)实现的HTTP传输,可以快速在两个端点间创建连接,支持多路复用连接。对于丢包问题,则采用冗余传输,能够根据接收到的包,对缺失的包进行恢复(类似RAID)。对于一个TCP连接,需要四个参数:源IP/端口,目的IP/端口,任何一个改变都需要重新建立连接。QUIC则不需要这些,因为UDP并不需要来源信;它使用UUID来标识一个连接,只要这个UUID在,那么这个连接会话就能够继续(与HTTP会话有点像)。因此在不同网络,特别是移动网络之间的切换,都将保持稳定(参考:Mosh)。目前Google的网站已经支持,并可以在Chrome浏览器的里面查看连接情况:chrome://net-internals/#quic。

通常说TCP是可靠连接,可用于数据流,复杂网络情况;UDP由于缺少确认机制等,属不可靠连接,通常应用于网络情况良好(内部)或者允许丢包的场景。事实上UDP也应用于游戏,VoIP等需要高效传输的场景。当网络状况糟糕时,使用TCP通信反而可能更差,因为需要更多步骤建立连接和确认。参考这里:QQ 为什么采用 UDP 协议,而不采用 TCP 协议实现?。TCP连接是有状态的,服务端需要去维持,及TIME_WAIT问题。如果连接一段时间没用,TCP并不能感知连接是否仍然有效,需要自定义心跳包,维持连接。UDP则是无连接状态的,只要知道目标IP及端口即可以发送。

UDP通信简单许多,不需要socket_connect和socket_connect,主动暴露一端(服务端)需要socket_bind。例如,服务端:

<?php
$socket = stream_socket_server("udp://127.0.0.1:1113", $errno, $errstr, STREAM_SERVER_BIND);
if (!$socket) {
    die("$errstr ($errno)");
}

do {
    $pkt = stream_socket_recvfrom($socket, 1024, 0, $peer);
    echo "$peer,$pkt\n";
    //客户端互相通信时,注释下面一行
    stream_socket_sendto($socket, date("D M j H:i:s Y\r\n"), 0, $peer);
} while ($pkt !== false);

客户端:

<?php
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

$msg = "Ping !";
$len = strlen($msg);

socket_sendto($sock, $msg, $len, 0, '127.0.0.1', 1113);

//sleep(30)
$res = socket_recv( $sock , $reply , 2045 , MSG_WAITALL);
echo $reply;

socket_close($sock);

服务端的代码里面有个stream_socket_sendto(或者socket_sendto),向客户端发送消息。UDP并不需要向客户端回复消息,这并像TCP在同一个连接里面回复,而是另一次UDP通信罢了。实际上,两个客户端之间就能够互相通信(分别使用不同端口),任何能够拿到对方IP和端口,都能够向其发送消息。复制客户端代码,更改目标IP和端口为服务端打印出来的信息,然后运行,原先的客户端也将收到消息。利用中间服务器交换两个客户端的IP和端口,以便直接通信,是NAT穿透常用的方法。

综上,使用TCP可以保证数据可靠传输,复用连接,比如请求-应答模型;使用UDP则可以更高效发送数据,但能容忍部分丢包的场景,比如视频画面;或者只发送不需响应的数据,比如日志;或者无需连接状态的场景(类似HTTP会话),减少服务器压力和客户端等待。当然,也可以在UDP基础上自定义传输规则,实现譬如Mosh或则TCP那样的应用。选择TCP或者UDP,应该综合考虑使用场景,网络,数据包大小,效率,安全等,而不是盲从字面上的“面向可靠的”或者“不可靠的”或者“高效的”。

参考链接:
如何理解HTTP协议的 “无连接,无状态” 特点?
Google’s QUIC protocol: moving the web from TCP to UDP(Google QUIC协议:从TCP到UDP的Web平台
如何看待谷歌 Google 打算用 QUIC 协议替代 TCP/UDP? – 回答作者: Trotyl Yu
How Mosh works
使用TCP协议的NAT穿透技术
PHP Socket通信
日志系统设计
可靠 UDP 传输
DDOS(拒绝服务攻击)
php使用socket感悟–tcp和udp
UDP socket programming in php
Does WebRTC use TCP or UDP?

日志系统设计

日志系统是项目开发/运维当中非常重要的一部分,提供产品使用情况,跟踪调试信息等。以前都是各个项目各自实现日志记录,比如PHP、JAVA各自实现一套,如果要跨项目/服务器进行查询/跟踪/统计,则比较麻烦,比如Web后台–>业务模块–>基础组件,客户端–>公共接口–>业务模块等等。
目前的项目都是将日志写入本地,首先需要定义统一的规范:

格式
DateTime | ServerIP | ClientIP | PID | RequestID|  Type | Level | Message|  Code

解释
DateTime:记录日志的当前时间戳
ServerIP:当前服务器IP
ClientIP:客户端IP
PID:进程ID
RequestID:交易号,即请求的唯一标识,使用uniqid(+UserCode)或UUID或,一次请求会有多条日志,,用于关联本次请求内的相关日志
Type:日志类型,比如统计,操作(审计),业务等
Level:日志等级
Message:日志内容,当为值为数组或者对象时转为JSON
Type为RUNTIME时,表示运行日志,属性:自定义
Type为HTTP时,表示来源请求,属性:Url,Method(Get/Post),Params,RemoteIP,UserAgent,ReferUrl[,Action,Method]
Type为REST时,表示外部调用,属性:Type(Http/Https),Url,Port,RequestParams,Response,RunTime;
Type为SQL时,表示SQL执行,属性:Sql,RunTime
Code:标识码,记录错误码、响应码、统计码、版本、自定义信息,方便统计

这里的RequestID由入口处自动产生,用于标识一次请求,关联所产生的所有日志,需要在各个项目之间传递。为了减少日志,Level通常为Info级别,避免产生过多日志。为了方便调试追踪,RequestID和Level也可以在其他参数中指明,比如HTTP头里面附加。
然后是日志收集:客户端收走日志系统,发送给日志系统服务端。
然后分析处理呈现:服务端将接收到的日志,发给处理其他组件分析处理,提供Web界面的查询系统。研发人员,可以错误信息,定位问题;获悉程序运行情况进行调优;大数据分析日志,得出产品使用情况;运维平台则可以进行业务报警。

日志产生由个语言依照规范自行实现,收集、保持则由FlumeKafka实现。Flume是一个分布式的日志收集、合并、移动系统,能够监控文件变化,将变化部分传输出去。Kafka是一个分布式的发布/订阅的消息流平台,包括Broker,Consumer,Producer都支持分布式,依赖Zookeeper实现。

在PHP上面的实现,一开始使用log4php,看起来很美好,但是性能很差,对业务影响较大。决定再次简化,砍掉不必要的东西(Socket,邮件等等),在C语言开发的PHP日志扩展SeasLog基础上在做开发,将日志文件保存在本地。为了减少日志所占用内存,每超过一定的大小的日志即进行保存,否则在最后进行保存,利用了Nginx的fastcgi_finish_request特性。生产上发现,每天产生的文件日志太大了,需要控制日志信息大小、等级,并及时清理。
对于Web后台,还结合FirePHP,将日志直接输出到浏览器,方便边运行变调试。

参考链接:
最佳日志实践
Optimal Logging
Flume+Kafka收集Docker容器内分布式日志应用实践
基于Flume的美团日志收集系统(一)架构和设计
Twitter是如何构建高性能分布式日志的
开源日志系统比较
Kafka剖析(一):Kafka背景及架构介绍
基于Flume的野狗实时日志系统的演进和优化
EVODelavega/phpkafka
有赞统一日志平台初探
RabbitMQ和kafka从几个角度简单的对比
Flume-ng的原理和使用
利用flume+kafka+storm+mysql构建大数据实时系统

HTTP文件分块上传下载

在开发中经常需要上传下载文件,涉及web页面,手机应用,线下服务器等,文件传输方式有HTTP,FTP,BT。。于是基于HTTP开发了统一文件上传下载接口。
由于上传涉及多端,网络状况复杂,需要能够支持断点续传,因此需要对文件进行分块和校验。
请求上传:

  • chunk:当前块编号
  • chunks:文件分块总数
  • md5:整个文件md5
  • content:文件块内容
  • size:文件块大小

服务端返回:

  • statu:状态,0失败,非0成功
  • chunk:需要上传的块编号
  • message:操作信息

服务端有几种情况

  • 1. 上传成功,md5已经存在,说明文件已存在,则返回上传成功,并结束上传,类似秒传
  • 2. 上传成功,md5已经不存在,但当前文件块已经存在,说明该文件已经上传过一部分,则返回成功,并给出需要开始上传的块号,类似断点续传
  • 3. 上传成功,文件块不存在,当前块号小于总块数减一,则返回上传成功,并给出下一块块号
  • 4. 上传成功,文件块不存在,当前块号等于总块数减一,进行合并,校验成功,则返回上传成功,并结束上传
  • 5. 上传成功,文件块不存在,当前块号等于总块数减一,进行合并,校验成功,则返回上传失败,从第一块重新开始上传
  • 6. 上传失败,服务端返回失败,则根据错误信息重传
  • 7. 上传失败,服务端没有返回,则重传当前块

客户端依据自定义的大小对文件进行切割,每次传递一个块,当服务端接收到当前块号为总块数减一则认为全部上传完毕,进行文件块合并,清除临时文件块,计算MD5,如果MD5与传递过来的相等则认为上传成功,否则失败,要求客户端从第一块重传。
这个是基于客户端顺序上传的,假如是并发上传呢?那么就需要在每个分块上传结束后触发合并,需要借助锁来管理。
又拍云表单分块上传则分为3步骤:

  • 1.初始化,上报文件信息
  • 2.上传分块
  • 3.上传结束,触发合并

百度的WebUploader则支持浏览器向服务端的断点续传,利用HTML5或Flash对文件进行分块,计算MD5。
HTTP分块下载,也就是断点续传下载,是根据HTTP1.1协议(RFC2616)中定义的HTTP头 Range和Content-Range字段来控制的:

  • 1. 客户端在HTTP请求头里面指明Range,即开始下载位置
  • 2. 服务端在HTTP响应头里面返回Content-Range,告知下载其实点和范围

服务端可以将文件MD5等加入Etag或自定义Header字段里面,HTTP客户端便可以分开请求数据,最后合并;否则永远都是从头开始。其实也可以把大文件切成多个小文件,再一个个下载回来合并。
有些web服务器直接支持文件上传和下载的断点续传,比如Nginx。

刚才的文件分块传输过程,有点像TCP通信,也有建立连接,分片,校验,重发,断开连接。
而这个将大任务分解为多个小任务进行处理的思想,即准备–>循环(并行)处理–>结束,可以应用到很多项目里面,比如大数据,爬虫。为Web管理系统写了jQuery插件用来处理的耗时任务,将一个耗时任务分解为多个循环请求,避免超时

  • 1. prepare:服务端返任务标示ID,总步骤,及附加参数
  • 2. process:根据返回参数进行循环请求
  • 3. complete:循环完成,触发结束

后面又延伸出另外一个插件,支持任意顺序步骤的处理。

参考链接:
聊聊大文件上传
md5 javascript
HTTP协议--断点续传
http断点续传的秘密
nginx-upload-module模块实现文件断点续传
通过curl测试服务器是否支持断点续传

jQuery 插件开发

最近开发一个前端图表模块,使用jQuery和Highchart开发,然后提供给其他人用,发现调用时要写太多代码,能不能一行搞定。之前开发过几个jQuery扩展,于是也做成jQuery扩展,调用时类似$(‘#chart’).warning(options)这样子,简洁了不少。
jQuery扩展有两种,一种是$(‘#element’).func(),使用了选择器,定义是:

(function($) {
    //Plugin fucntions
    $.fn.PluginName = function(options) {
        // Plugin initial
    }
})(jQuery);

将PluginName这个插件注册到$.fn上面,另外一种$.func(options),不绑定元素的,定义是:

(function($) {
    //Plugin fucntions
    $.PluginName = function(options) {
        // Plugin initial
    }
})(jQuery);

将PluginName直接挂在了$对象下面。
jQeury.extend方法可以用来扩展对象,也可以用来增加插件:

//$('#el').PluginName方式
jQuery.fn.extend({
    PluginName: function(options) {
        // Plugin initial
    }
});


//对原有的PluginName插件再进一步扩展,也可用于升级插件而不修改原有代码
jQuery.fn.PluginName.extend({
    doSomething: function(options) {
        // Plugin extend
    }
});

jQuery.extend(true, jQuery.fn.PluginName.prototype, {});

//$.PluginName方式
jQuery.extend({
    PluginName: function(options) {
        // Plugin initial
    }
});

jQuery.PluginName.extend({
    doSomething: function(options) {
        // Plugin extend
    }
});

以下是一个简单的jQuery插件模板,来自这里

/*
 *  Project: 
 *  Description: 
 *  Author: 
 *  License: 
 */

// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
;(function ( $, window, document, undefined ) {

    // undefined is used here as the undefined global variable in ECMAScript 3 is
    // mutable (ie. it can be changed by someone else). undefined isn't really being
    // passed in so we can ensure the value of it is truly undefined. In ES5, undefined
    // can no longer be modified.

    // window is passed through as local variable rather than global
    // as this (slightly) quickens the resolution process and can be more efficiently
    // minified (especially when both are regularly referenced in your plugin).

    // Create the defaults once
    var pluginName = 'defaultPluginName',
        defaults = {
            propertyName: "value"
        };

    // The actual plugin constructor
    function Plugin( element, options ) {
        this.element = element;

        // jQuery has an extend method which merges the contents of two or
        // more objects, storing the result in the first object. The first object
        // is generally empty as we don't want to alter the default options for
        // future instances of the plugin
        this.options = $.extend( {}, defaults, options) ;

        this._defaults = defaults;
        this._name = pluginName;

        this.init();
    }

    Plugin.prototype.init = function () {
        // Place initialization logic here
        // You already have access to the DOM element and the options via the instance,
        // e.g., this.element and this.options
    };

    // You don't need to change something below:
    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations and allowing any
    // public function (ie. a function whose name doesn't start
    // with an underscore) to be called via the jQuery plugin,
    // e.g. $(element).defaultPluginName('functionName', arg1, arg2)
    $.fn[pluginName] = function ( options ) {
        var args = arguments;

        // Is the first parameter an object (options), or was omitted,
        // instantiate a new instance of the plugin.
        if (options === undefined || typeof options === 'object') {
            return this.each(function () {

                // Only allow the plugin to be instantiated once,
                // so we check that the element has no plugin instantiation yet
                if (!$.data(this, 'plugin_' + pluginName)) {

                    // if it has no instance, create a new one,
                    // pass options to our plugin constructor,
                    // and store the plugin instance
                    // in the elements jQuery data object.
                    $.data(this, 'plugin_' + pluginName, new Plugin( this, options ));
                }
            });

        // If the first parameter is a string and it doesn't start
        // with an underscore or "contains" the `init`-function,
        // treat this as a call to a public method.
        } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {

            // Cache the method call
            // to make it possible
            // to return a value
            var returns;

            this.each(function () {
                var instance = $.data(this, 'plugin_' + pluginName);

                // Tests that there's already a plugin-instance
                // and checks that the requested public method exists
                if (instance instanceof Plugin && typeof instance[options] === 'function') {

                    // Call the method of our plugin instance,
                    // and pass it the supplied arguments.
                    returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) );
                }

                // Allow instances to be destroyed via the 'destroy' method
                if (options === 'destroy') {
                  $.data(this, 'plugin_' + pluginName, null);
                }
            });

            // If the earlier cached method
            // gives a value back return the value,
            // otherwise return this to preserve chainability.
            return returns !== undefined ? returns : this;
        }
    };

}(jQuery, window, document));

根据这个就可以方便的开发jQuery插件了。
也可以使用jQuery.widget来开发一个插件,参考这里

//新插件
$.widget( "custom.superDialog", {} );
//继承原有jQuery UI插件
$.widget( "custom.superDialog", $.ui.dialog, {} );

除此之外,也可以自己写插件/模块:

(function () {
	// 将插件/模块定义在闭包私有空间里,加载时候就会立即执行,初始化
	this.PluginName = function(options) {
	}
	PluginName.prototype.doSomething = function(options) {
	}
}());

然后就可以调用了

var plugin = new PluginName(options);
plugin.doSomething();

也可以这样子:

var MODULE = (function (my) {
	my.moduleMethod = function () {
		// method override, has access to old through old_moduleMethod...
	};
	return my;
}(MODULE || {}));

#子模块,挂在MODULE这个对象/命名空间下面
MODULE.sub = (function () {
	var my = {};
	// ...

	return my;
}());

模块多了就需要模块管理工具了,比如RequireJS

//定义
define('myModule', 
    ['foo', 'bar'], 
    // module definition function
    // dependencies (foo and bar) are mapped to function parameters
    function ( foo, bar ) {
        // return a value that defines the module export
        // (i.e the functionality we want to expose for consumption)
    
        // create your module here
        var myModule = {
            doStuff:function(){
                console.log('Yay! Stuff');
            }
        }
 
        return myModule;
});
#使用
require(['app/myModule'], 
    function( myModule ){
        // start the main module which in-turn
        // loads other modules
        var module = new myModule();
        module.doStuff();
});

Javascript模块话加载可以参考这里

参考链接:
JavaScript Module Pattern: In-Depth
Advanced Plugin Concepts
Learning JavaScript Design Patterns
jQuery Plugin Pattern
理解jquery的$.extend()、$.fn和$.fn.extend()
Making Use of jQuery UI’s Widget Factory
Building Your Own JavaScript Modal Plugin
Writing Modular JavaScript With AMD, CommonJS & ES Harmony
Javascript文件加载:LABjs和RequireJS
jQuery Boilerplate——流行的jQuery插件开发模板

在Raspberry Pi上面运行Docker

公司有个项目是运行在Raspberry Pi上面,这个项目涉及到Perl,Python,Java Applet,并且未来Pi的数量可能达到上千个。最近碰到了一样的代码遵循一样的部署脚本,在不同的树莓派上运行竟然到不同的结果。于是也想将Docker应用到树莓派上面去,使用docker主要优点:
1 消除环境差异,程序运行所依赖的环境已经一起打包,随身携带了
2 简化配置,只有docker相关配置,不再需要每个语言单独配置
3 重复利用
4 方便升级,之前有差异时,是重写镜像。。
5 方便管理,可以使用docker已有的工具来管理
6 性能接近原生应用

Raspberry Pi在jessie这个版本就已经支持了docker,检查版本:

$ lsb_release -da
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 8.0 (jessie)
Release:    8.0
Codename:  jessie

如果你的版本已经支持,直接执行以下命令就可以了:

curl -sSL https://get.docker.com | sh

如果你的版本不支持,你需要升级系统或重新刷镜像,可以采用hypriot

#下载
wget https://downloads.hypriot.com/hypriotos-rpi-v1.0.0.img.zip
#解压
unzip hypriotos-rpi-v1.0.0.img.zip
#查找 SD card ID
lsblk
#查找类似/dev/mmcblk0

#弹出SD卡
umount /run/media/mac/8734-1E4C
#将镜像写入 SD card
sudo dd if=hypriotos-rpi-v1.0.0.img of=/dev/mmcblk0 bs=1m

然后启动,默认的用户名/密码是pirate/hypriot。
当然也可以采用Debian或者是Alpine Linux定制(Raspberry Pi仅支持 arm版本的系统,并且只能在arm平台上构建,或者这个)。
运行测试一下:

docker run -d -p 80:80 hypriot/rpi-busybox-httpd

访问以下树莓派的IP,就会看到一个非常神器的网页,而这个容器仅仅几M。在这里可以看到基于hypriot的好几个镜像,也可以搜索其他的raspberry pi镜像。
现在我们使用hypriot/rpi-python来构建一个使用Python Selenium测试带有Java Applet的网页:

docker run -idt -P --name web hypriot/rpi-python
docker attach web

进入到容器里面,执行以下命令:

sudo apt-get update
#安装openjdk
sudo apt-get install openjdk-7-jdk
#安装Firefox
sudo apt-get install iceweasel
#安装Java Applet插件
sudo apt-get install icedtea-7-plugin
#安装虚拟桌面
sudo apt-get install xvfb
#安装Python包管理工具
sudo apt-get install python-pip
#安装selenium
sudo pip install selenium
#安装Python虚拟桌面库
sudo pip install pyvirtualdisplay

创建测试test.py:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
from pyvirtualdisplay import Display
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
display = Display(visible=0, size=(800, 600))
display.start()
caps = DesiredCapabilities.FIREFOX
caps["marionette"] = False
#caps["binary"] = "/usr/bin/firefox"
firefox_binary = FirefoxBinary("/usr/bin/firefox")
fp = webdriver.FirefoxProfile()
fp.set_preference("security.enable_java", True )
fp.set_preference("plugin.state.java", 2)
driver = webdriver.Firefox(firefox_profile=fp,firefox_binary=firefox_binary)
driver.maximize_window()
driver.get("http://businessconnect.telus.com/bandwidth/en_us/index.html?voip=on&voiplines=1&testlength=15&codec=g711&_=1470641590&startTest=on")
time.sleep(120)
mos = driver.find_element_by_class_name("sectiontitle18").find_element_by_tag_name("span").get_attribute("innerHTML")
print mos
driver.close()
display.stop()

运行测试用例:

python test.py

如果仅仅是这样子,是行不通的,Java Applet的安全询问弹窗超出了浏览器范围是点击不到的,需要更为强大的组件来做,比如。
这里采用取巧的方法,首先把pyvirtualdisplay相关代码注释掉,在界面上观察具体的执行效果,并点击安全询问,运行通过。然后取消注释,再次运行,就可以了。最后将运行通过的配置文件/root/.icedtea目录拷贝出来,放到其他的树莓派里面就可以了。
如果碰到报错”selenium.common.exceptions.WebDriverException: Can’t load the profile”,通常是说Firefox或selenium版本太低,如果检查都不是,需要修改WebDriver的超时时间

nano /usr/local/lib/python2.7/dist-packages/selenium/webdriver/firefox/webdriver.py

timeout从30秒改为120秒:

class WebDriver(RemoteWebDriver):


    # There is no native event support on Mac
    NATIVE_EVENTS_ALLOWED = sys.platform != "darwin"


    def __init__(self, firefox_profile=None, firefox_binary=None, timeout=120,

修改为容器后,提交变更:

root@black-pearl:/home/pirate# docker ps -l
CONTAINER ID        IMAGE                COMMAND            CREATED            STATUS              PORTS              NAMES
04762770df2d        hypriot/rpi-python  "bash"              2 days ago          Up 2 days                              web
root@black-pearl:/home/pirate# docker commit 04762770df2d hypriot/rpi-python
root@black-pearl:/home/pirate# docker rm web

运行新镜像

#docker run -idt -P --name web hypriot/rpi-python
#映射程序目录和配置文件进去
docker run -idt -P --name web -v /home/pi:/home/pi -v /home/pi/.icedtea:/root/.icedtea hypriot/rpi-python

程序正常运行后,可以将这个镜像打包:

docker save -o hypriot.tar hypriot/rpi-python:latest

分发给其他树莓派运行:

docker load --input hypriot.tar
docker images
docker run -idt -P --name web -v /home/pi:/home/pi -v /home/pi/.icedtea:/root/.icedtea hypriot/rpi-python

这样子便可以利用docker工具来管理这个应用程序,比起原本复杂的环境配置要简单很多。

在查找Pi可用镜像的时候,顺便发现了一个IOT(internet of things)管理平台:resin

参考链接:
add support to install Docker on raspbian/jessie
Getting started with Docker on your Raspberry Pi
How to get Docker running on your Raspberry Pi using Linux
Getting selenium python to work on Raspberry Pi Model B