RabbitMq客户端 – php-amqplib库

官网教程地址:http://www.rabbitmq.com/tutorials/tutorial-one-php.html

Hello World!简单使用

安装

直接使用composer加载

composer require php-amqplib/php-amqplib

send发送

send.php中包含库并使用:

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

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

创建到服务器的连接:

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

要发送,我们必须声明一个队列供我们发送; 然后我们可以向队列发布消息:

$channel->queue_declare('hello', false, false, false, false);
$msg = new AMQPMessage('Hello World!');
$channel->basic_publish($msg, '', 'hello');
echo " [x] Sent 'Hello World!'\n";

声明队列是幂等的 - 只有在它不存在的情况下才会创建它。

关闭了频道和连接:

$channel->close();
$connection->close();

php send.php执行失败,可能是未安装php的bcmath扩展,可以用过phpize动态编译安装

receive接收

receive.php

设置与send生产者相同; 我们打开一个连接和一个通道,并声明我们将要消耗的队列。请注意,这与发送的队列匹配。

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

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('hello', false, false, false, false);
echo " [*] Waiting for messages. To exit press CTRL+C\n";

定义一个PHP callable,它将接收服务器发送的消息。请记住,消息是从服务器异步发送到客户端的。

$callback = function ($msg) {
    echo ' [x] Received ', $msg->body, "\n";
};
$channel->basic_consume('hello', '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
    $channel->wait();
}

这里通过while保证进程常驻

  • 列出队列
rabbitmqctl list_queues

完整代码:send.phpreceive.php
测试结果如图:
hello world

Work queues工作队列

这里将创建一个工作队列,用于在多个工作人员之间分配耗时的任务。

准备

这里将通过sleep()函数,模拟耗时任务。通过字符串中.点的个数作为其复杂性。

稍微修改前一个示例中的send.php代码,以允许从命令行发送任意消息。重命名为new_task.php

$data = implode(' ', array_slice($argv, 1));
if (empty($data)) {
    $data = "Hello World!";
}
$msg = new AMQPMessage($data);

$channel->basic_publish($msg, '', 'hello');

echo ' [x] Sent ', $data, "\n";

旧的receive.php脚本还需要进行一些更改:它需要为消息体中的每个点伪造一秒钟的工作。它将从队列中弹出消息并执行任务,所以我们称之为worker.php

$callback = function ($msg) {
  echo ' [x] Received ', $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done\n";
};

$channel->basic_consume('hello', '', false, true, false, false, $callback);

循环调度

使用任务队列的一个优点是能够轻松地并行工作。如果我们正在积压工作积压,我们可以添加更多工人,这样就可以轻松扩展。

打开四个控制台。三个将运行worker.php 脚本。测试结果如图:
worker

默认情况下,RabbitMQ将按顺序将每条消息发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环法

消息确认

执行任务可能需要几秒钟。您可能想知道如果其中一个消费者开始执行长任务并且仅在部分完成时死亡会发生什么。使用我们当前的代码,一旦RabbitMQ向客户发送消息,它立即将其标记为删除。在这种情况下,如果你杀死一个工人,我们将丢失它刚刚处理的消息。我们还将丢失分发给这个特定工作者但尚未处理的所有消息。

为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发回ack(nowledgement)告诉RabbitMQ已收到,处理了特定消息,RabbitMQ可以自由删除它。

如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失)而不发送确认,RabbitMQ将理解消息未完全处理并将重新排队。如果其他消费者同时在线,则会迅速将其重新发送给其他消费者。

默认情况下,消息确认已关闭。现在是时候通过设置第四个参数来打开它们basic_consumefalse(true表示没有ACK)

$callback = function ($msg) {
  echo ' [x] Received ', $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done\n";
  $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

被遗忘的ack
错过ack是一个常见的错误。这是一个简单的错误,但后果是严重的。当您的客户端退出时,消息将被重新传递(这可能看起来像随机重新传递),但RabbitMQ将会占用越来越多的内存,因为它无法释放任何未经处理的消息。

可以使用rabbitmqctl 来打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
  

消息持久性

消息确认确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。

当RabbitMQ退出或崩溃时,它将忘记队列和消息,除非你告诉它不要。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久

首先,我们需要确保RabbitMQ永远不会丢失我们的队列。为此,我们需要声明它是持久的。为此,我们将第三个参数传递给queue_declare为true:

$ channel->queue_declare('hello',false,true,false,false);

虽然此命令本身是正确的,但它在我们当前的设置中不起作用。那是因为我们已经定义了一个名为hello的队列 ,这个队列不耐用。RabbitMQ不允许您使用不同的参数重新定义现有队列,并将向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法 - 让我们声明一个具有不同名称的队列,例如task_queue:

$channel->queue_declare('task_queue', false, true, false, false);

此标志设置为true需要应用于生产者和消费者代码。

此时我们确信即使RabbitMQ重新启动,task_queue队列也不会丢失。现在我们需要将消息标记为持久性 - 通过设置delivery_mode = 2消息属性,AMQPMessage将其作为属性数组的一部分。

$msg = new AMQPMessage(
    $data,
    array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
);

公平派遣

您可能已经注意到调度仍然无法完全按照我们的意愿运行。例如,在有两个工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一个工人将经常忙碌而另一个工作人员几乎不会做任何工作。好吧,RabbitMQ对此一无所知,仍然会均匀地发送消息。

发生这种情况是因为RabbitMQ只是在消息进入队列时调度消息。它不会查看消费者未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。

我们可以使用basic_qos方法和prefetch_count = 1设置。这告诉RabbitMQ不要一次向一个worker发送一条消息。或者,换句话说,在处理并确认前一个消息之前,不要向worker发送新消息。相反,它会将它发送给下一个仍然不忙的worker。

$channel->basic_qos(null, 1, null);

完整代码:new_task.phpworker.php

测试结果如图:
公平差遣

Publish/Subscribe(发布/订阅)

工作队列背后的假设是每个任务都交付给一个工作者。在这一部分,我们将做一些完全不同的事情 - 我们将向多个消费者传递信息。此模式称为“发布/订阅”。

交换器

前面教程中的内容:
- 生产者是发送消息的用户的应用程序
- 队列是存储消息的缓冲器
- 消费者是接收消息的用户的应用程序

RabbitMQ中消息传递模型的核心思想生产者永远不会将任何消息直接发送到队列。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。

相反,生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面将它们推送到队列。交换器必须确切知道如何处理收到的消息。它应该附加到特定队列吗?它应该附加到许多队列吗?或者它应该被丢弃。其规则由交换类型定义。
交换器

有几种交换类型可供选择:direct(直接)topic(主题)headers(标题)fanout(扇出)。我们将专注于最后一个 - fanout扇出。让我们创建一个这种类型的交换,并将其称为日志:

$channel->exchange_declare('logs', 'fanout', false, false, false);

列出清单

rabbitmqctl list_exchanges
  

在此列表中将有一些amq.*交换和默认(未命名)交换。这些是默认创建的。

默认交换
之前能发送消息,是因为我们使用的默认交换,通过空字符串""来识别

之前是这样发送消息的:

$channel->basic_publish($msg, '', 'hello');
  

这里我们使用默认或无名交换:消息被路由到具有routing_key指定的名称的队列(如果存在)。路由键是basic_publish的第三个参数

临时队列

能够命名队列对我们来说至关重要 - 我们需要将工作人员指向同一个队列。当您想要在生产者和消费者之间共享队列时,为队列命名很重要。

但我们的记录器并非如此。我们希望了解所有日志消息,而不仅仅是它们的一部分。我们也只对目前流动的消息感兴趣,而不是旧消息。要解决这个问题,我们需要两件事。

首先,每当我们连接到Rabbit时,我们都需要一个新的空队列。为此,我们可以使用随机名称创建队列,或者更好 - 让服务器为我们选择随机队列名称。

其次,一旦我们断开消费者,就应该自动删除队列。

在php-amqplib客户端中,当我们将队列名称作为空字符串提供时,我们使用生成的名称创建一个非持久队列:

list($queue_name, ,) = $channel->queue_declare("");

方法返回时,$queue_name变量包含RabbitMQ生成的随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。

当声明它的连接关闭时,队列将被删除,因为它被声明为独占。

绑定

绑定

我们已经创建了一个扇出交换和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。交换和队列之间的关系称为绑定。

$channel->queue_bind($queue_name, 'logs');

列出绑定

rabbitmqctl list_bindings
  

生成日志消息的生产者程序与前一个教程没有太大的不同。最重要的变化是我们现在想要将消息发布到我们的日志交换而不是无名交换。这里是emit_log.php脚本的代码 :

<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->exchange_declare('logs', 'fanout', false, false, false);

$data = implode(' ', array_slice($argv, 1));
if (empty($data)) {
    $data = "info: Hello World!";
}
$msg = new AMQPMessage($data);

$channel->basic_publish($msg, 'logs');

echo ' [x] Sent ', $data, "\n";

$channel->close();
$connection->close();

在建立连接后我们宣布了交换。此步骤是必要的,因为禁止发布到不存在的交换

receive_logs.php的代码:

<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->exchange_declare('logs', 'fanout', false, false, false);
list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);
$channel->queue_bind($queue_name, 'logs');
echo " [*] Waiting for logs. To exit press CTRL+C\n";
$callback = function ($msg) {
    echo ' [x] ', $msg->body, "\n";
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
    $channel->wait();
}
$channel->close();
$connection->close();

