作者归档:admin

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,

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

[email protected]:/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
[email protected]:/home/pirate# docker commit 04762770df2d hypriot/rpi-python
[email protected]:/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

使用Docker Compose管理Docker容器

上一篇创建了一个PHP的运行环境,现在需要一个MySQL服务,直接运行:

[email protected]:~# docker run --name rocket-mysql -v /home/rocketfish/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

就自动下载并得到一个mysql版本为5.7的服务镜像。
这时候,要运行一个Web服务我们需要运行两次的docker run才可以,如果还有更多的Web容器或其他的服务容器呢?
Docker官方提倡一个容器仅提供一个服务,多个服务/容器可以使用docker-compose来管理。
docker-compose本身是一个Python写的工具,可以直接通过pip安装:

[email protected]:~# sudo pip install --upgrade pip

如果你本地并没有Python环境,也可以采用docker-compose的docker镜像来运行:

[email protected]:/home/compose-web# curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
[email protected]:/home/compose-web# docker-compose --version
#开始下载镜像,建议用第一种

查看帮助

[email protected]:~# docker-compose -h
Define and run multi-container applications with Docker.

Usage:
  docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
  -f, --file FILE             Specify an alternate compose file (default: docker-compose.yml)
  -p, --project-name NAME     Specify an alternate project name (default: directory name)
  --verbose                   Show more output
  -v, --version               Print version and exit
  -H, --host HOST             Daemon socket to connect to

  --tls                       Use TLS; implied by --tlsverify
  --tlscacert CA_PATH         Trust certs signed only by this CA
  --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
  --tlskey TLS_KEY_PATH       Path to TLS key file
  --tlsverify                 Use TLS and verify the remote
  --skip-hostname-check       Don't check the daemon's hostname against the name specified
                              in the client certificate (for example if your docker host
                              is an IP address)

Commands:
  build              Build or rebuild services
  bundle             Generate a Docker bundle from the Compose file
  config             Validate and view the compose file
  create             Create services
  down               Stop and remove containers, networks, images, and volumes
  events             Receive real time events from containers
  exec               Execute a command in a running container
  help               Get help on a command
  kill               Kill containers
  logs               View output from containers
  pause              Pause services
  port               Print the public port for a port binding
  ps                 List containers
  pull               Pulls service images
  push               Push service images
  restart            Restart services
  rm                 Remove stopped containers
  run                Run a one-off command
  scale              Set number of containers for a service
  start              Start services
  stop               Stop services
  unpause            Unpause services
  up                 Create and start containers
  version            Show the Docker-Compose version information

这些命令包括了创建/启动/停止/暂停/继续运行容器。
首先要创建docker-compose.yml,这是一个YAML格式的文档

mkdir docker
cd docker
mkdir web
mkdir db
vim docker-compose.yml

内容如下:

version: '2'
services:
  db:
    image: mysql:5.7
    ports:
    - "3306:3306"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: web
      MYSQL_USER: root
      MYSQL_PASSWORD: rootweb
    volumes:
    - ./db/data:/var/lib/mysql
  web:
    depends_on:
      - db
    image: nginx-php-fpm:phalcon
    ports:
    - "80:80"
    restart: always
    environment:
      WEB_DB_HOST: db:3306
      WEB_DB_PASSWORD: root
    volumes:
    - ./web/html:/var/www/html/
    links:
    - db

这里的environment定义了环境变量,比如host的MAC,就可以在docker容器的系统变量里面得到,各个编程语言都有获取系统环境变量的方法。这些变量也可以组织起来放在文件里面加载进去,参考这里
这里的volumes定义了要映射进容器的文件或文件夹或者数据容器,可以参考这里。注意,多个容器共享同一个目录,会出现写冲突,比如MySQL,多个实例,数据目录需要分开。所以设计好你的程序,哪一部分是需要只读的(可以共享),哪一部分是需要写的;写的部分是否可以放到各自临时目录,或者其他公共服务里面。
运行并查看

[email protected]:/home/compose-web# docker-compose up -d
Creating network "composeweb_default" with the default driver
Creating composeweb_db_1
Creating composeweb_web_1
[email protected]:/home/compose-web# docker-compose ps
      Name                   Command             State              Ports            
------------------------------------------------------------------------------------
composeweb_db_1    docker-entrypoint.sh mysqld   Up      0.0.0.0:3306->3306/tcp      
composeweb_web_1   /start.sh                     Up      443/tcp, 0.0.0.0:80->80/tcp 
#也可以使用原来的命令
[email protected]:/home/compose-web# docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                         NAMES
efbdaf257748        nginx-php-fpm:phalcon   "/start.sh"              13 seconds ago      Up 11 seconds       0.0.0.0:80->80/tcp, 443/tcp   composeweb_web_1
a6935d20911e        mysql:5.7               "docker-entrypoint.sh"   14 seconds ago      Up 13 seconds       0.0.0.0:3306->3306/tcp        composeweb_db_1

