> 环境说明: 系统:Ubuntu14.04 (安装教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.7-stable
## **1.Timer定时器**
在实际应用中,往往会遇到需要每隔一段时间重复做一件事,比如心跳检测、订阅消息、数据库备份等工作。通常,我们会借助PHP的time()以及相关函数自己实现一个定时器,或者使用crontab工具来实现。但是,自定义的定时器容易出错,而使用crontab则需要编写额外的脚本文件,无论是迁移还是调试都比较麻烦。
因此,Swoole提供了一个内置的Timer定时器功能,通过函数[addtimer](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/03.swoole_server%E5%87%BD%E6%95%B0%E5%88%97%E8%A1%A8.md#swoole_serveraddtimer)即可在Swoole中添加一个定时器,该定时器会在建立之后,按照预先设定好的时间间隔,每到对应的时间就会调用一次回调函数[onTimer](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/02.%E4%BA%8B%E4%BB%B6%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0.md#8ontimer)通知Server。
简单示例如下:
~~~
$this->serv->on('Timer', array($this, 'onTimer'));
public function onWorkerStart( $serv , $worker_id) {
// 在Worker进程开启时绑定定时器
// 只有当worker_id为0时才添加定时器,避免重复添加
if( $worker_id == 0 ) {
$serv->addtimer(500);
$serv->addtimer(1000);
$serv->addtimer(1500);
}
}
public function onTimer($serv, $interval) {
switch( $interval ) {
case 500: { //
echo "Do Thing A at interval 500\n";
break;
}
case 1000:{
echo "Do Thing B at interval 1000\n";
break;
}
case 1500:{
echo "Do Thing C at interval 1500\n";
break;
}
}
}
~~~
可以看到,在[onWorkerStart](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/02.%E4%BA%8B%E4%BB%B6%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0.md#3onworkerstart)回调函数中,通过addtimer添加了三个定时器,时间间隔分别为500、1000、1500。而在onTimer回调中,正好通过间隔的不同来区分不同的定时器回调,从而执行不同的操作。
需要注意的是,在上述示例中,当1000ms的定时器被触发时,500ms的定时器同样会被触发,但是不能保证会在1000ms定时器前触发还是后触发,因此需要注意,定时器中的操作不能依赖其他定时器的执行结果。
[点此查看完整示例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/03/swoole_timer_server.php)
(PS:在Swoole-1.7.7版本,新提供了一个[after](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/03.swoole_server%E5%87%BD%E6%95%B0%E5%88%97%E8%A1%A8.md#swoole_serverafter)函数, 这个功能的用法会在以后的教程中给出。)
## **2.心跳检测**
上文提到过,使用Timer定时器功能可以实现发送心跳包的功能。事实上,Swoole已经内置了心跳检测功能,能自动close掉长时间没有数据来往的连接。而开启心跳检测功能,只需要设置[heartbeat_check_interval](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#11heartbeat_check_interval)和[heartbeat_idle_time](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#12heartbeat_idle_time)即可。如下:
~~~
$this->serv->set(
array(
'heartbeat_check_interval' => 60,
'heartbeat_idle_time' => 600,
)
);
~~~
其中heartbeat_idle_time的默认值是heartbeat_check_interval的两倍。 在设置这两个选项后,swoole会在内部启动一个线程,每隔heartbeat_check_interval秒后遍历一次全部连接,检查最近一次发送数据的时间和当前时间的差,如果这个差值大于heartbeat_idle_time,则会强制关闭这个连接,并通过回调onClose通知Server进程。 [点此查看完整示例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/03/swoole_heartbeat_server.php) **小技巧**: 结合之前的Timer功能,如果我们想维持连接,就设置一个略小于如果这个差值大于heartbeat_idle_time的定时器,在定时器内向所有连接发送一个心跳包。如果收到心跳回应,则判断连接正常,如果没有收到,则关闭这个连接或者再次尝试发送。
## **3.Task进阶:MySQL连接池**
上一章中我简单讲解了如何开启和使用Task功能。这一节,我将提供一个Task的高级用法。
在PHP中,访问MySQL数据库往往是性能提升的瓶颈。而MySQL连接池我想大家都不陌生,这是一个很好的提升数据库访问性能的方式。传统的MySQL连接池,是预先申请一定数量的连接,每一个新的请求都会占用其中一个连接,请求结束后再将连接放回池中,如果所有连接都被占用,新来的连接则会进入等待状态。
知道了MySQL连接池的实现原理,那我们来看如何使用Swoole实现一个连接池。
首先,Swoole允许开启一定量的Task Worker进程,我们可以让每个进程都拥有一个MySQL连接,并保持这个连接,这样,我们就创建了一个连接池。
其次,设置swoole的[dispatch_mode](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#5dispatch_mode)为抢占模式(主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker)。这样,每个task都会被投递给闲置的Task Worker。这样,我们保证了每个新的task都会被闲置的Task Worker处理,如果全部Task Worker都被占用,则会进入等待队列。
下面直接上关键代码:
~~~
public function onWorkerStart( $serv , $worker_id) {
echo "onWorkerStart\n";
// 判定是否为Task Worker进程
if( $worker_id >= $serv->setting['worker_num'] ) {
$this->pdo = new PDO(
"mysql:host=localhost;port=3306;dbname=Test",
"root",
"123456",
array(
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8';",
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true
)
);
}
}
~~~
首先,在每个Task Worker进程中,创建一个MySQL连接。这里我选用了PDO扩展。
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
$sql = array(
'sql'=>'select * from Test where pid > ?',
'param' => array(
0
),
'fd' => $fd
);
$serv->task( json_encode($sql) );
}
~~~
其次,在需要的时候,通过[task](https://github.com/LinkedDestiny/swoole-doc/blob/master)函数投递一个任务(也就是发起一次SQL请求)
~~~
public function onTask($serv,$task_id,$from_id, $data) {
$sql = json_decode( $data , true );
$statement = $this->pdo->prepare($sql['sql']);
$statement->execute($sql['param']);
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
$serv->send( $sql['fd'],json_encode($result));
return true;
}
~~~
最后,在onTask回调中,根据请求过来的SQL语句以及相应的参数,发起一次MySQL请求,并将获取到的结果通过send发送给客户端(或者通过return返回给Worker进程)。而且,这样的一次MySQL请求还不会阻塞Worker进程,Worker进程可以继续处理其他的逻辑。
可以看到,简单十几行代码,就实现了一个高效的异步MySQL连接池。
通过测试,单个客户端一共发起1W次select请求,共耗时9s;
1W次insert请求,共耗时21s。
(客户端会在每次收到前一个请求的结果后才会发起下一次请求,而不是并发)。
[点此查看完整服务端代码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/03/swoole_mysql_pool_server.php)
[点此查看完整客户端代码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/03/swoole_mysql_pool_client.php)
## **4.Task实战:yii中应用task**
在YII框架中结合了swoole 的task 做了异步处理。 本例中 主要用到 1、protected/commands/ServerCommand.php 用来做server。 2、protected/event/下的文件 这里是在异步中的具体实现。
客户端调用参照 TestController
~~~
<?php
class TestController extends Controller{
public function actionTT(){
$message['uid'] = 2;
$message['email'] = '83212019@qq.com';
$message['title'] = '接口报警邮件';
$message['contents'] = "'EmailEvent'接口请求过程出错! 错误信息如下:err_no:'00000' err_msg:'测试队列' 请求参数为:'[]'";
$message['type'] = 2;
$data['param'] = $message;
$data['class'] = 'Email';
$client = new EventClient();
$data = $client->send($data);
}
}
?>
~~~
有个task表是用来记录异步任务的。如果失败重试3次。sql在protected/data/sql.sql里。
[点此查看完整客户端代码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/03/swoole_mysql_pool_client.php)