Routing路由

这里我们将为其添加一个功能 - 我们将只能订阅一部分消息。例如,我们只能将关键错误消息定向到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

绑定

之前绑定流程:

$channel->queue_bind($queue_name, 'logs');

绑定是交换和队列之间的关系。这可以简单地理解为:队列对来自此交换的消息感兴趣。

绑定可以采用额外的routing_key参数。为了避免与$ channel::basic_publish参数混淆,我们将其称为绑定密钥。这就是我们如何使用键创建绑定:

$binding_key = 'black';
$channel->queue_bind($queue_name, $exchange_name, $binding_key);

绑定密钥的含义取决于交换类型。我们之前使用的扇出交换只是忽略了它的价值。

直接交换

我们上一个教程中的日志记录系统向所有消费者广播所有消息。我们希望扩展它以允许根据消息的严重性过滤消息。例如,我们可能希望将日志消息写入磁盘的脚本仅接收严重错误,而不是在警告或信息日志消息上浪费磁盘空间。

我们使用的是扇出交换,它没有给我们太大的灵活性 - 它只能进行无意识的广播。

我们将使用直接交换。直接交换背后的路由算法很简单 - 消息进入队列,其绑定密钥与消息的路由密钥完全匹配
直接交换

在此设置中,我们可以看到直接交换X与两个绑定到它的队列。第一个队列绑定橙色绑定,第二个绑定有两个绑定,一个绑定密钥为黑色,另一个绑定为绿色。

在这样的设置中,使用路由密钥orange发布到交换机的消息 将被路由到队列Q1。路由键为黑色 或绿色的消息将转到Q2。所有其他消息将被丢弃。

多个绑定

多个绑定

使用相同的绑定密钥绑定多个队列是完全合法的。在我们的例子中,我们可以在X和Q1之间添加绑定键黑色的绑定。在这种情况下,直接交换将表现得像扇出一样,并将消息广播到所有匹配的队列。路由密钥为黑色的消息将传送到 Q1和Q2。

发送日志

我们将此模型用于我们的日志系统。我们会将消息发送给直接交换,而不是扇出。我们将提供日志严重性作为路由密钥。这样接收脚本将能够选择它想要接收的严重性。让我们首先关注发送日志。

一如既往,我们需要先创建一个交换:

$channel->exchange_declare('direct_logs', 'direct', false, false, false);

我们已准备好发送消息:

$channel->exchange_declare('direct_logs', 'direct', false, false, false);
$channel->basic_publish($msg, 'direct_logs', $severity);

订阅

接收消息将像上一个教程一样工作,但有一个例外 - 我们将为我们感兴趣的每个严重性创建一个新的绑定。

foreach ($severities as $severity) {
    $channel->queue_bind($queue_name, 'direct_logs', $severity);
}

完整代码

直接交换多个绑定

emit_log_direct.php类的代码:

<?php
require_once __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->exchange_declare('direct_logs', 'direct', false, false, false);
$severity = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'info';
$data = implode(' ', array_slice($argv, 2));
if (empty($data)) {
    $data = "Hello World!";
}
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'direct_logs', $severity);
echo ' [x] Sent ', $severity, ':', $data, "\n";
$channel->close();
$connection->close();

receive_logs_direct.php的代码:

<?php
require_once __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->exchange_declare('direct_logs', 'direct', false, false, false);
list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);
$severities = array_slice($argv, 1);
if (empty($severities)) {
    echo '缺少安全级别参数', "\n";
    exit(1);
}
foreach ($severities as $severity) {
    $channel->queue_bind($queue_name, 'direct_logs', $severity);
}
echo " [*] Waiting for logs. To exit press CTRL+C\n";
$callback = function ($msg) {
    echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n";
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
    $channel->wait();
}
$channel->close();
$connection->close();

测试结果如图:
直接绑定测试结果

Topics主题

虽然使用直接交换改进了我们的系统,但它仍然有局限性 - 它不能基于多个标准进行路由。

我们需要了解更复杂的主题交换

主题交换

发送到主题交换的消息不能具有任意 routing_key - 它必须是由点分隔的单词列表。单词可以是任何内容,但通常它们指定与消息相关的一些功能。一些有效的路由密钥示例:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。路由密钥中可以包含任意数量的单词,最多可达255个字节。

绑定密钥也必须采用相同的形式。主题交换背后的逻辑 类似于直接交换- 使用特定路由密钥发送的消息将被传递到与匹配绑定密钥绑定的所有队列。但是,绑定键有两个重要的特殊情况:
- *(星号)可以替代一个单词。
- #(hash)可以替换零个或多个单词。

在一个例子中解释这个是最容易的:
主题交换

我们创建了三个绑定:Q1绑定了绑定键"* .orange.", Q2 绑定了".*.rabbit"和"lazy.#"。

这些绑定可以概括为:
- Q1对所有橙色动物感兴趣。
- Q2希望听到关于兔子的一切,以及关于懒惰动物的一切。

路由密钥设置为"quick.orange.rabbit"的消息将传递到两个队列。
消息"lazy.orange.elephant"也将同时发送给他们。
另一方面,"quick.orange.fox"只会进入第一个队列,而"lazy.brown.fox"只会进入第二个队列。
"lazy.pink.rabbit"将仅传递到第二个队列一次,即使它匹配两个绑定。
"quick.brown.fox"与任何绑定都不匹配,因此它将被丢弃。

