月度归档:2016年04月

PHP Laravel Excel

PHP Laravel Excel官方网址:http://www.maatwebsite.nl/laravel-excel/docs;

GitHub: https://github.com/Maatwebsite/Laravel-Excel;
Packagist: https://packagist.org/packages/maatwebsite/excel

这个程序包依赖PHPOffice/PHPExcel,实际就是在这个工具包上做了一些易于用户操作的封装。需要说明的是,目录依赖的PHPOffice/PHPExcel版本是1.8.*(也是当前的稳定版),在1.9.*和2.0.*中开始引入PHP的命名空间,意味着PHP版本至少要5.3以上,这两个分支还在开发中,看起来这个包的作者非常的保守。

安装就按照Laravel套路来就好:

#往composer.json中添加"maatwebsite/excel": "~2.1.0",然后update

#添加ServiceProvider
vi config/app.php
'Maatwebsite\Excel\ExcelServiceProvider',

#添加Facade(可选)
'Excel' => 'Maatwebsite\Excel\Facades\Excel',

#配置(会添加excel.php配置文件)
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"

#获取excel实例
$excel = App::make('excel');

Maatwebsite/excel本身有一个默认的配置文件,如果应用级别有配置文件,那么应用配置将覆盖默认配置配置,具体实现是在ExcelServiceProvider中:

    public function boot()
    {
        $this->publishes([
            __DIR__ . '/../../config/excel.php' => config_path('excel.php'),
        ]);

        $this->mergeConfigFrom(
            __DIR__ . '/../../config/excel.php', 'excel'
        );

        //Set the autosizing settings
        $this->setAutoSizingSettings();
    }

表格操作,主要涉及输入和输出。

Excel::load('file.xls', function($reader) {

    // Getting all results
    $results = $reader->get();

    // ->all() is a wrapper for ->get() and will work the same
    $results = $reader->all();

});

使用get和all方法获取结果,默认,如果表格只有一个sheet,那么直接返回行集合,如果有多个sheet,那么返回sheet的集合,每个sheet又是行的集合。为了统一操作,可以设置配置文件中的force_sheets_collection设置为true,这样都会返回sheet的集合。

表格sheet的第一行是头部,默认会被转换成slug(别名),可以设置import.heading为false表示不使用文件头,可用值true|false|slugged|slugged_with_count|ascii|numeric|hashed|trans|original,设置为original表示使用字面值作为key,这个比较常见。

Sheet/行/单元格都是集合,使用了get()之后,就可以使用集合的方法。

#
$reader->get()->groupBy('firstname');

#依赖force_sheets_collection,可能返回第一个sheet或第一个行
$reader->first();
// Get workbook title
$workbookTitle = $reader->getTitle();

foreach($reader as $sheet)
{
    // get sheet title
    $sheetTitle = $sheet->getTitle();
}

// You can either use ->take()
$reader->take(10);

// Or ->limit()
$reader->limit(10);

// Skip 10 results
$reader->skip(10);

// Skip 10 results with limit, but return all other rows
$reader->limit(false, 10);

// Skip and take
$reader->skip(10)->take(10);

// Limit with skip and take
$reader->($skip, $take);

$reader->toArray();

$reader->toObject();

// Dump the results
$reader->dump();

// Dump results and die
$reader->dd();

#也可以使用foreach
// Loop through all sheets
$reader->each(function($sheet) {

    // Loop through all rows
    $sheet->each(function($row) {

    });

});

选择Sheet和列

#仅加载sheet1
Excel::selectSheets('sheet1')->load("xx.xls", function($reader) {});
Excel::selectSheets('sheet1', 'sheet2')->load();

#通过下标选择比较靠谱
// First sheet
Excel::selectSheetsByIndex(0)->load();

// First and second sheet
Excel::selectSheetsByIndex(0, 1)->load();

#选择列,很熟悉的用法?
#All get methods (like all(), first(), dump(), toArray(), ...) accept an array of columns.
// Select
$reader->select(array('firstname', 'lastname'))->get();

// Or
$reader->get(array('firstname', 'lastname'));

日期:
By default the dates will be parsed as a Carbon object.

分批导入:

Excel::filter('chunk')->load('file.csv')->chunk(250, function($results)
{
        foreach($results as $row)
        {
            // do stuff
        }
});

每次读入250行,处理完毕在导入250行??

批量导入:

Excel::batch('app/storage/uploads', function($rows, $file) {

    // Explain the reader how it should interpret each row,
    // for every file inside the batch
    $rows->each(function($row) {

        // Example: dump the firstname
        dd($row->firstname);

    });

});

$files = array(
    'file1.xls',
    'file2.xls'
);

Excel::batch($files, function($rows, $file) {

});

Excel::batch('app/storage/uploads', function($sheets, $file) {

    $sheets->each(function($sheet) {

    });

});

导出也有很多定制化的操作,参考:http://www.maatwebsite.nl/laravel-excel/docs/export

例子:

// 关联数组,输出表头
$excel_array = [
    [
        "表头1" => "xxxx",
        "表头2" => "yyyy"
    ],
    [
        "表头1" => "xxxx2",
        "表头2" => "yyyy3"
    ]
];
// 直接数据输入
$excel_array2 = [
    [
        "表头1", "表头2"
    ],
    [
        "xxxx", "yyyy"
    ],
    [
        "xxxx2", "yyyy3"
    ]
];

        \Excel::create("test1", function ($excel) use($excel_array) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array) {
                $sheet->fromArray($excel_array);
            });
        })->store("xls","d:/");
        
        \Excel::create("test2", function ($excel) use($excel_array2) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array2) {
                $sheet->fromArray($excel_array2, null, 'A1', false, false);
            });
        })->save("xls");

默认,如果不指定导入的路径,会保存到storage_path(‘exports’),即app/storage/exports。可以修改配置文件export.store.path的值。

导出的sheet,默认第一行总是头部,这个可以修改配置文件的export.generate_heading_by_indices为false取消这个默认值。也可以指定fromArray的第5参数为false达到同样目的。

store()的第一参数是导入文件的类型,第二参数是路径(不需要包含文件名),第三参数控制是否返回保存文件的信息(比如保存的路径,扩展名等)。

Laravel HTML转换PDF

首先,HTML转换成PDF使用一个叫wkhtmltopdf的工具,地址为:http://wkhtmltopdf.org/index.html,安装之后会提供一个命令行工具,这个命令行工具可配置的参数非常多:

wkhtmltopdf --page-width 100 --page-height 100 http://blog.ifeeline.com i.pdf
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done

在Linux下安装,可能会缺少中文字体,最简单的方式是在Windows下拷贝字体上传到Linux。Windows下字体位置:控制面板\所有控制面板项\字体,拷贝:
window_fonts
然后上传到Linux:

mkdir zh_cn
cd zh_cn
#安装字体
fc-cache -fv
#查看字体是否安装
fc-list | grep simsun

为了可以在PHP中使用这个命令行工具,有一个PHP包对其进行了封装,地址:https://github.com/KnpLabs/snappy:

require __DIR__ . '/vendor/autoload.php';

use Knp\Snappy\Pdf;

$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');

// or you can do it in two steps
$snappy = new Pdf();
$snappy->setBinary('/usr/local/bin/wkhtmltopdf');

// Display the resulting pdf in the browser
// by setting the Content-type header to pdf
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="file.pdf"');
echo $snappy->getOutput('http://www.github.com');

// Merge multiple urls into one pdf
// by sending an array of urls to getOutput()
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="file.pdf"');
echo $snappy->getOutput(array('http://www.github.com','http://www.knplabs.com','http://www.php.net'));

// .. or simply save the PDF to a file
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
$snappy->generateFromHtml('<h1>Bill</h1><p>You owe me money, dude.</p>', '/tmp/bill-123.pdf');

// Pass options to snappy
// Type wkhtmltopdf -H to see the list of options
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
$snappy->setOption('disable-javascript', true);
$snappy->setOption('no-background', true);
$snappy->setOption('allow', array('/path1', '/path2'));
$snappy->setOption('cookie', array('key' => 'value', 'key2' => 'value2'));
$snappy->setOption('cover', 'pathToCover.html');
// .. or pass a cover as html
$snappy->setOption('cover', '<h1>Bill cover</h1>');
$snappy->setOption('toc', true);
$snappy->setOption('cache-dir', '/path/to/cache/dir');

从例子可以看到,这个snappy工具包,主要实现了把wkhtmltopdf参数传递wkhtmltopdf命令行工具,然后调用命令行工具产生PDF而已。

每次调用这个工具都要设置一版数据显然很麻烦,所以为了在Laravel框架中有效的使用,就需要把这个工具包继续做一次封装基础到框架,可以使用laravel-snappy工具,地址:https://github.com/barryvdh/laravel-snappy。

Laravel集成第三方工具,基本套路是:
1 添加配置文件
2 添加ServiceProvider
3 添加Facade
ServiceProvider一般会读取配置文件中的配置(如果需要配置),然后把实例对象注入Laravel容器,接着就可以直接从容器中获取实例对象进行操作,也可以直接使用Facade,它实际也是从容器中获取实例对象。

以下是操作过程:

composer require barryvdh/laravel-snappy

#添加ServiceProvider
vi app/config/app.php
Barryvdh\Snappy\ServiceProvider::class,

#添加Facade
'PDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Image' => Barryvdh\Snappy\Facades\SnappyImage::class,

#添加配置(可以直接拷贝)
php artisan vendor:publish

#使用
$snappy = App::make('snappy.pdf');
//To file
$snappy->generateFromHtml('<h1>Bill</h1><p>You owe me money, dude.</p>', '/tmp/bill-123.pdf');
$snappy->generate('http://www.github.com', '/tmp/github.pdf'));
//Or output:
return new Response(
    $snappy->getOutputFromHtml($html),
    200,
    array(
        'Content-Type'          => 'application/pdf',
        'Content-Disposition'   => 'attachment; filename="file.pdf"'
    )
);

$pdf = App::make('snappy.pdf.wrapper');
$pdf->loadHTML('<h1>Test</h1>');
return $pdf->inline();

// Facade操作
$pdf = PDF::loadView('pdf.invoice', $data);
return $pdf->download('invoice.pdf');

return PDF::loadFile('http://www.github.com')->inline('github.pdf');

PDF::loadHTML($html)->setPaper('a4')->setOrientation('landscape')->setOption('margin-bottom', 0)->save('myfile.pdf')

