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远程过程调用

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 — 从入门到实践》

Sublime入门

目录

文档

参考资料

激活码

插件

常见问题

  1. 如何更改Markdown背景颜色?
    选择配置文件:Preferences -> Package Settings -> Markdown Editing
    更改用户配置,粘贴到json配置中即可
    "color_scheme": "Packages/MarkdownEditing/MarkdownEditor-Dark.tmTheme",

  2. 汉化版Package Control无法使用?
    建议升级最新版

  3. 每次要手动保存,太麻烦?
    请下载auto-save插件

postman入门

目录

文档

常见问题

  1. 沙箱中如何使用jquery? 如 $.each?
    说明:jQuery已经不被推荐,取而代之的是cheerio。
    使用方法:$.each => _.forEach
    官方说明
    文档

  2. postman 设置格式,每次都要手动转换成json?
    说明:postman 返回格式是auto,会自动识别,但是效果不好,会把json当初字符串来解析。
    解决办法:在设置更改格式为json

  3. postman可以用来做啥?
    不仅仅可以用来测试接口,还可以做接口自动化测试,生成api文档。

  4. runner里执行的时候,一定要注意request的是要保存里的,不然更改了是无效的。send本身是直接起效

  5. 如何传递数据?
    键名设置为key[]
    array[] => 1
    array[] => 2

  6. 返回的数据有问题,怎么处理?

try {
    data = JSON.parse(responseBody);
} catch (err) {
    tests["返回错误格式"] = true;
}

调用链跟踪系统的首秀—查找打印配置未正确加载的bug

背景

问题

连续两天都有商家反馈无法打印,远程查看后都发现打印配置加载的有问题,以前设置好的打印机都没显示出来——显示的是 store 级别的打印配置(打印配置从store细化到entity_store后,如果请求没有entity_store_id,会去查找store级别的打印配置)。

思路

调用链跟踪系统,记录了每次api请求的url、params、返回数据、时间戳、ua、x-forwarded-for、埋点标记 等有用信息,足够我们用来回溯当时的程序运行路径,展示当时程序的部分状态。

操作

  1. 首先从庞大的日志里筛选出需要的所有调用链;
    1.1 根据商家反馈,早上从未成功打印过,我们先确定了一个较小的时间范围:从商家第一次登陆,到打印配置获取完成;
    1.2 我们先在记录里找商家store_id第一次出现的位置,再根据这条记录的ua找到了商家ip,根据此ip筛选出第一批数据,也找到了大概的时间范围;
    1.3 把所有记录复制出来,分组排序(按TraceId分组,组内按RpcId排序,组间按每组内第一条记录的At排序),方便分析;
    1.4 根据TraceId和RpcId补足中间缺失的记录(RpcId是有序的,中间缺失可以看出来)
    1.5 最终筛选出来106条记录,时间跨度约5秒,其中seller端的cash/get/lists接口产生了11个Api调用,相关记录就占了46条);
  2. 找到“获取打印配置”这个接口的请求参数和返回值,发现接口传参不正确,获取到的数据也不正确;
  3. 结合记录和代码,推测程序运行路径,找出问题的原因。

效果

最后找出来一个bug:

check接口和返回值:


PS: 接口没返回entity_store_id,后面却使用了entity_store_id去获取打印配置。

结语

  1. 最终张跃还在js端找出一个bug:打印配置的获取 写在了 setCookie方法 前面,导致获取配置的时候没有取到ntity_store_id(所以也没传)。
  2. 打印配置的获取有三个地方:打印js加载后自动获取、doLogin时获取、checkLogin时获取,所以出bug后也只有部分商户无法打印。
  3. 调用链跟踪记录查阅起来还不是很方便,主要问题有:
    3.1 商家没有固定的身份标识,在记录里难以找出所有的商家相关记录;
    3.2 日志记录无序,还需要自己排一遍序;
    3.3 如果能像数据库一样select筛选数据更好;
  4. 针对上面的问题,分别有一些解决方法:
    4.1 在客户端里获取商家电脑的硬件信息,作为商家的身份标识;
    4.2 日志记录排好序再写到log里,可以保证组内绝对有序,组间相对有序;
    4.3 找一些开源工具。。

mysql binlog日志查看

基本说明

  1. 定义
    binlog基本定义:二进制日志,也成为二进制日志,记录对数据发生或潜在发生更改的SQL语句,并以二进制的形式保存在磁盘中;

  2. 作用
    可以用来查看数据库的变更历史(具体的时间点所有的SQL操作)、数据库增量备份和恢复(增量备份和基于时间点的恢复)、Mysql的复制(主主数据库的复制、主从数据库的复制)

  3. 格式
    binlog的格式有三种,这也反应了mysql的复制技术:基于SQL语句的复制(statement-based replication, SBR),基于行的复制(row-based replication, RBR),混合模式复制(mixed-based replication, MBR)。相应地,binlog的格式也有三种:STATEMENT,ROW,MIXED。

  4. 日志位置
    修改my.cnf参数文件
    [mysqld]
    log-bin=mysql-bin