docker-compose up -d ,将会在后台启动并运行所有的容器。
docker-compose仅仅提供了对多个docker服务的管理,仍然可以在这些容器上运行原来的docker命令。检查下web容器的IP:

[email protected]:/home/compose-web# docker inspect composeweb_web_1 | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "",
                    "IPAddress": "172.18.0.3",

然后访问:http://172.18.0.3/ 就可以看到web页面了。
如果想要停止服务,可以用docker-compose stop:

[email protected]:/home/compose-web# docker-compose stop db
Stopping composeweb_db_1 ... done
[email protected]:/home/compose-web# docker-compose stop web
Stopping composeweb_web_1 ... done

再次启动,得到相同名称的服务:

[email protected]:/home/compose-web# docker-compose up -d
Starting composeweb_db_1
Starting composeweb_web_1

各个容器的名称也可以在配置文件里面通过参数container_name来指定。
docker-compose的配置还有很多其他的参数,可以参考这里。比如刚才我们通过docker inspect来查找容器IP,也可以配置成静态IP:

version: '2'

services:
  app:
    image: nginx-php-fpm:phalcon
    networks:
      app_net:
        ipv4_address: 172.18.0.10
        ipv6_address: 2001:3984:3989::10

networks:
  app_net:
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "true"
    ipam:
      driver: default
      config:
      - subnet: 172.18.0.0/24
        gateway: 172.18.0.1
      - subnet: 2001:3984:3989::/64
        gateway: 2001:3984:3989::1

也可以自定义网络
刚才我们的配置文件里面直接指定了镜像,也可以指定Dockerfile镜像构建,假如Dockerfile在web目录下面:

build:
  context: ./web

或者直接指明Dockerfile:

build:
  context: .
  dockerfile: Dockerfile-alternate

实际上,docker-compose的配置已经覆盖了Dockerfile的配置项,也可以直接用于构建多个docker服务,比如指定运行的命令

command: [/bin/bash]

这里的入口程序,如果不能持续运行的话,运行完成后,docker就会退出。所以如果你需要可以后台运行的docker容器,那么入口程序,就必须保持一个程序在前台运行,不退出,比如运行crontab的镜像,入口程序可以是:

cron && bash

cron这个程序是在后台运行的,如果不配合前台程序bash,容器执行玩cron就会退出了。其他运行crontab的容器,大都是强制一个程序在前台不退出,比如这个

cron && tail -f /var/log/cron.log

这里需要注意一下,通过docker设置的environment values在容器内的cli下面执行时取不到的,比如printenv,使用cron后台运行时取不到外部设置的变量,但是手动执行时又可以。这些外部环境变量只能在ENTRYPOINT或者CMD所执行的shell里面去设置,比如在CMD执行start.sh:

printenv | grep -E "^HOST" > /root/env.conf && cron && bash

Docker compose还可以跟Docker Swarm结合。

参考链接:
Install Docker Compose
Quickstart: Docker Compose and WordPress
YAML 模板文件
Introduction to Docker Compose Tool for Multi-Container Applications
Dockerfile基本结构
How to Get Environment Variables Passed Through docker-compose to the Containers
Access environment variables from crontab into a docker container
How can I access Docker set Environment Variables From a Cron Job
Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?
What is the difference between CMD and ENTRYPOINT in a Dockerfile?
Docker difference between run, cmd, entrypoint commands

基于Docker的Nginx + PHP-FPM + Phalcon镜像

上一篇简单介绍了Docker的安装,运行,这一篇来构建一个基于Nginx和PHP-FPM的Phalcon镜像。在官方找了以下,单独的Nginx和PHP镜像更加流行,混合的反倒不是很受欢迎。其实官方并不提倡在一个容器里面运行多个服务,最好是一个容器只对外提供一个服务:一个容器启动时仅仅运行一个命令(其实里面可以包含多个),也方便部署扩展升级。多个服务之间可以使用Docker Compose来管理。但是Docker并不阻止创建包含多个服务器的镜像,为了方便,所以我们仍然可以自己构建。
构建镜像可以有好几种方式,比如基于Alpine Linuxphusion/baseimage-docker构建,或者基于Ubuntu,CentOS等构建,又或者在PHP,Nginx的基础镜像上构建。注意:如果要采用Ubuntu或者CentOS构建,可能需要一些额外的工作,以便保持镜像轻量稳定运行。
这里采用已有的richarvey/nginx-php-fpm来构建,它是一个基于Nginx官方镜像来构建的。
Github上拉取相关文件从Dockerfile构建:

$ sudo git clone https://github.com/ngineered/nginx-php-fpm
$ sudo docker build -t nginx-php-fpm:latest .

关于Dockerfile的相关解释,可以参考这里。当然也可以直接拉取镜像使用

$ sudo docker pull richarvey/nginx-php-fpm
# 也可以直接运行,会自动拉取
#$ sudo docker run -d richarvey/nginx-php-fpm

查看本地的镜像,连单独的nginx也来了:

[email protected]:~# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx-php-fpm       latest              4fc9ac9f2945        7 hours ago         228.5 MB
nginx               mainline-alpine     00bc1e841a8f        5 days ago          54.21 MB

这里的mainline-alpine是指基于Alpine Linux构建的。Alpine Linux是一个仅有5M大小的linux系统,采用apk add/search来安装/查找相应软件,有许多镜像都是基于它构建的,官方PHP镜像也有基于它构建的Docker镜像。
然后运行nginx-php-fpm:

[email protected]:~# docker run --name web -d richarvey/nginx-php-fpm

docker inspect命令用来查看容器的相关信息,查看下分配的IP:

[email protected]:~# docker inspect web | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",

然后在浏览器里面访问:http://172.17.0.2/就可以看到phpinfo的页面。到这里,Nginx + PHP的web容器就已经运行起来了,对应的Nginx和PHP进程可以在宿主机器上直接查看:

[email protected]:~# ps aux | grep nginx
root     18167  0.0  0.0  13696  4300 pts/6    S    01:47   0:00 nginx: master process /usr/sbin/nginx
systemd+ 18168  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
systemd+ 18169  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
systemd+ 18170  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
systemd+ 18171  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
systemd+ 18172  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
root     18190  0.0  0.0  21292  1012 pts/18   S+   01:47   0:00 grep --color=auto nginx
[email protected]:~# ps aux | grep php-fpm
root     18166  0.0  0.2 167880 23364 pts/6    S    01:47   0:00 php-fpm: master process (/etc/php5/php-fpm.conf)
systemd+ 18173  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
systemd+ 18174  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
systemd+ 18175  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
root     18192  0.0  0.0  21292  1032 pts/18   S+   01:47   0:00 grep --color=auto php-fpm

接下来要为这个容器添加Phalcon扩展。首先要进入容器里面,使用docker attach命令进入:

[email protected]:~# docker attach web



结果在这里等了半天进不去。。。。查看下当前镜像入口程序:

[email protected]:~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
94176348a939        nginx-php-fpm       "/start.sh"         6 seconds ago       Up 5 seconds        80/tcp, 443/tcp     web

这个容器启动的时候运行的是start.sh这个脚本,这个脚本运行了Supervisor工具。于是重新启动容器,运行/bin/bash

#终止容器运行
[email protected]:~# docker stop web
web
#删除容器
[email protected]:~# docker rm web
web
#重新运行
[email protected]:~# docker run --name web -d -t -i nginx-php-fpm /bin/bash
ea21e10df702644a83ed75930b30c7764a786c4feabdf17cd868f86640137c47
[email protected]:~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
ea21e10df702        nginx-php-fpm       "/bin/bash"         6 seconds ago       Up 5 seconds        80/tcp, 443/tcp     web
[email protected]d:~# docker attach web
#进来了
bash-4.3# ls
bin       etc       lib       media     proc      run       srv       sys       usr
dev       home      linuxrc   mnt       root      sbin      start.sh  tmp       var

就可以进去了。
先安装编译相关工具包:

bash-4.3# apk --no-cache add php5-dev
bash-4.3# apk --no-cache add gcc
bash-4.3# apk --no-cache add make
bash-4.3# apk --no-cache add autoconf
bash-4.3# apk --no-cache add libc-dev
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
(1/2) Installing musl-dev (1.1.14-r12)
(2/2) Installing libc-dev (0.7-r0)
OK: 334 MiB in 106 packages

编译安装Phalcon:

bash-4.3# cd /home
bash-4.3# git clone --depth=1 git://github.com/phalcon/cphalcon.git
bash-4.3# cd cphalcon/build
bash-4.3# ./install
bash-4.3# ls -la /usr/lib/php5/modules/ | grep phalcon
-rwxr-xr-x    1 root     root       5045264 Sep 28 17:34 phalcon.so

更改PHP扩展的配置:

bash-4.3# cd /etc/php5/conf.d/
bash-4.3# vi phalcon.ini
#添加以下内容
#extension=phalcon.so