Facade的操作方式还是很便利的。比如PDF::loadFile(‘http://www.github.com’),这样直接抓取一个网页,可以调用inline或stream方法直接输出到浏览器(实际是output方法的封装,output方法返回pdf的字符流,而output又是调用snappy提供的方法),也可以调用download方法下载,调用save方法保存。

这里需要特别指出的是,可以使用loadView方法直接load一个Blade模板文件,第二参数是模板使用到的变量,这对于动态产生PDF输出提供了一个绝佳实现。

最后,看下配置文件:

<?php
return array(
    'pdf' => array(
        'enabled' => true,
        'binary'  => '/usr/local/bin/wkhtmltopdf',
        'timeout' => false,
        'options' => array(),
        'env'     => array(),
    ),
    'image' => array(
        'enabled' => true,
        'binary'  => '/usr/local/bin/wkhtmltoimage',
        'timeout' => false,
        'options' => array(),
        'env'     => array(),
    ),
);

这里的enabled表示对应命令是否启用,binary表示wkhtmltopdf命令的位置,timeout表示是否设置超时,options就是要传递到wkhtmltopdf命令行的参数,env是执行命令行的环境变量。options的可用值,就是wkhtmltopdf命令的可用值,这个可以查看Knp\Snappy\Pdf的configure方法得到一个列表:

 protected function configure()
    {
        $this->addOptions(array(
            'ignore-load-errors'           => null, // old v0.9
            'lowquality'                   => true,
            'collate'                      => null,
            'no-collate'                   => null,
            'cookie-jar'                   => null,
            'copies'                       => null,
            'dpi'                          => null,
            'extended-help'                => null,
            'grayscale'                    => null,
            'help'                         => null,
            'htmldoc'                      => null,
            'image-dpi'                    => null,
            'image-quality'                => null,
            'manpage'                      => null,
            'margin-bottom'                => null,
            'margin-left'                  => null,
            'margin-right'                 => null,
            'margin-top'                   => null,
            'orientation'                  => null,
            'output-format'                => null,
            'page-height'                  => null,
            'page-size'                    => null,
            'page-width'                   => null,
            'no-pdf-compression'           => null,
            'quiet'                        => null,
            'read-args-from-stdin'         => null,
            'title'                        => null,
            'use-xserver'                  => null,
            'version'                      => null,
            'dump-default-toc-xsl'         => null,
            'dump-outline'                 => null,
            'outline'                      => null,
            'no-outline'                   => null,
            'outline-depth'                => null,
            'allow'                        => null,
            'background'                   => null,
            'no-background'                => null,
            'checkbox-checked-svg'         => null,
            'checkbox-svg'                 => null,
            'cookie'                       => null,
            'custom-header'                => null,
            'custom-header-propagation'    => null,
            'no-custom-header-propagation' => null,
            'debug-javascript'             => null,
            'no-debug-javascript'          => null,
            'default-header'               => null,
            'encoding'                     => null,
            'disable-external-links'       => null,
            'enable-external-links'        => null,
            'disable-forms'                => null,
            'enable-forms'                 => null,
            'images'                       => null,
            'no-images'                    => null,
            'disable-internal-links'       => null,
            'enable-internal-links'        => null,
            'disable-javascript'           => null,
            'enable-javascript'            => null,
            'javascript-delay'             => null,
            'load-error-handling'          => null,
            'load-media-error-handling'    => null,
            'disable-local-file-access'    => null,
            'enable-local-file-access'     => null,
            'minimum-font-size'            => null,
            'exclude-from-outline'         => null,
            'include-in-outline'           => null,
            'page-offset'                  => null,
            'password'                     => null,
            'disable-plugins'              => null,
            'enable-plugins'               => null,
            'post'                         => null,
            'post-file'                    => null,
            'print-media-type'             => null,
            'no-print-media-type'          => null,
            'proxy'                        => null,
            'radiobutton-checked-svg'      => null,
            'radiobutton-svg'              => null,
            'run-script'                   => null,
            'disable-smart-shrinking'      => null,
            'enable-smart-shrinking'       => null,
            'stop-slow-scripts'            => null,
            'no-stop-slow-scripts'         => null,
            'disable-toc-back-links'       => null,
            'enable-toc-back-links'        => null,
            'user-style-sheet'             => null,
            'username'                     => null,
            'window-status'                => null,
            'zoom'                         => null,
            'footer-center'                => null,
            'footer-font-name'             => null,
            'footer-font-size'             => null,
            'footer-html'                  => null,
            'footer-left'                  => null,
            'footer-line'                  => null,
            'no-footer-line'               => null,
            'footer-right'                 => null,
            'footer-spacing'               => null,
            'header-center'                => null,
            'header-font-name'             => null,
            'header-font-size'             => null,
            'header-html'                  => null,
            'header-left'                  => null,
            'header-line'                  => null,
            'no-header-line'               => null,
            'header-right'                 => null,
            'header-spacing'               => null,
            'replace'                      => null,
            'disable-dotted-lines'         => null,
            'cover'                        => null,
            'toc'                          => null,
            'toc-depth'                    => null,
            'toc-font-name'                => null,
            'toc-l1-font-size'             => null,
            'toc-header-text'              => null,
            'toc-header-font-name'         => null,
            'toc-header-font-size'         => null,
            'toc-level-indentation'        => null,
            'disable-toc-links'            => null,
            'toc-text-size-shrink'         => null,
            'xsl-style-sheet'              => null,
            'viewport-size'                => null,
            'redirect-delay'               => null, // old v0.9
            'cache-dir'                    => null,
        ));
    }

大概浏览一下,可以传递cookie,cookie-jar,这个在抓取一个需要验证登录的网络时就非常有意义。可以设置外边距,可以设置页大小,也可以指定页的宽度和高度,可以传递用户名密码,可以POST数据等等,实际就是一个脚本解析器和html渲染工工具(可以看成一个浏览器),只有像浏览器一样渲染html和执行js,才能获取到渲染结果(我们看到的模样),然后才能按照这个模样转换成PDF(或者图片)。

关于打印尺寸,参考:http://doc.qt.io/qt-4.8/qprinter.html#PaperSize-enum

————————————————————————————-
以上内容是PHP中调用命令行工具的封装。也可以直接使用对应的PHP扩展,地址:https://github.com/mreiferson/php-wkhtmltox,例子:

foreach (range(1, 4) as $i) {
    wkhtmltox_convert('pdf', 
        array('out' => '/tmp/test'.$i.'.pdf', 'imageQuality' => '95'), // global settings
        array(
            array('page' => 'http://www.visionaryrenesis.com/'),
            array('page' => 'http://www.google.com/')
            )); // object settings
}

第二参数是一个配置数组,比如imageQuality,对应的命令行参数是image-quality,详细的配置参考:http://wkhtmltopdf.org/libwkhtmltox/pagesettings.html, 可以把配置写入文件,然后直接代入。

#安装wkhtmltox
wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-centos7-amd64.rpm

yum install xorg-x11-fonts-75dpi.noarch
yum install xorg-x11-fonts-Type1

rpm -i wkhtmltox-0.12.2.1_linux-centos7-amd64.rpm

rpm -ql wkhtmltox-0.12.2.1-1.x86_64
/usr/local/bin/wkhtmltoimage
/usr/local/bin/wkhtmltopdf
/usr/local/include/wkhtmltox/dllbegin.inc
/usr/local/include/wkhtmltox/dllend.inc
/usr/local/include/wkhtmltox/image.h
/usr/local/include/wkhtmltox/pdf.h
/usr/local/lib/libwkhtmltox.so
/usr/local/lib/libwkhtmltox.so.0
/usr/local/lib/libwkhtmltox.so.0.12
/usr/local/lib/libwkhtmltox.so.0.12.2
/usr/local/share/man/man1/wkhtmltoimage.1.gz
/usr/local/share/man/man1/wkhtmltopdf.1.gz

##安装PHP扩展(https://github.com/mreiferson/php-wkhtmltox)
cd phpwkhtmltox
phpize
./configure –with-php-config=/usr/local/php/bin/php-config #此处按照各自系统php安装路径不同而定
make && make install

通过简单设置,也可以直接输出PDF文档。
————————————————————————————-

PHP GuzzleHttp使用文档

内容来自:http://docs.guzzlephp.org/en/latest/,这个Http库实现上可以用完美来形容,之前使用过Zend\Http库,感觉已经很不错了,不过看起来,GuzzleHttp更胜一筹。

Guzzlehttp/guzzle当前最新分支6.x,5.x是维护阶段,4.x之前已经终结。

6.x需要PHP5.5以上,依赖guzzlehttp下的psr7和promises包。它提供两种驱动支持,一是PHP的stream扩展,需要在php.ini中启用allow_url_fopen,二是cURL扩展。如果cURL没有安装,那么就使用PHP的stream扩展,也可以指定自己的驱动。

Composer安装:

 {
   "require": {
      "guzzlehttp/guzzle": "~6.0"
   }
}

开始开始:

use GuzzleHttp\Client;
// 客户端
$client = new Client([
    'base_uri' => 'https://foo.com/api/',
    'timeout'  => 2.0,
]);
// 发起请求
$response = $client->request('GET', 'test');
$response = $client->request('GET', '/root');

看明白这里的请求URL是关键,第一个发起的URL是https://foo.com/api/test,第二发起的URL是https://foo.com/root。(RFC 3986规范)

客户端Client构造函数接受的参数是base_uri,handler和任何请求参数(可以传递到request对象的参数)。这里的参数除了handler,都是可以覆盖的。handler参数的解释:
“(callable) Function that transfers HTTP requests over thewire. The function is called with a Psr7\Http\Message\RequestInterface and array of transfer options, and must return a GuzzleHttp\Promise\PromiseInterface that is fulfilled with a Psr7\Http\Message\ResponseInterface on success. “handler” is a constructor only option that cannot be overridden in per/request options. If no handler is provided, a default handler will be created that enables all of the request options below by attaching all of the default middleware to the handler.”

要理解这段话并不容易。大体上是说这个handler被Psr7\Http\Message\RequestInterface对象调用返回一个被Psr7\Http\Message\ResponseInterface填充的GuzzleHttp\Promise\PromiseInterface。一般我们理解应该是返回Response,这里返回一个Promise,引入了一个中间层,实际是为了可以产生异步调用而准备的。Promise可以是同步的,也可以是异步的。

看这个例子:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;

$handler = new CurlHandler();
$stack = HandlerStack::create($handler); // Wrap w/ middleware
$client = new Client(['handler' => $stack]);

可见,hanlder就是底层实际传输http内容的工具,比如curl,stream。(默认的hanlder就是优先使用curl,如果要强制使用curl,可以参考这个例子),可以给halder添加中间件。

发起请求:

$response = $client->get('http://httpbin.org/get');
$response = $client->delete('http://httpbin.org/delete');
$response = $client->head('http://httpbin.org/get');
$response = $client->options('http://httpbin.org/get');
$response = $client->patch('http://httpbin.org/patch');
$response = $client->post('http://httpbin.org/post');
$response = $client->put('http://httpbin.org/put');

#替代
$response = $client->request('GET',"");

也可以先创建一个请求对象,然后通过Client的send的方法发起请求:

use GuzzleHttp\Psr7\Request;

$request = new Request('PUT', 'http://httpbin.org/put');
$response = $client->send($request, ['timeout' => 2]);

对应,可以使用sendAsync()和requestAsync()发起异步请求:

use GuzzleHttp\Psr7\Request;

// Create a PSR-7 request object to send
$headers = ['X-Foo' => 'Bar'];
$body = 'Hello!';
$request = new Request('HEAD', 'http://httpbin.org/head', $headers, $body);

// Or, if you don't need to pass in a request instance:
$promise = $client->requestAsync('GET', 'http://httpbin.org/get');

如果是异步请求,可以使用then方法接收响应,或者异常:

use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;

$promise = $client->requestAsync('GET', 'http://httpbin.org/get');
$promise->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
);