语法说明

  1. 命令
    binlog不能直接用文本的方式打开。
    使用show binlog events方式可以获取当前以及指定binlog的日志,不适宜提取大量日志。
    使用mysqlbinlog命令行提取(适宜批量提取日志)。

  2. 语法

    直接查看单个二进制日志文件:
    mysqlbinlog filename

    提取指定position位置的binlog日志
    --start-position="120" --stop-position="332"

    提取指定数据库binlog并转换字符集到UTF8
    --database=test --set-charset=utf8

    指定结束时间
    --start-datetime='2015-01-20 09:00:00' --stop-datetime='2015-01-20 12:59:59'

    指定row格式解码
    --base64-output=decode-rows

    -v用于输出基于row模式的binlog日志
    -vv为列数据类型添加注释

日志格式

# at 579744(开始位置)
#150905  7:02:54(时间截) server id 2543308(产生该事件的服务id)  end_log_pos(日志的结束位置) 579815  Query(事件类型)   thread_id=21    exec_time=0     error_code=0
SET TIMESTAMP=1441407774/*!*/;
BEGIN
执行的sql语句

其他说明

  1. 在数据出错的情况下,使用 MYSQLBINLOG 来恢复数据

5.3 迁移 5.6 需要注意的问题

👁代表额外需要注意的,✅代表遇到过

5.3 Migrating 5.4

重点

  1. breakcontinue不再接受可变参数(break 1 + max(x, y)),break 0;continue 0不允许出现
  2. 现在参数名使用全局变量将会导致一个致命错误。禁止类似 function foo($_GET, $_POST) {} 这样的代码。
  3. 非数字的字符串偏移量👁
    $a = '12345';
     var_dump(isset($a['x']); // 5.3 true, 5.4 false
  1. 调用时的引用传递👁
 function f(&$v){$v = true;}
 f(&$v); // 5.3 no problem 5.4 PHP Fatal error

 function f1($v){$v = true;}
 f1(&$v); // 5.3 no problem 5.4 PHP Fatal error

function f2(&$v){$v = true;}
f2($v); // ok

一些不常用的

  1. 不支持安全模式
  2. 移除魔术引号
  3. register_globals 和 register_long_arrays php.ini 指令被移除。

    PHP: 不向后兼容的变更 - Manual

5.4 Migrating 5.5

重点

  1. 原始的 MySQL 扩展 现在被废弃,当连接到数据库时会产生一个 E_DEPRECATED 错误。👁

PHP: 不向后兼容的变更 - Manual

5.5 Migrating 5.6

重点

  1. json_decode 严格模式
  2. cURL文件上传 👁✅
    > 必须先设置 CURLOPT_SAFE_UPLOAD为 FALSE 才能够使用 @file 语法来上传文件。 建议使用 CURLFile 类来上传文件。
  3. 使用::调用非静态方法,现在产生 E_DEPRECATED 错误 (以前是 E_STRICT)
  4. 使用 always_populate_raw_post_data 会导致在填充 $HTTP_RAW_POST_DATA 时产生 E_DEPRECATED 错误。 请使用 php://input 替代 $HTTP_RAW_POST_DATA, 因为它可能在后续的 PHP 版本中被移除。 在php.ini中设置 always_populate_raw_post_data 为 -1 (这样会强制 $HTTP_RAW_POST_DATA 未定义,所以也不回导致 E_DEPRECATED 的错误) 👁✅
  5. 使用数组标识符为类定义数组类型的属性时,数组的键不会被覆盖
    class C {
    const ONE = 1;
    public $array = [
        self::ONE => 'foo',
        'bar',
        'quux',
    ];
}
var_dump((new C)->array);
/* 5.5 before array(2) {
  [0]=>
  string(3) "bar"
  [1]=>
  string(4) "quux"
}
*
* 5.6 
* array(3) {
  [1]=>
  string(3) "foo"
  [2]=>
  string(3) "bar"
  [3]=>
  string(4) "quux"
}
*/

PHP: 向后不兼容 - Manual

其他问题

  1. 线上的php5.6没有编译fileinfo,这个会导致读不到文件信息,因为运维都是统一安装的,所以最好看一下是否enable-fileinfo

  2. 启用 Opcache,之前因为出现了一些问题,运维那边关掉这个缓存,最好和运维沟通,并测试缓存是否正确使用、正常更新。

    现代 PHP 新特性系列(六) —— Zend Opcache