如果我们违反规则并发送带有一个或四个单词的消息,例如"orange"或"quick.orange.male.rabbit",会发生什么?好吧,这些消息将不匹配任何绑定,将丢失。

另一方面,"lazy.orange.male.rabbit",即使它有四个单词,也会匹配最后一个绑定,并将被传递到第二个队列。

主题交换
主题交换功能强大,可以像其他交换器一样。

当队列与"#"(哈希)绑定密钥绑定时 - 它将接收所有消息,而不管路由密钥 - 如扇出交换。

当特殊字符"*"(星号)和"#"(哈希)未在绑定中使用时,主题交换的行为就像直接交换一样。

完整代码

emit_log_topic.php的代码:

<?php
require_once __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->exchange_declare('topic_logs', 'topic', false, false, false);
$routing_key = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'anonymous.info';
$data = implode(' ', array_slice($argv, 2));
if (empty($data)) {
    $data = "Hello World!";
}
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'topic_logs', $routing_key);
echo ' [x] Sent ', $routing_key, ':', $data, "\n";
$channel->close();
$connection->close();

receive_logs_topic.php的代码:

<?php
require_once __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->exchange_declare('topic_logs', 'topic', false, false, false);
list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);
$binding_keys = array_slice($argv, 1);
if (empty($binding_keys)) {
    echo '缺少安全级别参数', "\n";
    exit(1);
}
foreach ($binding_keys as $binding_key) {
    $channel->queue_bind($queue_name, 'topic_logs', $binding_key);
}
echo " [*] Waiting for logs. To exit press CTRL+C\n";
$callback = function ($msg) {
    echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n";
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
    $channel->wait();
}
$channel->close();
$connection->close();

测试结果如图:
主题交换测试

rpc远程过程调用

php的几种运行模式CLI、CGI、FastCGI、mod_php

常用运行模式

CLI

CLI:命令行,可以在控制台或者shell中输入命令获取输出,没有header头信息

CGI

公共网关接口(Common Gateway Interface/CGI):是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。即web server将用户请求以消息的方式转交给PHP独立进程,PHP与web服务之间无从属关系。

FastCGI

快速通用网关接口(Fast Common Gateway Interface/FastCGI):是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。

CGI 和 FastCGI 是一种通信协议规范,不是一个实体

CGI 程序和FastCGI程序,是指实现这两个协议的程序,可以是任何语言实现这个协议的。(PHP-CGI 和 PHP-FPM就是实现FastCGI的程序)

FastCGI和CGI的区别

  • CGI每接收一个请求就要fork一个进程处理,只能接收一个请求作出一个响应。请求结束后该进程就会结束。
  • FastCGI会事先启动起来,作为一个cgi的管理服务器存在,预先启动一系列的子进程来等待处理,然后等待web服务器发过来的请求,一旦接受到请求就交由子进程处理,这样由于不需要在接受到请求后启动cgi,会快很多。
  • FastCGI使用进程/线程池来处理一连串的请求。这些进程/线程由FastCGI服务器管理,而不是Web服务器。 当进来一个请求时,Web服务器把环境变量和这个页面请求通过一个Socket长连接传递给FastCGI进程。FastCGI像是一个常驻型的CGI,它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI对位人诟病的fork-and-execute模式)。正是因为它只是一个通信协议,它还支持分布式的运算,即FastCGI程序可以在网站服务器以外的主机上执行并且接受来自其他网站服务器的请求。

FastCGI整个流程

  1. Web server启动时载入FastCGI进程管理器
  2. FastCGI自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web server的请求
  3. 当请求Web server时,Web server通过socket请求FastCGI进程管理器,FastCGI进程管理器选择并连接到一个CGI解释器,Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
  4. FastCGI子进程处理请求完成后将标准输出和错误从同一连接返回给Web server,当FastCGI子进程结束后请求便结束。FastCGI子进程接着等待处理来自FastCGI进程管理器的下一个连接,在CGI模式中,php-cgi在此便退出了。

PHP-FPM:PHP的FastCGI进程管理器

PHP-CGI 和 PHP-FPM的区别

php-cgi与php-fpm一样,也是一个fastcgi进程管理器

php-cgi的问题在于:
- php-cgi变更php.ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
- 直接杀死php-cgi进程,php就不能运行了。

PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程。针对php-cgi的不足,php-fpm应运而生。

PHP-FPM 的管理对象是php-cgi。使用PHP-FPM来控制PHP-CGI的FastCGI进程

mod_php(传统模式)

apache的php模块,将PHP做为web-server的子进程控制,两者之间有从属关系。

最明显的例子就是在CGI模式下,如果修改了PHP.INI的配置文件,不用重启web服务便可生效,而模块模式下则需要重启web服务。

以mod_php模式运行PHP,意味着php是作为apache的一个模块来启动的,因此只有在apache启动的时候会读取php.ini配置文件并加载扩展模块,在apache运行期间是不会再去读取和加载扩展模块的。如果修改php的配置,需要重启apache服务

Apache的工作模式 prefork的工作原理

一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答。

Apache总是试图保持一些备用的 (spare)或是空闲的子进程用于迎接即将到来的请求。这样客户端就无需在得到服务前等候子进程的产生。

在Unix系统中,父进程通常以root身份运行以便邦定80端口,而 Apache产生的子进程通常以一个低特权的用户运行。User和Group指令用于配置子进程的低特权用户。运行子进程的用户必须要对他所服务的内容有读取的权限,但是对服务内容之外的其他资源必须拥有尽可能少的权限。

Apache的工作模式 worker的工作原理

每个进程能够拥有的线程数量是固定的。服务器会根据负载情况增加或减少进程数量。

一个单独的控制进程(父进程)负责子进程的建立。每个子进程能够建立ThreadsPerChild数量的服务线程和一个监听线程,该监听线程监听接入请求并将其传递给服务线程处理和应答。

nginx默认是使用的fastcgi模式,可以配合fpm使用

WebSocket入门

为什么需要WebSocket?

虽然有HTTP协议,但是一个很明显的缺点是:所有请求只能有客户端发起,向服务端请求。而服务端有任何状态变化,无法直接通知到客户端。简单处理的方法就是轮询,连续不断发起请求,但是这个非常浪费资源,因为需要不断请求连接。最常见的例子就是聊天室。

WebSocket

优点:
- 支持双向通信,实时性更强
- 更好的支持二进制
- 较少的控制开销,数据交换时,数据包请求头较小
- 支持更多扩展

websocket也是通过http请求去建立连接,请求格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

跟普通http请求的区别:
- GET请求的地址不是类似/path/,而是以ws://开头的地址
- 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接
- Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据
- Sec-WebSocket-Version指定了WebSocket的协议版本
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

服务器返回数据:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议

成功建立连接后,客户端和服务端就可以直接主动发消息给对方。消息传递的格式有两种:文本,二进制数据.通常可以发送JSON数据,方便处理

WebSocket对象

var ws = new WebSokcet(url, [protocol])

const ws = new WebSocket('ws://echo.websocket.org', ['myProtocol1', 'myProtocol2'])

WebSocket 构造函数可接受两个参数,其中,第一个参数必须是以 ws://wss:// 开头的完全限定的 URL
第二个为非必要参数,用于指定可接受的子协议,有两种可能的类型:
- String 类型,值为客户端和服务器端均能理解的协议
- Arrary 类型,包含一组客户端支持的协议(String 类型)