有了异步的实现,那么就可以并行发起一批请求:

use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client(['base_uri' => 'http://httpbin.org/']);

// Initiate each request but do not block
$promises = [
    'image' => $client->getAsync('/image'),
    'png'   => $client->getAsync('/image/png'),
    'jpeg'  => $client->getAsync('/image/jpeg'),
    'webp'  => $client->getAsync('/image/webp')
];

// Wait on all of the requests to complete. Throws a ConnectException
// if any of the requests fail
$results = Promise\unwrap($promises);

// Wait for the requests to complete, even if some of them fail
$results = Promise\settle($promises)->wait();

// You can access each result using the key provided to the unwrap
// function.
echo $results['image']->getHeader('Content-Length');
echo $results['png']->getHeader('Content-Length');

或者使用Pool来进行并发请求:

use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;

$client = new Client();

$requests = function ($total) {
    $uri = 'http://127.0.0.1:8126/guzzle-server/perf';
    for ($i = 0; $i < $total; $i++) {
        yield new Request('GET', $uri);
    }
};

$pool = new Pool($client, $requests(100), [
    'concurrency' => 5,
    'fulfilled' => function ($response, $index) {
        // this is delivered each successful response
    },
    'rejected' => function ($reason, $index) {
        // this is delivered each failed request
    },
]);

// Initiate the transfers and create a promise
$promise = $pool->promise();

// Force the pool of requests to complete.
$promise->wait();

使用响应:

$code = $response->getStatusCode(); // 200
$reason = $response->getReasonPhrase(); // OK

// Check if a header exists.
if ($response->hasHeader('Content-Length')) {
    echo "It exists";
}

// Get a header from the response.
echo $response->getHeader('Content-Length');

// Get all of the response headers.
foreach ($response->getHeaders() as $name => $values) {
    echo $name . ': ' . implode(', ', $values) . "\r\n";
}

$body = $response->getBody();
// Implicitly cast the body to a string and echo it
echo $body;
// Explicitly cast the body to a string
$stringBody = (string) $body;
// Read 10 bytes from the body
$tenBytes = $body->read(10);
// Read the remaining contents of the body as a string
$remainingBytes = $body->getContents();

查询参数:

$response = $client->request('GET', 'http://httpbin.org?foo=bar');

$client->request('GET', 'http://httpbin.org', [
    'query' => ['foo' => 'bar']
]);

$client->request('GET', 'http://httpbin.org', ['query' => 'foo=bar']);

上传数据(数据直接作为body体):

// Provide the body as a string.
$r = $client->request('POST', 'http://httpbin.org/post', [
    'body' => 'raw data'
]);

// Provide an fopen resource.
$body = fopen('/path/to/file', 'r');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// Use the stream_for() function to create a PSR-7 stream.
$body = \GuzzleHttp\Psr7\stream_for('hello!');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// 传送JSON数据
$r = $client->request('PUT', 'http://httpbin.org/put', [
    'json' => ['foo' => 'bar']
]);

POST表单请求(如果是GET的表单,就是简单的查询字符串,不是这里讨论的内容)

$response = $client->request('POST', 'http://httpbin.org/post', [
    'form_params' => [
        'field_name' => 'abc',
        'other_field' => '123',
        'nested_field' => [	// 嵌套,checkbox多选
            'nested' => 'hello'
        ]
    ]
]);

// 上传文件
$response = $client->request('POST', 'http://httpbin.org/post', [
    'multipart' => [
        [
            'name'     => 'field_name',
            'contents' => 'abc'
        ],
        [
            'name'     => 'file_name',
            'contents' => fopen('/path/to/file', 'r')
        ],
        [
            'name'     => 'other_file',
            'contents' => 'hello',
            'filename' => 'filename.txt',
            'headers'  => [
                'X-Foo' => 'this is an extra header to include'
            ]
        ]
    ]
]);

Cookies

// Use a specific cookie jar
$jar = new \GuzzleHttp\Cookie\CookieJar;
$r = $client->request('GET', 'http://httpbin.org/cookies', [
    'cookies' => $jar
]);

// 如果要对所有请求使用cookies,可以在Client指定使用cookies
// Use a shared client cookie jar
$client = new \GuzzleHttp\Client(['cookies' => true]);
$r = $client->request('GET', 'http://httpbin.org/cookies');

重定向(GET请求和POST请求的重定向需要注意)
Guzzle自动跟踪重定向,可以明确关闭:

$response = $client->request('GET', 'http://github.com', [
    'allow_redirects' => false
]);
echo $response->getStatusCode();
// 301

异常:

use GuzzleHttp\Exception\ClientException;

try {
    $client->request('GET', 'https://github.com/_abc_123_404');
} catch (ClientException $e) {
    echo $e->getRequest();
    echo $e->getResponse();
}