#检查扩展是否加载成功
bash-4.3# php -i | grep phalcon
/etc/php5/conf.d/phalcon.ini,
phalcon
phalcon => enabled
phalcon.db.escape_identifiers => On => On
phalcon.db.force_casting => Off => Off
phalcon.orm.cast_on_hydrate => Off => Off
phalcon.orm.column_renaming => On => On
phalcon.orm.enable_implicit_joins => On => On
phalcon.orm.enable_literals => On => On
phalcon.orm.events => On => On
phalcon.orm.exception_on_failed_save => Off => Off
phalcon.orm.ignore_unknown_columns => Off => Off
phalcon.orm.late_state_binding => Off => Off
phalcon.orm.not_null_validations => On => On
phalcon.orm.virtual_foreign_keys => On => On
OLDPWD => /home/cphalcon/build
_SERVER["OLDPWD"] => /home/cphalcon/build
_ENV["OLDPWD"] => /home/cphalcon/build

加载成功了,需要保持本次镜像变更。首先退出容器:

bash-4.3# cd /home
#删除各种不必要的东西,比如gcc
bash-4.3# rm -rf cphalcon/
bash-4.3# exit
exit

然后查看版本并提交变更:

[email protected]:~# docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
ea21e10df702        nginx-php-fpm       "/bin/bash"         31 minutes ago      Exited (0) 6 seconds ago                       web
[email protected]:~# docker commit ea2 nginx-php-fpm:phalcon
sha256:bb388df328ecc33fac02dba69759d5c992a145f650a0e5b20ca29a4b122fa933

docker commit命令可以用来提交变更,ea2是container id的前三位,也可以写全;然后跟的是要提交的镜像。这里提交到phalcon这个标签下,以便与原来的区分开。查看所有镜像,发现有两个不同的标签:

[email protected]:~# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx-php-fpm       phalcon             bb388df328ec        11 seconds ago      364.4 MB
nginx-php-fpm       latest              4fc9ac9f2945        4 hours ago         228.5 MB

采用新镜像来运行,这里要讲程序运行入口改回/start.sh,以便能正常启动Nginx和PHP-FPM:

[email protected]:~# docker rm web
web
[email protected]:~# docker run --name web -d -t -i nginx-php-fpm:phalcon /start.sh
deecb19467cda2676b24248e3f55970a2481255c6022a80ffbf5087792ccb559
[email protected]:~# docker ps
CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS               NAMES
deecb19467cd        nginx-php-fpm:phalcon   "/start.sh"         4 seconds ago       Up 3 seconds        80/tcp, 443/tcp     web

入口程序改变了,需要再提交一次变更:

[email protected]:~# docker stop web
web
[email protected]:~# docker ps -l
CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS                       PORTS               NAMES
c7600e62733d        nginx-php-fpm:phalcon   "/start.sh"         34 seconds ago      Exited (137) 8 seconds ago                       web
[email protected]:~# docker commit c76 nginx-php-fpm:phalcon
sha256:1c97ee169a551dd8441f42b40beafd102c71f3e887e2317dc11ce0ef136ceaf0

运行最终的镜像:

[email protected]:~# docker rm web
web
[email protected]:~# docker run --name web -d -t -i nginx-php-fpm:phalcon
cb5b0c9e55913a538539e46c53ac7905b21def84a05eb00ef81c4b500853576c
[email protected]:~# docker ps
CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS               NAMES
cb5b0c9e5591        nginx-php-fpm:phalcon   "/start.sh"         4 seconds ago       Up 3 seconds        80/tcp, 443/tcp     web

访问http://172.17.0.2/,便可以在页面找到phalcon扩展。
通常我们会将程序和数据分开,挂载外部文件目录到容器里面去:

[email protected]:~# docker stop web
web
[email protected]:~# docker rm web
web
[email protected]:~# docker run --name web -d -t -i -v /home/docker/nginx-php-fpm/src:/var/www/html/ nginx-php-fpm:phalcon
ffd64793fe8e7a2a95b68f514e221b7ec3b6cadfe668c016f55a7bb6d48bc702

-v参数可以用来挂载目录或者文件,可以又多个-v参数。
刚才容器里面做的那些已经添加到Dockerfile里面去,你直接使用它来构建。
至此Nginx + PHP-FPM + Phalcon镜像构建完成,介绍绍了如何进入容器,提交变更,网络访问和文件挂载。

参考链接:
A minimal Ubuntu base image modified for Docker-friendliness
eboraas/phalcon
基于Docker的PHP开发环境
Docker for PHP Developers
Docker在PHP项目开发环境中的应用
使用 Supervisor 来管理进程
PHP C扩展框架Phalcon
Alpine Linux,一个只有5M的Docker镜像