WebSocket 属性

Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:
- 0 | WebSocket.CONNECTING:表示连接尚未建立
- 1 | WebSocket.OPEN:表示连接已经建立
- 2 | WebSocket.CLOSEING:表示连接正在关闭
- 3 | WebSocket.CLOSED: 表示连接已经关闭或者连接不能打开

bufferedAmount

WebSocket 对象的 bufferedAmount 属性可以用检查已经进入发送队列,但是还未发送到服务器的字节数。可以用来判断发送是否结束

protocol

WebSocket 对象的 protocol 属性值为 WebSocket 打开连接握手期间,服务器端所选择的协议名

protocol 属性在最初的握手完成之前为空,如果服务器没有选择客户端提供的某个协议,则该属性保持空值

WebSockets事件处理

WebSocket 对象具有以下 4 个事件:

open 事件

当服务器响应了 WebSocket 连接请求,触发open事件并建立一个连接,此时WebSocket已经准备好发送和接收数据,open事件对应的回调函数是onopen()

ws.onopen = (event) => {
    console.log('开启连接');
}

// 或者
ws.addEventListener('open', (event) => {
    console.log('开启连接');
}, false)

message事件

message事件在接收到消息是触发,消息内容存储在事件对象eventdata中,对应的回调函数是onmessage()

ws.onmessage = (event) => {
    if (typeof event.data === 'string') {
        console.log('接收到的string消息内容为:' + event.data)
    } else {
        console.log('其他类型消息')
    }
}

除了普通文件,WebSocket消息内容还可以是二进制,这种数据作为Blob消息或者ArraryBuffer消息处理。暂不赘述。

error事件

error事件在响应意外发生故障时触发,对应的回调函数是onerror()。错误会导致WebSocket连接关闭。

close事件

close事件在连接关闭时触发,对应的回调函数是onclose()。一旦连接关闭,客户端和服务器端不在接续接收和发送消息。
close事件的3个常用属性:
- wasClean:布尔值,表示连接是否被正确关闭。如果是来自服务器的close帧的响应,则为true;如果是因为其他原因关闭,则为false
- code:服务器发送的关闭连接握手状态码
- reason:服务器发送的关闭连接握手状态

WebSocket方法

WebSocket API提供两个方法供调用。

send()

使用send()方法可以从客户端向服务端发送消息。前提是必须当WebSocket在客户端和服务端建立全双工双向连接后,才可以调用该方法。所以一般是在open事件触发之后,close触发之前调用send()发送消息

ws.onopen = (event) {
    ws.send('hello websocket');
}

close()

通过使用close()方法,可以人为的手动关闭WebSocket连接或者终止连接尝试。如果连接已关闭,则该方法什么也不做

可以向close()方法传递两个参数:
- code:Number类型,状态代码
- reason: String类型,文本字符串,传递一些关于关闭连接的信息

参考链接:
《WebSocket 教程 - 阮一峰》
《WebSocket客户端编程》

docker入门篇

阅读《Docker — 从入门到实践》

windows使用

  • 下载docker for windows,安装
  • 使用镜像加速,我自己使用的是阿里云镜像加速setting->Daemon->Registry mirrors->粘贴自己的加速器地址
  • 可以使用gui界面,Kitematic

常用命令

获取镜像

可以使用docker pull --help查看

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

# etc
docker pull ubuntu:16.04

运行

docker run -it --rm ubuntu:16.04 bash

# 查看当前系统版本
cat /etc/os-release
  • -it: -i: 交互式操作, -t: 终端
  • --rm: 执行退出后删除该容器实例
  • bash: shell使用的方式

镜像列表

docker image ls

# etc
REPOSITORY TAG IMAGE ID CREATED SIZE

各列意义:仓库名, 标签, 镜像id, 创建时间, 解压后文件大小
镜像ID 则是镜像的唯一标识,一个镜像可以对应多个标签

镜像大小

docker system df

删除镜像

docker image rm [选项] <镜像1> [<镜像2> ...]

启动已终止的container镜像

docker [container] start 

后台运行容器

-d参数能够让容器后台运行,保证输出结果不会打印出来。但容器是否长久运行(一直后台挂起),跟-d参数无关,需要一直有指令执行,才不会“秒退”

# -c exec执行
docker run ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"

# 后台运行
docker run -d ubuntu:17.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"

可以通过docker [container] logs去查看容器输出信息

终止运行

docker [contanier] stop

进入容器

docker attach  

# 推荐使用
docker exec -it [container id] /bin/bash

两者的区别:前者exit退出后,会停止当前容器;而exec仍然会保持运行

删除容器

docker container rm  

# 清楚所有处于终止状态的容器
docker container prune

commit

为什么不建议使用docker commit?
使用docker commit提交后,对于其他使用者而言,这个image镜像是一个黑箱,别人无处得知执行过什么命令、如何生成的镜像

Dokcerfile 定制镜像

FROM指定基础镜像

RUN执行命令

  • shell格式:RUN <命令>
  • exec格式:RUN ["可执行文件", "参数1", "参数2"]

构建镜像

docker build [选项] <上下文路径/URL/->

# etc, 注意镜像构建上下文(context)
docker build -t nginx:v3 .

Dockerfile 指令详解

CMD 启动命令

CMD 指令就是用于指定默认的容器主进程的启动命令的,也分为shell格式以及exec格式
- shell格式:CMD <命令>
- exec格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数

一般推荐使用exec格式,这类格式在解析时会被解析成JSON数组,因此要使用双引号

如果是shell格式,实际会被包装成sh -c的格式

CMD echo $HOME

实际执行会变成:

CMD [ "sh", "-c", "echo $HOME" ]

注意:Docker不是虚拟机,容器中的应用都是前台执行,没有后台服务的概念

错误示范:

CMD service nginx start