请求对象可选项:

##############
allow_redirects
默认:
[
    'max'             => 5,
    'strict'          => false,
    'referer'         => true,
    'protocols'       => ['http', 'https'],
    'track_redirects' => false
]

#指定false关闭
$res = $client->request('GET', '/redirect/3', ['allow_redirects' => false]);
echo $res->getStatusCode();

##############
auth
$client->request('GET', '/get', ['auth' => ['username', 'password']]);
$client->request('GET', '/get', [
    'auth' => ['username', 'password', 'digest']
]);

##############
body 请求体内容

##############
cert

##############
cookies 可以在Client设置cookie为true以设置所有请求使用cookie

$jar = new \GuzzleHttp\Cookie\CookieJar();
$client->request('GET', '/get', ['cookies' => $jar]);

##############
connect_timeout
默认为0,表示不超时(无限等待)

##############
debug 可以输出调试信息,或把调试信息写入文件(fopen)

##############
decode_content
默认为ture,表示解码服务端回送的压缩的内容

##############
delay

##############
expect

##############
form_params

##############
headers

$client->request('GET', '/get', [
    'headers' => [
        'User-Agent' => 'testing/1.0',
        'Accept'     => 'application/json',
        'X-Foo'      => ['Bar', 'Baz']
    ]
]);

##############
http_errors
默认为true,表示出错时抛出异常

##############
json

##############
multipart

##############
on_headers

##############
query
$client->request('GET', '/get?abc=123', ['query' => ['foo' => 'bar']]);

##############
sink
保存请求体
$resource = fopen('/path/to/file', 'w');
$client->request('GET', '/stream/20', ['sink' => $resource]);

$resource = fopen('/path/to/file', 'w');
$stream = GuzzleHttp\Psr7\stream_for($resource);
$client->request('GET', '/stream/20', ['save_to' => $stream]);

##############
verify
默认为true,验证SSL证书

##############
timeout
默认为0,不超时。(请求超时)

当使用curl时(默认优先使用,如果指定curl参数,最后可以明确指定使用curl作为hanlder,否则无效):

$client->request('GET', '/', [
    'curl' => [
        CURLOPT_INTERFACE => 'xxx.xxx.xxx.xxx'
    ]
]);

PHP账户登录-拉验证码手动登录实例

<?php

/*
 * 当出现验证码时,拉验证码手动登录一次,解决账户登录需要验证码问题
 * Auth:vfeelit@qq.com
 * At:2016-04-25
 */
namespace App\Console\Commands;

use Illuminate\Console\Command;

class AmazonLogin extends Command {
    protected $signature = 'amazon:login {--vcode=}';
    protected $description = '';
    private $_cookie;
    private $_auth;
    private $_url;
    private $_timeout = 20;
    public function __construct() {
        $this->_url = [ 
            'preLogin' => env ( 'AMZ_PRELOGIN' ),
            'postLogin' => env ( 'AMZ_POSTLOGIN' ) 
        ];
        $this->_cookie = env ( 'AMZ_COOKIE' );
        $this->_auth = [ 
            'username' => env ( 'AMZ_UNA' ),
            'password' => env ( 'AMZ_PWD' ) 
        ];
        
        parent::__construct ();
    }
    public function handle() {
        $vcode = trim ( $this->option ( 'vcode' ) );
        if (! empty ( $vcode )) {
            $loginResult = $this->loginForce ( $vcode );
            if (str_contains ( $loginResult, 'id="ap_password"' )) {
                echo "使用验证码登录失败\n";
                file_put_contents ( "login2.html", $loginResult );
            } else {
                echo "使用验证码登录成功\n";
                
                // 清理垃圾
                @unlink ( "login.html" );
                @unlink ( "login2.html" );
                @unlink ( "vcode.jpg" );
            }
        } else {
            $loginResult = $this->login ();
            
            if (str_contains ( $loginResult, 'id="ap_password"' )) {
                echo "需要输入验证码\n";
                file_put_contents ( "login.html", $loginResult );
                // 取回验证码图片
                $this->getVcode ();
            } else {
                echo "成功登录\n";
                
                // 清理垃圾
                @unlink ( "login.html" );
                @unlink ( "login2.html" );
                @unlink ( "vcode.jpg" );
            }
        }
    }
    