这里容器执行后会秒退出,因为上面的命令会被转化为CMD ["sh", "-c", "service nginx start],主进程是sh,当service nginx start执行结束后,sh也就结束了,sh作为主进程结束,所以容器也会退出

正确做法:

CMD [ "nginx", "-g", "daemon off;" ]

这边执行docker run的时候,不需要再跟/bin/bash启动命令,因为会覆盖。否则就是秒结束进程

ENTRYPOINT 入口点

如果指定了ENTRYPOINTCMD就不会直接执行命令,而是讲内容作为参数传给ENTRYPOINT,实际执行指令会变为:

ENTRYPOINT "<CMD>"

场景一:让镜像变成像命令一样使用

docker run myip -i

场景二:应用运行前的准备工作

在启动主进程之前,需要一些准备工作,比如数据库的配置、初始化

ENV 设置环境变量

两种格式:
- ENV \ \
- ENV \=\ \=\ ...

# 含有空格的值使用双引号
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

ARG构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和ENV效果一样,都是设置环境变量,唯一的区别是,ARG构建的环境变量,在将来容器运行的时候,不会存储这些环境变量

Dokcerfile中的ARG指令是定义参数名称,以及其默认值。可以通过构建命令docker build中用--build-arg <参数名>=<值>来覆盖

VOLUME 定义匿名卷

格式为:
- VOLUME ["<路径1>", "<路径2>"...]
- VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化

docker run -d -v mydata:/data xxxx

这里mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置

EXPOSE 声明端口

格式为:EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
- 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为: WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录

USER 指定当前用户

格式:USER <用户名>

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份

HEALTHCHECK 健康检查

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常

格式:
- HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
- HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

ONBUILD

格式:ONBUILD <其它指令>

NOBUILD指令是别人定制镜像。即使用FROM的时候,才执行的命令

推送镜像

# 先打标签
docker tag ubuntu:17.10 username/ubuntu:17.10

# 在push
docker push username/ubuntu:17.10

配置私有仓库

配置私有仓库

数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录:
- 数据卷可以在容器之间共享和重用
- 对 数据卷 的修改会立马生效
- 对 数据卷 的更新,不会影响镜像
- 数据卷 默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。

外部访问容器

使用- P标记时,Docker会随机映射 49000~49900 的端口到内部容器开放的网络端口

- p可以指定要映射的端口,也可以指定地址
ip:hostPort:containerPort

docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

查看映射端口配置

docker port

docker port nostalgic_morse 5000
  • 容器有自己的内部网络和 ip 地址
  • -p 标记可以多次使用来绑定多个端口

容器互联

查看已有网络

docker network ls

新建网络

docker network create -d bridge my-net

-d 可以指定Docker网络类型,有bridge, overlay,其中 overlay 网络类型用于 Swarm mode(集群服务)

连接容器

运行一个容器并连接到新建的 my-net 网络

docker run -it --rm --name busybox1 --network my-net busybox sh

# 再运行一个容器
docker run -it --rm --name busybox2 --network my-net busybox sh

# 测试连接
# 在busybox1 容器里,执行
# /ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.060 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.046 ms
64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.075 ms

Compose 项目

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose 中有两个重要的概念:
- 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
- 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 命令说明

命令对象与格式

docker-compose 命令的基本的使用格式是:

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项

  • -f, --file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
  • -p, --project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。
  • --x-networking 使用 Docker 的可拔插网络后端特性
  • --x-network-driver DRIVER 指定网络后端的驱动,默认为 bridge
  • --verbose 输出更多调试信息。
  • -v, --version 打印版本并退出

命令使用说明

Tips: 这里的service name是指服务的名称,不是container name 或者 container id

build

构建(重新构建)项目中的服务容器

docker-compose build [options] [SERVICE...]

服务容器一旦构建后,将会带上一个标记名,例如对于 web 项目中的一个 db 容器,可能是 web_db
选项包括:
- --force-rm 删除构建过程中的临时容器
- --no-cache 构建镜像过程中不使用 cache(这将加长构建过程)
- --pull 始终尝试通过 pull 来获取更新版本的镜像

config

验证 Compose 文件格式是否正确,若正确则显示配置,若格式错误显示错误原因

down

此命令将会停止 up 命令所启动的容器,并移除网络

exec

进入指定的容器

# 如果执行/bin/bash失败,报错OCI runtime exec failed,是因为bash不存在,替换成sh    

docker-compose exec web /bin/sh

images

列出 Compose 文件中包含的镜像

kill

格式为 docker-compose kill [options] [SERVICE...]

通过发送 SIGKILL 信号来强制停止服务容器

支持通过 -s 参数来指定发送的信号,例如通过如下指令发送 SIGINT 信号

 docker-compose kill -s SIGINT

logs

查看服务容器的输出。
默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color 来关闭颜色。

格式为:docker-compose logs [options] [SERVICE...]

pause

暂停服务
格式为:docker-compose pause [SERVICE...]

port

打印某个容器端口所映射的公共端口

格式为 docker-compose port [options] SERVICE PRIVATE_PORT

选项:
- --protocol=proto 指定端口协议,tcp(默认值)或者 udp。
- --index=index 如果同一服务存在多个容器,指定命令对象容器的序号(默认为 1)

ps

pull

push

restart

stop

rm

run

格式为 docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]

docker-compose run ubuntu ping docker.com

默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中。

该命令类似启动容器后运行指定的命令,相关卷、链接等等都将会按照配置自动创建。

两个不同点:

给定命令将会覆盖原有的自动运行命令;

不会自动创建端口,以避免冲突。

如果不希望自动启动关联的容器,可以使用 --no-deps 选项,例如:

docker-compose run --no-deps web python manage.py shell

将不会启动 web 容器所关联的其它容器.

选项:
- -d 后台运行容器。
- --name NAME 为容器指定一个名字。
- --entrypoint CMD 覆盖默认的容器启动指令。
- -e KEY=VAL 设置环境变量值,可多次使用选项来设置多个环境变量。
- -u, --user="" 指定运行容器的用户名或者 uid。
- --no-deps 不自动启动关联的服务容器。
- --rm 运行命令后自动删除容器,d 模式下将忽略。
- -p, --publish=[] 映射容器端口到本地主机。
- --service-ports 配置服务端口并映射到本地主机。
- -T 不分配伪 tty,意味着依赖 tty 的指令将无法运行。

scale

设置指定服务运行的容器个数

格式为 docker-compose scale [options] [SERVICE=NUM...]

通过 service=num 的参数来设置数量。例如:

docker-compose scale web=3 db=2

一般的,当指定数目多于该服务当前实际运行容器,将新创建并启动容器;反之,将停止容器。

top

unpause

启动已暂停的服务

up

格式为 docker-compose up [options] [SERVICE...]

该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作

链接的服务都将会被自动启动,除非已经处于运行状态

默认情况,docker-compose up 启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试

如果使用 docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。

选项:
- -d 在后台运行服务容器。
- --no-color 不使用颜色来区分不同的服务的控制台输出。
- --no-deps 不启动服务所链接的容器。
- --force-recreate 强制重新创建容器,不能与 --no-recreate 同时使用。
- --no-recreate 如果容器已经存在了,则不重新创建,不能与 --force-recreate 同时使用。
- --no-build 不自动构建缺失的服务镜像。
- -t, --timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)

Compose 模板文件

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟 docker run 相关参数的含义都是类似的。

默认的模板文件名称为 docker-compose.yml,格式为 YAML 格式。

version: '3'
services:

  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

注意每个服务都必须通过 image 指令指定镜像或 build 指令(需要 Dockerfile)等来自动构建生成镜像

如果使用 build 指令,在 Dockerfile 中设置的选项(例如:CMD, EXPOSE, VOLUME, ENV 等) 将会自动被获取,无需在 docker-compose.yml 中再次设置。

build

指定 Dockerfile 所在文件夹的路径(可以是绝对路径,或者相对 docker-compose.yml 文件的路径)。 Compose 将会利用它自动构建这个镜像,然后使用这个镜像。

可以使用 context 指令指定 Dockerfile 所在文件夹的路径

使用 dockerfile 指令指定 Dockerfile 文件名

使用 arg 指令指定构建镜像时的变量

使用 cache_from 指定构建镜像的缓存

build:
  context: .
  cache_from:
    - alpine:latest
    - corp/web_app:3.14

cap_add, cap_drop

指定容器的内核能力(capacity)分配

command

覆盖容器启动后默认执行的命令

command: echo "hello world"

container_name

指定容器名称。默认将会使用 项目名称_服务名称_序号 这样的格式

container_name: docker-web-container

注意: 指定容器名称后,该服务将无法进行扩展(scale),因为 Docker 不允许多个容器具有相同的名称

devices

指定设备映射关系。

devices:
  - "/dev/ttyUSB1:/dev/ttyUSB0"

depends_on

解决容器的依赖、启动先后的问题。
以下例子中会先启动 redis db 再启动 web

version: '3'

services:
  web:
    build: .
    depends_on:
      - db
      - redis

  redis:
    image: redis

  db:
    image: postgres

注意:web 服务不会等待 redis db 「完全启动」之后才启动。

dns

自定义 DNS 服务器。可以是一个值,也可以是一个列表。

dns: 8.8.8.8

dns:
  - 8.8.8.8
  - 114.114.114.114

env_file

从文件中获取环境变量,可以为单独的文件路径或列表。

如果通过 docker-compose -f FILE 方式来指定 Compose 模板文件,则 env_file 中变量的路径会基于模板文件路径

如果有变量名称与 environment 指令冲突,则按照惯例,以后者为准

env_file: .env

env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

环境变量文件中每一行必须符合格式,支持 # 开头的注释行。

# common.env: Set development environment
PROG_ENV=development

environment

设置环境变量。可以使用数组或字典两种格式

只给定名称的变量会自动获取运行 Compose 主机上对应变量的值,可以用来防止泄露不必要的数据

environment:
  RACK_ENV: development
  SESSION_SECRET:

environment:
  - RACK_ENV=development
  - SESSION_SECRET

如果变量名称或者值中用到 true|false,yes|no 等表达 布尔 含义的词汇,最好放到引号里,避免 YAML 自动解析某些内容为对应的布尔语义。

expose

暴露端口,但不映射到宿主机,只被连接的服务访问

仅可以指定内部端口为参数

expose:
 - "3000"
 - "8000"

image

指定为镜像名称或镜像 ID

读取变量

Compose 模板文件支持动态读取主机的系统环境变量和当前目录下的 .env 文件中的变量。

例如,下面的 Compose 文件将从运行它的环境中读取变量 ${MONGO_VERSION} 的值,并写入执行的指令中。

version: "3"
services:

db:
  image: "mongo:${MONGO_VERSION}"

如果执行 MONGO_VERSION=3.2 docker-compose up 则会启动一个 mongo:3.2 镜像的容器;如果执行 MONGO_VERSION=2.8 docker-compose up 则会启动一个 mongo:2.8 镜像的容器。

若当前目录存在 .env 文件,执行 docker-compose 命令时将从该文件中读取变量。

在当前目录新建 .env 文件并写入以下内容。

# 支持 # 号注释
MONGO_VERSION=3.6

执行 docker-compose up 则会启动一个 mongo:3.6 镜像的容器。

附录

常见问题总结

常见问题总结

资源链接

资源链接

进阶深入

进阶深入,参考原文档《Docker — 从入门到实践》

js和php闭包的使用和区别

匿名函数

如果只是省去函数名,单纯的当作一个函数式方法返回,只能称为匿名函数(闭包需要将匿名函数当作结果返回),比如:

// js
var foo = function(x, y) {
    return x + y ;
};
console.log(foo(1, 2));  // 3
<?php
// php
$foo = function($a, $b) {
    return $a + $b;
}; // 一定要加分号
echo $foo(1, 2); // 3

闭包

闭包通常是用来创建内部变量,使得这些变量不得被外部随意修改,而只能通过指定的函数接口去修改

js闭包

这里举一个阮老师博客里的例子,阮老师博客:学习Javascript闭包(Closure)

js基础

参考链接:深入理解JS中声明提升、作用域(链)和this关键字

  • js比较特殊的一点是:函数内部可以直接读取到全局变量(对于阮老师的这句话不是很能理解,大概是想表达的意思:父作用域的变量可以在子作用域直接访问,而不需要去声明访问真正的全局变量?)
    • 大部分语言,变量都是先声明在使用,而对于js,具有声明提升的特性(不管在哪里声明,都会在代码执行前处理)
    • 函数和变量的声明总是会隐式地被移动到当前作用域的顶部,函数的声明优先级高于变量的声明
    • var 会在当前作用域声明一个变量,而未声明的变量,会隐式地创建一个全局变量