    // 正常登录
    private function login() {
        $ch = curl_init ();
        
        curl_setopt ( $ch, CURLOPT_URL, $this->_url ['preLogin'] );
        curl_setopt ( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36' );
        curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
        // curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt ( $ch, CURLOPT_HEADER, false );
        curl_setopt ( $ch, CURLOPT_CONNECTTIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_TIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_COOKIEFILE, $this->_cookie );
        curl_setopt ( $ch, CURLOPT_COOKIEJAR, $this->_cookie );
        
        $indexHtml = curl_exec ( $ch );
        
        $reHiddenPostData = '/<input type="hidden"\s*name="([^"]*)"\s*value="([^"]*)"/i';
        $count = preg_match_all ( $reHiddenPostData, $indexHtml, $hiddenPostData );
        
        $postData = array ();
        for($i = 0; $i < $count; $i ++) {
            $postData [$hiddenPostData [1] [$i]] = $hiddenPostData [2] [$i];
        }
        $postData ['username'] = $this->_auth ['username'];
        $postData ['password'] = $this->_auth ['password'];
        
        curl_setopt ( $ch, CURLOPT_URL, $this->_url ['postLogin'] );
        curl_setopt ( $ch, CURLOPT_POST, true );
        curl_setopt ( $ch, CURLOPT_POSTFIELDS, $postData );
        
        return curl_exec ( $ch );
    }
    
    // 拉回验证码图片
    private function getVcode() {
        $indexHtml = file_get_contents ( "login.html" );
        
        $vcodeBase = "https://opfcaptcha-prod.s3.amazonaws.com";
        
        preg_match ( '#src="' . $vcodeBase . '\/([^"]+)#', $indexHtml, $actions );
        $postUrl = $vcodeBase . '/' . $actions [1];
        $fp = fopen ( "vcode.jpg", 'wb' );
        
        $ch = curl_init ();
        
        curl_setopt ( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36' );
        curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt ( $ch, CURLOPT_FILE, $fp );
        curl_setopt ( $ch, CURLOPT_HEADER, false );
        curl_setopt ( $ch, CURLOPT_CONNECTTIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_TIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_COOKIEFILE, $this->_cookie );
        curl_setopt ( $ch, CURLOPT_COOKIEJAR, $this->_cookie );
        
        curl_setopt ( $ch, CURLOPT_URL, $postUrl );
        
        curl_exec ( $ch );
        curl_close ( $ch );
        fclose ( $fp );
    }
    
    // 使用验证码登录
    private function loginForce($vcode) {
        $indexHtml = file_get_contents ( "login.html" );
        
        preg_match ( '/action="https:([^"]+)/', $indexHtml, $actions );
        $postUrl = 'https:' . $actions [1];
        
        $reHiddenPostData = '/<input type="hidden"\s*name="([^"]*)"\s*value="([^"]*)"/i';
        $count = preg_match_all ( $reHiddenPostData, $indexHtml, $hiddenPostData );
        
        $postData = array ();
        for($i = 0; $i < $count; $i ++) {
            $postData [$hiddenPostData [1] [$i]] = $hiddenPostData [2] [$i];
        }
        // 使用验证码登录时用户名对应的是email,不是username
        $postData ['email'] = $this->_auth ['username'];
        $postData ['password'] = $this->_auth ['password'];
        $postData ['guess'] = $vcode;
        // print_r($postData);
        
        $ch = curl_init ();
        curl_setopt ( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36' );
        curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt ( $ch, CURLOPT_HEADER, false );
        curl_setopt ( $ch, CURLOPT_CONNECTTIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_TIMEOUT, $this->_timeout );
        curl_setopt ( $ch, CURLOPT_COOKIEFILE, $this->_cookie );
        curl_setopt ( $ch, CURLOPT_COOKIEJAR, $this->_cookie );
        
        curl_setopt ( $ch, CURLOPT_URL, $postUrl );
        curl_setopt ( $ch, CURLOPT_POST, true );
        curl_setopt ( $ch, CURLOPT_POSTFIELDS, $postData );
        
        return curl_exec ( $ch );
    }
}

Ubuntu 软件安装

Ubuntu基于Debian发行版和GNOME桌面环境,而从11.04版起,Ubuntu发行版放弃了Gnome桌面环境,改为Unity,与Debian的不同在于它每6个月会发布一个新版本。

Ubuntu 12.04和14.04桌面版与服务器版都有5年支持周期。而之前的长期支持版本为桌面版3年,服务器版5年。

版本号
代号
发布时间
15.04 Vivid Vervet
14.10 Utopic Unicorn 2014/10/23
14.04 LTS Trusty Tahr 2014/04/18
13.10 Saucy Salamander 2013/10/17
13.04 Raring Ringtail 2013/04/25
12.10
Quantal Quetzal
2012/10/18
12.04 LTS
Precise Pangolin
2012/04/26
11.10
Oneiric Ocelot
2011/10/13
11.04(Unity成为默认桌面环境)
Natty Narwhal
2011/04/28
10.10
Maverick Meerkat
2010/10/10
10.04 LTS
Lucid Lynx
2010/04/29
9.10
Karmic Koala
2009/10/29
9.04
Jaunty Jackalope
2009/04/23
8.10
Intrepid Ibex
2008/10/30
8.04 LTS
Hardy Heron
2008/04/24
7.10
Gutsy Gibbon
2007/10/18
7.04
Feisty Fawn
2007/04/19
6.10
Edgy Eft
2006/10/26
6.06 LTS
Dapper Drake
2006/06/01
5.10
Breezy Badger
2005/10/13
5.04
Hoary Hedgehog
2005/04/08
4.10(初始发布版本)
Warty Warthog
2004/10/20
Ubuntu历史版本一览表

对于桌面应用,我们可以使用其最新的发布版本,但是对于服务器,我们主要关注LTS版本,目前(2016年)还被支持的LTS版本有12.04和14.04和最新发布的16.04, 12.04会在17年停止支持。按照惯例,LTS版本发布后,没过一段时间(大约周期视乎是6个月),会发布一个积累了之前更新的版本,命名格式为12.04.1(第二个点之后编号1,2,3….)。注意每个版本的发布,都有一个代号(以水果名称命名)。

官方发布:http://releases.ubuntu.com/
国内镜像:http://mirrors.aliyun.com/ubuntu-releases/
http://mirrors.163.com/ubuntu-releases/

系统离不开应用软件,Ubuntu提供了许多的软件包,它使得应用软件的安装非常便利。最常用的安装软件的工具是apt(Advanced Packaging Tool),它类似RedHead系列系统中的yum。apt使用/etc/apt/sources.list文件中定义的来源来安装软件,模板:

#14.04LTS trusty
deb http://mirrors.aliyun.com/ubuntu trusty main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu trusty-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu trusty-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu trusty-backports main restricted universe multiverse

deb-src http://mirrors.aliyun.com/ubuntu trusty main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu trusty-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu trusty-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu trusty-backports main restricted universe multiverse

在这个文件中,deb关键字用来定义已编译的软件包的来源,deb-src关键字用来定义源码包的来源:

deb(或deb-src)	网络地址	主版本代号 软件仓库1  软件仓库2 .....

如果不需要源码包,可以把deb-src的行注释。

Ubuntu的软件主版本代号trusty,trusty-security,trusty-updates,trusty-backports,这里的trusty对应的就是14.04 LTS,如果是12.04 LTS对应的就是Precise。

Ubuntu的软件仓库分为4个部分,分别是main restricted universe和multiverse。第一部分main的软件包是自由软件,Ubuntu提供完全支持。第二部分restricted的软件包,不是完全的自由软件,但是应为这些软件包被广泛使用,所以Ubuntu也提供支持。第三部分universe的软件包,Ubuntu官方不提供支持。最后一部分multiverse软件包,是非自由软件。

修改了sources.list文件,需要运行apt-get update才能使之生效。

如果在Precise下,安装的软件可能比较旧,可以修改sources.list文件,让其指定到更新的版本,比如trusty。如果这样做,那么很多依赖的软件包可能会被更新,而且某些软件无法安装到最新的版本。在某个版本下,为了可以安装到最新的某个软件包,可以使用Ubuntu提供的PPA。

Personal Package Archives,个人软件包档案,Ubuntu Launchpad网站提供的一项源服务,允许个人用户上传软件源代码,通过Launchpad进行编译并发布为2进制软件包,作为apt/新立得源供其他用户下载和更新。

安装使用PPA上的软件,过程:
1 首先到https://launchpad.net/+ppa搜索软件包(比如搜索PHP)
search_ppa_php
2 进入详细页,根据说明添加这个PPA源到系统
add_ppa_to
选择对应的版本,然后编辑source.list文件添加:

deb http://ppa.launchpad.net/ondrej/php5/ubuntu trusty main 
deb-src http://ppa.launchpad.net/ondrej/php5/ubuntu trusty main 

然后运行apt-get update就完成添加。也可以运行一条命令:

add-apt-repository ppa:ondrej/php5

这里的ppa:ondrej/php5就是这个PPA软件包的名称(冒号之后的名称)。这个名称一般在详情页面有详细说明。

add-apt-repository ppa:ondrej/php5
You are about to add the following PPA to your system:
 This PPA contains latest PHP 5.5 packaged for Ubuntu 14.04 LTS (Trusty).

You can get more information about the packages at https://deb.sury.org

If you need other PHP versions use:
  PHP 5.4: ppa:ondrej/php5-oldstable (Ubuntu 12.04 LTS)
  PHP 5.5: ppa:ondrej/php5 (Ubuntu 14.04 LTS)
  PHP 5.6: ppa:ondrej/php5-5.6 (Ubuntu 14.04 LTS - Ubuntu 16.04 LTS)
  PHP 5.6 and PHP 7.0: ppa:ondrej/php (Ubuntu 14.04 LTS - Ubuntu 16.04 LTS)

BUGS&FEATURES: This PPA now has a issue tracker: https://deb.sury.org/pages/bugreporting.html

PLEASE READ: If you like my work and want to give me a little motivation, please consider donating: https://deb.sury.org/pages/donate.html

WARNING: add-apt-repository is broken with non-UTF-8 locales, see https://github.com/oerdnj/deb.sury.org/issues/56 for workaround:

# apt-get install -y language-pack-en-base
# LC_ALL=en_US.UTF-8 add-apt-repository ppa:ondrej/php5
 More info: https://launchpad.net/~ondrej/+archive/ubuntu/php5
Press [ENTER] to continue or ctrl-c to cancel adding it

gpg: keyring `/tmp/tmpXJxwwj/secring.gpg' created
gpg: keyring `/tmp/tmpXJxwwj/pubring.gpg' created
gpg: requesting key E5267A6C from hkp server keyserver.ubuntu.com
gpg: /tmp/tmpXJxwwj/trustdb.gpg: trustdb created
gpg: key E5267A6C: public key "Launchpad PPA for xx imported
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)
OK

从这个输出来看,就仅导入了公钥。实际上它还往/etc/apt/sources.list.d里面写入了一个文件(ondrej-php5-precise.list):

vi ondrej-php5-precise.list
deb http://ppa.launchpad.net/ondrej/php5/ubuntu precise main
deb-src http://ppa.launchpad.net/ondrej/php5/ubuntu precise main

这些文件会和source.list文件进行合并。然后运行apt-get update之后就可以安装这个PPA软件了。相比CentOS下到处找第三方的YUM源相比,PPA提供了一个集中的方式,让我们可以更加容易的找到需要的软件包。

安装PHP实例:

#安装PHP源
add-apt-repository ppa:ondrej/php
apt-get update

#搜索
apt-cache search php
apt-cache search php5.6
apt-cache search php7

#列表
php7.0-common - documentation, examples and common module for PHP
php7.0-cgi - server-side, HTML-embedded scripting language (CGI binary)
php7.0-cli - command-line interpreter for the PHP scripting language
php7.0-fpm - server-side, HTML-embedded scripting language (FPM-CGI binary)
php7.0-dev - Files for PHP7.0 module development
php7.0-curl - CURL module for PHP
php7.0-enchant - Enchant module for PHP
php7.0-gd - GD module for PHP
php7.0-gmp - GMP module for PHP
php7.0-imap - IMAP module for PHP
php7.0-interbase - Interbase module for PHP
php7.0-intl - Internationalisation module for PHP
php7.0-ldap - LDAP module for PHP
php7.0-mcrypt - libmcrypt module for PHP
php7.0-readline - readline module for PHP
php7.0-odbc - ODBC module for PHP
php7.0-pgsql - PostgreSQL module for PHP
php7.0-pspell - pspell module for PHP
php7.0-recode - recode module for PHP
php7.0-snmp - SNMP module for PHP
php7.0-tidy - tidy module for PHP
php7.0-xmlrpc - XMLRPC-EPI module for PHP
php7.0-xsl - XSL module for PHP (dummy)
php7.0-json - JSON module for PHP
php7.0-sybase - Sybase module for PHP
php7.0-sqlite3 - SQLite3 module for PHP
php7.0-mysql - MySQL module for PHP
php7.0-opcache - Zend OpCache module for PHP
php7.0-bz2 - bzip2 module for PHP
php7.0-bcmath - Bcmath module for PHP
php7.0-mbstring - MBSTRING module for PHP
php7.0-soap - SOAP module for PHP
php7.0-xml - DOM, SimpleXML, WDDX, XML, and XSL module for PHP
php7.0-zip - Zip module for PHP
php7.0-dba - DBA module for PHP

#确认安装的软件包
dpkg -l | grep php
CentOS
Ubuntu
软件包后缀 *.rpm *.deb
软件源配置文件 /etc/yum.conf /etc/apt/sources.list
更新软件包列表 每次yun时自动执行 apt-get update
从软件仓库中安装软件 yum install package apt-get install package
安装一个已下载的软件 yum install pkg.rpm
rpm -i pkg.rpm
dpkg -i pkg.deb
删除软件包
rpm -e package
apt-get remove package
软件包升级检查
yum check-update
apt-get -s upgrade
升级软件包
yum update
apt-get upgrade
升级整个系统
yum upgrade
apt-get dist-upgrade
获取某软件包的信息(搜索)
yum search package
apt-cache show package
apt-cache search package
获取所有软件包的信息
yum list available
apt-cache dumpavail
显示所有已安装的软件
yum list installed
rpm -qa
dpkg -l
dpkg –list
获取某个已安装软件信息
yum info package
rpm -qi package
dpkg –status packages
获取文件列表
rpm -ql package
dpkg –listfile package
dpkg -L package
显示依赖列表
rpm -qR package
apt-cache depends package
显示反向依赖(被依赖)
rpm -q -whatrequires [args]
apt-cache rdeprends package
搜索文件有哪个软件包提供
rpm -qf /file/name
dpkg -S /file/name
dpkg –search /file/name
Ubuntu CentO 对照表

关于自动启动:
Ubuntu控制软件启动的方式,就是SystemV哪套,所有系统服务都保存在/etc/init.d下面,这里面的实际就是一个个Shell脚本。Ubuntu默认运行级别是2,在/etc/rcN.d中(N代表运行级别,比如2,就是rc2.d),如果在这个级别需要启动就会对应一个软连接到/etc/init.d下的脚本,格式是SxxName(xx表示顺序);如果需要关闭就会对应一个软连接到/etc/init.d下的脚本,格式是KxxName(xx表示顺序)。由此可见,服务的启动关闭是顺序执行的。

由于顺序执行的缺陷,Ubuntu自己搞了一套基于事件驱动的服务启动方式,所有需要开机启动的服务,都会放入到/etc/init下,格式xxx.conf。查看一个例子:

cat ssh.conf 
# ssh - OpenBSD Secure Shell server
#
# The OpenSSH server provides secure shell access to the system.

description	"OpenSSH server"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
respawn limit 10 5
umask 022

env SSH_SIGSTOP=1
expect stop

# 'sshd -D' leaks stderr and confuses things in conjunction with 'console log'
console none

pre-start script
    test -x /usr/sbin/sshd || { stop; exit 0; }
    test -e /etc/ssh/sshd_not_to_be_run && { stop; exit 0; }

    mkdir -p -m0755 /var/run/sshd
end script

# if you used to set SSHD_OPTS in /etc/default/ssh, you can change the
# 'exec' line here instead
exec /usr/sbin/sshd -D

这个配置里面,明确了这个服务需要运行的级别。另外,可以在/etc/default/xxx中放入一些所谓的默认值。大概就是这么个意思了。

由于有两套方式,所以Ubuntu是兼容这两套方式的,A没有就检查B。

很不幸的是,在Ubuntu16.04中,Ubuntu放弃了自己的那套事件驱动的服务启动方式,开始使用Systemd(CentOS7 中也使用它)。

HTTP BASIC认证操作实践

HTTP BASIC认证是HTTP层次内容,以下使用PHP来实现这个过程:

vi basic.php

<?php
$users = [
    "admin" => "xxxxxx"
];
$needAuth = true;
if(!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
    // 用户名和密码
    $user = trim($_SERVER['PHP_AUTH_USER']);
    $pwd = trim($_SERVER['PHP_AUTH_PW']);

    if(isset($users[$user]) && ($users[$user] === $pwd)) {
        $needAuth = false;
    }
}

if($needAuth) {
    header("Content-type:text/html;charset=utf-8");
    header('WWW-Authenticate: Basic realm="admin xxxxxx"');
    header('HTTP/1.0 401 Unauthorized');
    echo date("Y-m-d H:i:s")." -> Need Auth...";
    exit;
}

echo date("Y-m-d H:i:s")." -> Auth...";

http_basic_auth
对于一个小应用程序,如果需要做隐藏保护,这个方式会非常便利。不过这个认证方式更多见于API访问中。

查看发送的HTTP请求头:
http_basic_header
用户名和密码通过HTTP的一个请求头Authorization来传输的,内容是Basic YWRtaW46eHh4eHh4,第一个字符串Basic为认证方式,第二个字符串是用户名和密码冒号分隔的字符串的base64编码(admin:xxxxxx -> YWRtaW46eHh4eHh4)。

这个用户名和密码传递到服务器端,对于Nginx(Apache等),它可以首先处理,也可以继续转发到PHP,让PHP来处理(这里就是这个情况)。PHP接收这两个变量使用:

$_SERVER['PHP_AUTH_USER']
$_SERVER['PHP_AUTH_PW']

这两个变量是经过了base64解码之后得到的,这个解码应该是HTTP服务进行的,把得到的变量传递给PHP。注意,这里的base64编码目的不是在加密,而是方便传输。所有如果直接通过HTTP传输是不安全的(其它的一般用户名密码登录也一样),所以,对于API设计,为了安全,一般通过HTTPS传送数据。

BASCIC认证是HTTP层次的内容,所以对于Nginx这样的HTTP服务器软件,当然可以配置其进行BASIC认证,这样就不需要由PHP来处理。Nginx配置参考:

server {
    listen       80;
    server_name  xx.xx.xx.xx;
    root /var/www/xxx/public;
    index index.php

    error_page 404 /index.php;
    
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/conf.d/htpasswd;

    if (!-e $request_filename) {
        rewrite ^/(.+)$ /index.php last;
        break;
    }

    location / {
	root /var/www/xxx/public;
	try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* ^.+\.(css|js|jpeg|jpg|gif|png|ico|eot|ttf|woff|svg) {
        expires 30d;
    }

    location ~* \.(eot|ttf|woff|svg|html)$ {
        add_header Access-Control-Allow-Origin *;
    }

    location ~ .(php|php5)?$ {
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 256 32k;

        fastcgi_pass   127.0.0.1:9000;
	fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
   }
}

主要是添加auth_basic指令和auth_basic_user_file指令,auth_basic和PHP中的如下两行设置类似:

header('WWW-Authenticate: Basic realm="admin xxxxxx"');
header('HTTP/1.0 401 Unauthorized');

指令auth_basic的值直接对应Basic realm=”xxx”中的xxx值,表示是BASIC认证,认证的用户名和密码是auth_basic_user_file指定的密码文件,这个文件中保存的用户名密码可不是用base64编码的,它使用的是Hash算法,格式:

vfeelit:CQArTEgiT84So:注释

匹配过程大体应该是这样:获取HTTP的Authorization请求头,从中获取经过base64编码的字符串,解码获取用户名和密码,然后匹配用户名,再通过Hash算法得到密码的Hash值,最后和保存的Hash值进行比较。

这个密码文件产生(htpasswd或者openssl):

# printf "vfeelit:$(openssl passwd -crypt 123456)\n" >> conf.d/htpasswd
# cat conf.d/htpasswd 
vfeelit:DmZ3GXV9zFegY

Laravel 把命令推入队列

如果一个命令需要执行很长时间才会退出,那么把这个命令推入队列就很有必要。Laravel提供了方法:

Artisan::queue('email:send', [
    'user' => 1, '--qn' => 'default'
]);

这里的意思就是由Worker去执行email:send命令,参数就是后面给出的数组。

不过这里需要整明白的是,它推入了队列,但是队列名称是什么,不能指定吗? 第二,Worker取回这个Job时,如果知道它是一个命令,然后执行?

首先,Atisan这个Facade对应一个实现了Illuminate\Contracts\Console\Kernel接口的实例,实现这个接口的类是Illuminate\Foundation\Console\Kernel,我们最终使用的App\Console\Kernel就是从这里继承类继承而来。换句话说,Atisan这个Facade直接对应了App\Console\Kernel实例,它是一个命令管理工具,提供了调用命令的方法。

所以,需要去到Illuminate\Foundation\Console\Kernel中查看具体实现:

    public function call($command, array $parameters = [])
    {
        $this->bootstrap();

        return $this->getArtisan()->call($command, $parameters);
    }

    /**
     * Queue the given console command.
     *
     * @param  string  $command
     * @param  array   $parameters
     * @return void
     */
    public function queue($command, array $parameters = [])
    {
        $this->app['Illuminate\Contracts\Queue\Queue']->push(
            'Illuminate\Foundation\Console\QueuedJob', func_get_args()
        );
    }

看到Kernel的call实际是Artisan应用的call方法的包装器。主要看下queue方法,它从容器中取回队列对象,压了一个Illuminate\Foundation\Console\QueuedJob类型的对象进入队列,push方法第三参数指定队列,这里没有指定,意思就是说它直接放入默认名称(default)的队列。对于一个Job,最终fire方法会被调用,查看Illuminate\Foundation\Console\QueuedJob:

    public function fire($job, $data)
    {
        call_user_func_array([$this->kernel, 'call'], $data);

        $job->delete();
    }

可以看到,直接调用Kernel的call方法。