// 声明提升
console.log(a);  // 1, 未报错
var a = 1;
// 上文链接中的例子
function testOrder(arg) {
    console.log(arg); // arg是形参,不会被重新定义
    console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数
    var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行
    var a = 10; // var a;被忽视; a = 10被执行,a变成number
    function a() {
        console.log('fun');
    } // 被提升到作用域顶部
    console.log(a); // 输出10
    console.log(arg); // 输出hello
}; 
testOrder('hi');
/* 输出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/
// 全局作用域
var foo = 42;
function test() {
    // 局部作用域
    foo = 21;
}
test();
foo; // 21
// 全局作用域
foo = 42;
function test() {
    // 局部作用域
    var foo = 21;
}
test();
foo; // 42
  • js变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所有内部函数可以访问函数外部的变量,反之无效
function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2);
function foo() {
    var a = 1;
}
console.log(a);  //a is not defined
function foo1() {
    var num = 0;
    addNum = function() {  // 这里未通过var去声明,默认是全局变量
        num += 1;
    };
    function foo2() {
        console.log(num);
    }
    return foo2;
}
var tmp = foo1();
tmp();  // 0

addNum();
tmp(); // 1

这里第二次调用foo2函数,foo1函数的局部变量num并没有被初始化为0,说明打印的是内存中的num。正常函数在每次调用结束后都会销毁局部变量,在重新调用的时候会再次声明变量;而这边没有重新声明的原因是:把foo2函数赋值给了一个全局变量tmp,导致foo2函数一直存在内存中,而foo2函数依赖于foo1函数存在,所以foo1函数也存在内存中,并没有被销毁,所以foo1的局部变量也是存在内存中。

  • this的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。
    • 在全局上下文(任何函数以外),this指向全局对象(windows)
    • 在函数内部时,this由函数怎么调用来确定
      • 当函数作为对象方法调用时,this指向该对象

下面是阮老师博客里的两个思考题:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    return function(){
      return this.name;
    };
  }
};
alert(object.getNameFunc()()); // The Window
var name = "The Window";
var object = {
    name : "My Object",
  getNameFunc : function(){
      var that = this;
    return function(){
        return that.name;
    };
  }
};
alert(object.getNameFunc()());// My Object

this的作用域好像一直是个比较奇怪的东西,对于上面两个例子,我的理解是:第一个例子,是在方法里调用的this,而这个this并没有声明,会隐式地创建一个全局变量,所以调用的全局的name;第二个,调用的that的时候,会向顶级链式查找是否声明that,而这个that有this赋值,这里的this又是通过对象方法调用,则该this指向这个object对象,所有最终调用的是object作用域内的name。不知道这么理解是不是有问题,还望大神指正。

那其实js闭包的主要目的:访问函数内部的局部变量,即延长作用域链
参考链接:js闭包MDN文档

php闭包

php回调函数

mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )
mixed call_user_func_array ( callable $callback , array $param_arr )

这两个函数都是把第一个参数作为回调函数d调用,后面接收参数,区别就是第二个函数第二参数接收数组;在使用上唯一的区别就是,call_user_func函数无法接收引用传递; 个人觉得同样是使用,call_user_func 相比call_user_func_array完全可以被替代,不知道是不是有一些性能上的优势。具体使用样例,请参考官方文档。

<?php
// 引用传递
function foo(&$a, &$b) {
    $a ++;
    $b --;
};
$a = $b = 10;
call_user_func_array('foo', [&$a, &$b]);
echo $a."\n", $b; // 11, 9
基本用法

基本用法,跟js的闭包类似
- 普通调用

<?php
global $tmp = 'hello world';
function foo() {
    var_dump(global $tmp);
}
foo(); // null, 函数内部无法直接调用上级作用域的变量,除非声明为全局变量
<?php
$foo1 = function() {
    $a = 10;
    $foo2 = function() {
        var_dump($a);
    };
    return $foo2;
};
$tmp = $foo1();
$tmp();  // null,原因同上 
  • php想要能够获取上级作用域的变量,需要通过use传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 10,  use并不能实际改变变量的值,只是值传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 11,  通过值传递改变变量的值
  • 下面两段代码的区别,不是很明白,望大佬指点,为什么后一个值传递就可以获取到已经改变后变量的值。好像都是在调用方法之前,已经执行过变量的递增了吧?
<?php
// 值传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10
<?php
// 引用传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 11
  • 正确使用
<?php
// 值传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use ($a) {
        var_dump($num + $a);
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
<?php
// 引用传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use (&$a) {
        var_dump($num + $a);
        $a ++;
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
$tmp(100); // 111
$tmp(100); // 112  跟js类似,保证变量常驻内存
php Closure 类

共同点

都是为了扩展作用域,获取内部变量

区别

js能够在方法内部直接获取到父级作用域的变量,而php需要通过use声明,并且默认是值传递

应用场景

  • 不是很能理解应用场景,搜索了一下,很多只是写了一个闭包实现的购物车,感觉并不是那么的实用。
  • 如果只是单纯的使用匿名函数,感觉还不如封装成一个私有方法
    >这些只是个人粗鄙的理解,望指正.

phpstorm或vscode使用psr2规范

  • 安装composer
  • 全局安装phpcs
composer global require squizlabs/php_codesniffer

vscode直接插件搜索phpcs安装

phpstorm

全局安装phpcs后,会在C:\Users{user name}\AppData\Roaming\Composer\vendor\bin下生成一个phpcs.bat,后面会用到
- phpstorm -> setting
- languages & Frameworks->PHP->Code Sniffer点击Configuration右侧的按钮
- 找到刚才的phpcs.bat,点击Validate,确认
- Editor->Inspection->PHP
- 双击PHP Code Sniffer validation,点击Coding standard右侧的刷新按钮,然后选择psr2,确定

参考链接: 如何优雅地使用phpstorm?

移动端列表页操作优化

详情见ppt
移动端列表页操作优化
旧项目代码
code

大纲如下
方案一:PHP + ajax方式
tab切换使用PHP判断
第一页数据PHP获取,输出
当前页面滚动到底部,ajax加载后续数据

方案二:纯ajax方式
tab切换也使用加载
默认加载第一页数据
滚动到屏幕底部,加载下一页

方案三:本地存储缓存列表
同纯ajax加载方式
点击详情页的时候,保存当前状态
屏幕滚动位置
当前列表页数据
当前页码
初始化时,如果本地存储有数据
恢复相应数据(列表页、滚动位置、页码等)
删除本地存储数据

对比总结

优化方案
纯ajax处理
每次ajax请求的时候,将请求参数附加到当前URL上
window.location.replaceState
点击内页,本地存储中存储当前滚动条滚动位置
初始化的时候,读取url中的请求参数进行请求
page如果不是第一页,则合并请求,修改limit
读取本地存储中的滚动位置,js滚动

常见JS写法问题
PHP和JS代码混合
列表页数据统一,不要一部分PHP,一部分JS
在JS中,到处有PHP输出的内容
污染全局,各种变量,函数定义
到处有监听事件的代码,同一个事件可能都有几个地方
JS使用样式定义的class或者ID
代码耦合严重,全都堆积在一起
JS文件未压缩

如何使用CasperJs抓取商品数据

标签: 爬虫 casperjs phantomjs


编 写:袁 亮
时 间:2016-08-30
说 明:如何使用CasperJs抓取商品数据

一、目的

抓取天猫、淘宝、京东的相应商品数据
抓取商品名、价格、轮播图、详情、产品属性等数据

二、方案选择

1、情况分析
    1.1 对应的页面数据很多都是后加载的
    1.2 页面请求很多,想找到其对应的ajax请求比较麻烦
    1.3 ajax请求的地址是服务端生成的,不能通过简单的拼接来生成
    1.4 对正则水平要求较高,很多请求地址是写在js里的

2、可选方案
    2.1 php等语言,直接发钱http请求,然后通过正则匹配等方式,找到响应数据源
        curl、phpquery、Snopy等等 
    2.2 使用浏览器实际访问,然后获取浏览器的最终结果数据
        phantomJS
        casperJS + phantomJS
        python + selenium + phantomJS
3、选用方案 casperJS + phantomJS
    3.1 纯JS,方便前后端使用
    3.2 Casper封装的比较友好、文档齐全,比较好用
        特别是针对后加载的元素,之间waitForSelector的方法非常方便
        可以引入jquery等自己的js文件,方便dom操作
    3.3 获取数据,只需要在浏览器打开的时候,找到对应的节点选择器即可
    3.4 安装部署方便

三、简单例子

var casper = require('casper').create();
casper.start('http://casperjs.org/', function() {
    this.echo(this.getTitle());
});

casper.thenOpen('http://phantomjs.org', function() {
    this.echo(this.getTitle());
});
casper.run();

四、环境部署 (非源码安装)

1、安装phantomjs
    1.1 下载安装
        wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
    1.2 解压
        tar jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
    1.3 做软连接,放在环境变量里,比如(注意权限问题,如果需要apache调用,更要注意)
        ln -s /opt/ci123/phantomjs/bin/phantomjs /usr/local/sbin/
    1.4 测试运行
        phantomjs --version

2、安装casperjs
    2.1 选择相应版本下载
        wget https://github.com/casperjs/casperjs/archive/1.1.3.zip
    2.2 解压
        unzip 1.1.3.zip 
    2.3 做软连接,放在环境变量里,比如(注意权限问题,如果需要apache调用,更要注意)
        ln -s /opt/ci123/casper/bin/casperjs /usr/bin/casperjs
    2.4 测试是否正常
        casperjs --version

五、实际使用

1、apache调用php
2、php根据url调用不同的CasperJS,传入要抓取的url
3、Casper输出json数据
4、php使用exec调用,并获取casper的输出
5、php将相应的json输出返回apache或者存入数据库等等
演示代码:
    192.168.0.249 
    /opt/ci123/www/html/yuanliang/casper/fetch

六、踩过的坑

1、抓取https的时候,249上一直失败
    原因:
        249上的ssl配置应该问题,导致抓取部分https网站的时候,一直报错
    解决办法:
        a:CasperJS需要较高版本才能用
            casperjs --ignore-ssl-errors=yes --ssl-protocol=any xxx.js
            phantomjs --ssl-protocol=any xxx.js
        b:升级249上的ca证书或者openssl版本

2、抓取速度太慢
    原因:
        这个工具本质是一个无界面的浏览器,因此里面的所有资源都跟浏览器一样,会去加载,图片等资源较多的时候会卡
    解决办法:
        设置不加载图片 loadImages:false
3、clientScripts 注入远程jquery文件无效
    原因:
        只能加载本地的js文件,不能加载远程的js文件
    解决办法:
        可以下载到本地,或者使用remoteScripts来加载,这个可以使用远程文件
        不过使用本地文件比较好,少一次http请求,速度会快很多
4、淘宝的后加载数据一直获取不到
    原因:
        之前https的出问题的时候,以为是ua有问题,所以设置了一个chrome的ua,但是实际上内核不一样
        导致淘宝本身的js执行失败,数据加载不出来
    解决办法:
        去掉ua设置即可
5、天猫的后加载数据(详情)一直加载不出来,报语法错误
    原因:
        天猫的代码中,如何屏幕大小超过1260,会执行一段JS,这段JS会出错
    解决办法:
        设置屏幕宽度小于1260即可
6、waitForSelector获取某个数据,超时,导致后面都不执行了
    原因:
        默认的timeout处理函数,会直接将整个进程中止,后面的所有代码都不执行
    解决办法:
        如果可以接受部分字段抓取不到,则可以在waitForSelector中自行设置timeout处理函数,不中止
        如果需要全部抓取才算结束,可以在timeout的时候触发一个报警等来通知
7、根据浏览器的元素定位,返回不了相应的数据
    原因:
        浏览器最终呈现的元素,有些跟phantomJs返回的不一样
        比如图片后加载,这是在页面滚动到一定位置之后才会触发的
    解决办法:
        如图片后加载这种,数据其实已经返回了,只是存在其他地方,可以返回对应的html,然后看数据存储在哪个字段上
        如果有些是点击、滚动等之后,从服务端获取的数据,那就需要模拟浏览器的点击滚动等事件,然后获取,比较少见
8、执行获取页面数据的时候,失败报错
    原因:
        页面代码执行在不同的沙箱中,在CasperJS本身是读取不到的
    解决办法:
        均在this.evaluate函数中执行,在内部可用操作所有的DOM,跟页面执行JS效果一样
9、天猫商品详情图片一直抓取不对
    原因:
        图片本身是后加载的,之前数据是放在data-ks-lazyload中,后加载执行之后,这个data被干掉了
        直接取src也会有问题,因为有的时候取数据的时候,后加载js还没执行到
        所以导致一会好一会坏
    解决办法:
        先取data-ks-lazyload,如果没有,则去src数据

七、注意事项

1、debug的时候,可用通过将当前页面截图出来,方便查看加载到什么情况了
    也可以将完整的html输出到文件里查看
2、打印json数据,方便跟php等交互
    打印数据
    casper.then(function(){
        require('utils').dump(data);
    });
3、注意很多异步操作,很多时候,执行效果会跟预期不一样
4、当Casper运行结果跟预期不一样的时候,可以考虑直接写相应的phantomJs做对应小块功能来调试
5、淘宝url等在命令行传参会出错,需要用单引号包含起来

八、参考资料

1、CasperJS 官方文档
    http://docs.casperjs.org/en/latest/index.html
2、phantomJS 文档
    http://phantomjs.org/documentation/
3、PhantomJS基础及示例
    http://imweb.io/topic/560b402ac2317a8c3e08621c
4、casperjs模拟登陆https页面获取当前面地址后发现为about:blank问题排查
    http://www.bubuko.com/infodetail-1018663.html
5、 Linux使用curl访问https站点时报错汇总
    http://www.ipcpu.com/2014/12/curl-https-error/

关于ip的那些事


编	写:袁	亮
时	间:2014-05-20
说	明:关于ip的那些事

一、ip传递过程
	1、【真实客户端】 ==> [多级代理服务器] ==> [CDN加速] ==> [前端代理nginx|squid] ==> 【apache】==> 【PHP】
	2、中文括号的是必然会经过的,英文括号是可能经过的
	3、标准的ip传递是REMOTE_ADDR和HTTP_X_FORWARDED_FOR,前一个跟当前服务连接的真实ip,后一个是请求到前一个ip之前,经过了哪些代理
	4、REMOTE_ADDR是不可伪造的,HTTP_X_FORWARDED_FOR是可以任意修改的
	5、按标准,每传递到下一层,都会将上一层的实际ip地址加入到HTTP_X_FORWARDED_FOR中,继续传递
	6、对每一层来说,只有上一层的时间地址是可信的(REMOTE_ADDR),HTTP_X_FORWARDED_FOR均有风险
	7、真实情况中,到了cdn或者前端代理之后,ip传递都是可信的(我们自己可控制),之前的都有篡改的危险

二、各服务的真实ip传递情况
	1、CDN 快网的cdn会将用户的实际地址或者代理服务器地址传递到后面的服务中
		$_SERVER["HTTP_USER_IP"]	【不用快网的时候可伪造】
		$_SERVER["HTTP_FW_ADDR"]	【不用快网的时候可伪造】
		测试了一个新的cdn测试,没有传递真实ip过来

	2、nginx代理的情况下,可以使用x_real_ip来获取真实ip(有cdn的时候,该值获取的是cdn的ip地址)
		$_SERVER["HTTP_X_REAL_IP"]	【不用nginx的时候可伪造】

	3、$_SERVER["HTTP_CLIENT_IP"] :代理服务器发送的客户端真实ip【可伪造】

三、现在使用的获取ip函数
	a、如果有HTTP_CLIENT_IP,则该ip为用户ip(可被伪造)
	b、如果有HTTP_X_FORWARDED_FOR,则将HTTP_CLIENT_IP也加入到HTTP_X_FORWARDED_FOR,判断HTTP_X_FORWARDED_FOR中的ip是否是内网的,取第一个非内网的ip为客户端真实ip
	c、经过以上两步还没有取到ip的话,则根据REMOTE_ADDR取用户的ip
	ps:该函数的问题在于,前面两个的ip都是可以被任意伪造改写,从而导致获取不到用户的真实ip情况

四、附:(线上使用的获取ip函数)
function getIp(){//获取IP函数
        $ip = false;
        if(!empty($_SERVER["HTTP_CLIENT_IP"])){
                $ip = $_SERVER["HTTP_CLIENT_IP"];
        }
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
                $ips = explode (", ", $_SERVER['HTTP_X_FORWARDED_FOR']);
                if ($ip) {
                        array_unshift($ips, $ip);
                        $ip = FALSE;
                }
                for ($i = 0; $i < count($ips); $i++) {
                        if (!preg_match("/^(10|172\.16|192\.168)\./", $ips[$i])) { // 判断是否内网的IP
                                $ip = $ips[$i];
                                break;
                        }
                }
        }
        return ($ip ? $ip : $_SERVER['REMOTE_ADDR']);
}



http 1.1 和 http1.0 主要区别


编	写:袁	亮
时	间:2016-01-12
说	明:http 1.1 和 http1.0 主要区别

一、持久链接keep-alive
	1、标准的1.0版本中,每次请求都必须重新建立连接、传输数据、关闭连接
		但有些http服务和浏览器也实现了Connection: Keep-Alive的功能
	2、在1.1版本,默认就是Connection: Keep-Alive
		可以在同一次TCP连接中,多次传输数据
		减少了重新建立连接的开销,特别是当一个网页中有很多图片、js、css等的时候会非常有用
		但这也有可能会导致TCP一直不释放,从而影响性能,需要权衡设置
		
二、增加了HOST
	1、在1.0版本中,不支持HOST,同一ip同一端口,只能供一个服务使用
	2、1.1版本中,支持HOST来创建虚拟主机

三、带宽优化
	1、1.1版本中,增加了RANGE头来实现断点续传,从而防止下载中断之后又要全部重新上传
	2、增加压缩,通过Accept-Encoding头来实现,减少数据传输

四、其他
	1、缓存策略
	2、新增http状态码
	3、身份认证、状态管理
	4、我们服务器上有些apache还是返回http 1.0,感觉不大正常
	
		
附:参考文档
1、http://blog.csdn.net/elifefly/article/details/3964766
2、http://www.cnblogs.com/qqzy168/p/3141849.html
3、http://www.cnblogs.com/huangfox/archive/2012/03/31/2426341.html
4、http://www.360doc.com/content/14/0730/09/1073512_398058058.shtml