Swoole的文件操作及文件上传下载服务器
最后更新于:2022-04-01 01:09:04
Swoole的自定义协议功能的使用
最后更新于:2022-04-01 01:09:02
> 环境说明: 系统:Ubuntu14.04 (安装教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.8-alpha
## **1.为什么要提供自定义协议**
熟悉TCP通信的朋友都会知道,TCP是一个流式协议。客户端向服务器发送的一段数据,可能并不会被服务器一次就完整的收到;客户端向服务器发送的多段数据,可能服务器一次就收到了全部的数据。而实际应用中,我们希望在服务器端能一次接收**一段完整的**数据,不多也不少。传统的TCP服务器中,往往需要由程序员维护一个缓存区,先将读到的数据放进缓存区中,然后再通过预先设定好的协议内容,来区分一段完整数据的开头、长度和结尾,并将一段完整的数据交给逻辑部分处理。这就是自定义协议的功能。
而在Swoole中,已经在底层实现了一个数据缓存区,并内置好了几个常用的协议类型,直接在底层做好了数据的拆分,保证了在onReceive回调函数中,一定能收到一个(或数个)完整的数据段。数据缓存区的大小可以通过配置选项[package_max_length](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#19package_max_length)来控制。下面我就将讲解如何使用这些内置协议。
## **2.EOF标记型协议**
第一个比较常用的协议就是EOF标记协议。协议的内容是通过规定一个一定不会出现在正常数据中的字符或者字符串,用这个来标记一段完整数据的结尾。这样,只要发现这个结尾,就可以认定之前的数据已经结束,可以开始接收一个新的数据段了。
在Swoole中,可以通过[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)和[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)两个配置项来开启。其中,[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)指定开启了EOF检测,[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)指定了具体的EOF标记。通过这两个选项,Swoole底层就会自动根据EOF标记来缓存和拆分收到的数据包。示例如下:
~~~
$this->serv->set(array(
'package_max_length' => 8192,
'open_eof_check'=> true,
'package_eof' => "\r\n"
));
~~~
就这样,swoole就已经开启了EOF标记协议的解析。那么让我们来测试一下效果:
服务器这边:
~~~
// Server
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
}
~~~
客户端这边:
~~~
$msg_eof = "This is a Msg\r\n";
$i = 0;
while( $i < 100 ) {
$this->client->send( $msg_eof );
$i ++;
}
~~~
然后运行一下,你会发现:哎不对啊,为什么还是一次收到了好多数据啊!
这是因为,在Swoole中,采用的不是遍历识别的方法,而只是简单的检查每一次接收到的数据的末尾是不是定义好的EOF标记。因此,在开启EOF检测后,onReceive回调中还是可能会一次收到多个数据包。
这要怎么办?你会发现,虽然是多个数据包,但是实际上收到的是N个完整的数据片段,那就只需要根据EOF把每个包再拆出来,一个个处理就好啦。
修改后的服务器端代码如下:
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
$data_list = explode("\r\n", $data);
foreach ($data_list as $msg) {
if( !empty($msg) ) {
echo "Get Message From Client {$fd}:{$msg}\n";
}
}
}
~~~
再次运行,妥了~
[点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_eof_server.php)
另外,如果担心这样运行多段数据会长时间占用Worker,可以采用把数据+fd转发给Task进程的做法。如何转发请读者自己尝试实现。
## **3.固定包头类型协议**
固定包头协议是在实际应用中最常用的协议。该协议的内容是规定一个固定长度的包头,在包头的固定位置有一个指定好的字段存放了后续数据的实际长度。这样,服务器端可以先读取固定长度的数据,从中提取出长度,然后再读取指定长度的数据,即可获取一段完整的数据。
在Swoole中,同样提供了固定包头的协议格式。需要注意的是,Swoole只允许二进制形式的包头,因此,需要使用pack、unpack来打包、解包。
通过设置[open_length_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#15open_length_check)选项,即可打开固定包头协议解析功能。此外还有[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset),[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)和[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)三个配置项用于控制解析功能。[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset)规定了包头中第几个字节开始是长度字段,[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)规定了包头的长度,[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)规定了长度字段的类型。
具体设置如下:
~~~
$this->serv->set(array(
'package_max_length' => 8192,
'open_length_check'=> true,
'package_length_offset' => 0,
'package_body_offset' => 4,
'package_length_type' => 'N'
));
~~~
具体如何设置这些参数请参考[文档](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md)
OK,废话不多讲,直接上实例:
服务器端:
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
$length = unpack("N" , $data)[1];
echo "Length = {$length}\n";
$msg = substr($data,-$length);
echo "Get Message From Client {$fd}:{$msg}\n";
}
~~~
客户端:
~~~
$msg_length = pack("N" , strlen($msg_normal) ). $msg_normal;
$i = 0;
while( $i < 100 ) {
$this->client->send( $msg_length );
$i ++;
}
~~~
直接运行,Perfect!
[点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_length_check_server.php)
[点此查看其他相关源码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05)
## **4.特别篇:Http协议-Swoole内置的http_server**
从Swoole-1.7.7-stable开始,Swoole在内部封装并实现了一个Http服务器。是的,没错,再也不用在PHP层缓存和解析http协议了,Swoole直接内置Http服务器了。
创建一个swoole_http_server的代码如下:
~~~
$http = new swoole_http_server("127.0.0.1", 9501);
$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {
$response->end("<h1>Hello Swoole.</h1>");
});
$http->start();
~~~
只需创建一个swoole_http_server对象并设置onRequest回调函数,即可实现一个http服务器。
在onRequest回调中,共有两个参数。参数`$request`存放了来自客户端的请求,包括Http请求的头部信息、Http请求相关的服务器信息、Http请求的GET和POST参数以及HTTP请求携带的COOKIE信息。参数`$response`用于发送数据给客户端,可以通过该参数设置HTTP响应的Header信息、cookie信息和状态码。
此外,swoole_http_server还提供WebSocket功能,使用此功能需要设置onMessage回调函数,如下:
~~~
$http_server->on('message', function(swoole_http_request $request, swoole_http_response $response) {
echo $request->message;
$response->message(json_encode(array("data1", "data2")));
})
~~~
通过`$request->message`获取WebSocket发送来的消息,再通过`$response->message()`回复消息即可。
[点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_http_server.php)
[点此查看swoole_http_server相关文档](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/14.swoole_http_server.md)
(最后做个小广告,经过尝试,已经初步将php的Yaf框架移植到了swoole_http_server上,经过测试,swoole-yaf的性能远远超过了nginx+php-fpm+yaf的性能。
项目地址:[https://github.com/LinkedDestiny/swoole-yaf](https://github.com/LinkedDestiny/swoole-yaf)
我将继续不断完善这个项目,力争能够真正用于线上项目)
Swoole多端口监听、热重启以及Timer进阶:简单crontab
最后更新于:2022-04-01 01:09:00
> 环境说明: 系统:Ubuntu14.04 (安装教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.7-stable
## **1.多端口监听**
在实际运用场景中,服务器可能需要监听不同host下的不同端口。比如,一个应用服务器,可能需要监听外网的服务端口,同时也需要监听内网的管理端口。在Swoole中,可以轻松的实现这样的功能。 Swoole提供了[addlistener](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_serveraddlistener)函数用于给服务器添加一个需要监听的host及port,并指定对应的Socket类型(TCP,UDP,Unix Socket以及对应的IPv4和IPv6版本)。 代码如下:
~~~
$serv = new swoole_server("192.168.1.1", 9501); // 监听外网的9501端口
$serv->addlistener("127.0.0.1", 9502 , SWOOLE_TCP); // 监听本地的9502端口
$serv->start(); // addlistener必须在start前调用
~~~
此时,swoole_server就会同时监听两个host下的两个端口。这里要注意的是,来自两个端口的数据会在同一个onReceive中获取到,这时就要用到swoole的另一个成员函数[connection_info](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_serverconnection_info),通过这个函数获取到fd的from_port,就可以判定消息的类型。
~~~
$info = $serv->connection_info($fd, $from_id);
//来自9502的内网管理端口
if($info['from_port'] == 9502) {
$serv->send($fd, "welcome admin\n");
}
//来自外网
else {
$serv->send($fd, 'Swoole: '.$data);
}
~~~
[点此查看完整源码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/04/swoole_multi_port_server.php)
## **2.服务器热重启**
所谓热重启,就是当服务器相关代码有所变动之后,无需停止服务,而是在服务器仍然运行的状态下更新文件。Swoole通过内置的reload函数以及两个自定义信号量实现了这一功能。 首先我讲解一下Swoole可用的三个信号:SIGTERM,SIGUSR1,SIGUSR2。SIGTERM用于停止服务器,SIGUSR1用于重启全部的Worker进程,SIGUSR2用于重启全部的Task Worker进程。 那要如何实现热更新代码文件呢?Swoole的回调函数中有这个一个回调[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);该回调会在Worker进程启动时被调用。因此,当swoole_server收到SIGUSR1信号并重启全部Worker进程后,onWorkerStart就会被调用。如果在onWorkerStart中require全部的代码文件,每次onWorkerStart后都会重新require一次php文件,这样就能实现代码文件的热更新。 来看下代码实现:
~~~
public function onStart( $serv ) {
cli_set_process_title("reload_master");
}
public function onWorkerStart( $serv , $worker_id) {
require_once "reload_page.php";
Test(); // reload_page.php中定义的一个函数
}
~~~
首先,在[onStart](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#2onstart)回调函数中通过php的cli_set_process_title函数设置进程名。 在[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)中,require相关的php文件。 然后,新建一个reload.sh文件,输入如下内容:
~~~
echo "Reloading..."
cmd=$(pidof reload_master)
kill -USR1 "$cmd"
echo "Reloaded"
~~~
这样,就可以通过执行这个脚本重启服务器了。 [点此查看完整源码](https://github.com/LinkedDestiny/swoole-doc/tree/master/src/04/reload)
## **3.Timer补充:after函数**
在swoole-1.7.7stable版本中,Timer新增了一个函数[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)。该函数的作用是在指定的时间间隔后执行回调函数,并且只执行一次。 这个函数可以弥补Timer本身做不到或者做起来很难的一些定时工作。 代码如下:
~~~
$serv->after( 1000 , array($this, 'onAfter') , $str );
~~~
这里指定在1000ms后,执行onAfter回调函数,函数参数为$str。 举个例子,比如服务器要求在收到某个请求后,在30S后向所有用户发起推送。这样的需求就可以直接用after函数来实现。 [点此查看完整源码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/04/swoole_after_server.php)
## **4.Timer进阶:简易crontab**
未完成
Timer定时器、心跳检测及Task进阶实例:mysql连接池
最后更新于:2022-04-01 01:08:57
> 环境说明: 系统: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)
Swoole的Task使用以及swoole_client
最后更新于:2022-04-01 01:08:55
> 环境说明: 系统:Ubuntu14.04 (安装教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.6-stable
上一章已经简单介绍了如何写一个简单的Echo服务器,并了解了onReceive等几个核心回调函数的使用方法。这一章,我将介绍如何使用Swoole的异步任务Task。
## **1.Task简介**
Swoole的业务逻辑部分是同步阻塞运行的,如果遇到一些耗时较大的操作,例如访问数据库、广播消息等,就会影响服务器的响应速度。因此Swoole提供了Task功能,将这些耗时操作放到另外的进程去处理,当前进程继续执行后面的逻辑。
## **2.开启Task功能**
开启Task功能只需要在swoole_server的配置项中添加[task_worker_num](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#6task_worker_num)一项即可,如下:
~~~
$serv->set(array(
'task_worker_num' => 8
));
~~~
即可开启task功能。此外,必须给swoole_server绑定两个回调函数:[onTask](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#6ontask)和[onFinish](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#7onfinish)。这两个回调函数分别用于执行Task任务和处理Task任务的返回结果。
## **3.使用Task**
首先是发起一个Task,代码如下:
~~~
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
// send a task to task worker.
$param = array(
'fd' => $fd
);
// start a task
$serv->task( json_encode( $param ) );
echo "Continue Handle Worker\n";
}
~~~
可以看到,发起一个任务时,只需通过swoole_server对象调用task函数即可发起一个任务。swoole内部会将这个请求投递给task_worker,而当前Worker进程会继续执行。
当一个任务发起后,task_worker进程会响应[onTask](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#6ontask)回调函数,如下:
~~~
public function onTask($serv,$task_id,$from_id, $data) {
echo "This Task {$task_id} from Worker {$from_id}\n";
echo "Data: {$data}\n";
for($i = 0 ; $i < 10 ; $i ++ ) {
sleep(1);
echo "Task {$task_id} Handle {$i} times...\n";
}
$fd = json_decode( $data , true )['fd'];
$serv->send( $fd , "Data in Task {$task_id}");
return "Task {$task_id}'s result";
}
~~~
这里我用sleep函数和循环模拟了一个长耗时任务。在onTask回调中,我们通过task_id和from_id(也就是worker_id)来区分不同进程投递的不同task。当一个task执行结束后,通过return一个字符串将执行结果返回给Worker进程。Worker进程将通过[onFinish](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#7onfinish)回调函数接收这个处理结果。
下面来看onFinish回调:
~~~
public function onFinish($serv,$task_id, $data) {
echo "Task {$task_id} finish\n";
echo "Result: {$data}\n";
}
~~~
在[onFinish](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#7onfinish)回调中,会接收到Task任务的处理结果$data。在这里处理这个返回结果即可。 (**Tip:** 可以通过在传递的data中存放fd、buff等数据,来延续投递Task之前的工作)
[点此查看完整示例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/02/swoole_task_server.php)
## **4.swoole_client**
之所以在这里讲解如何使用swoole_client是因为,在写服务端代码的时候,不可避免的需要用到客户端来进行测试。swoole提供了swoole_client用于编写测试客户端,下面我将讲解如何使用这个工具。
swoole_client有两种工作模式:同步阻塞模式和异步回调模式。其中,同步阻塞模式在上一章中已经给出示例,其使用和一般的socket基本无异。因此,我将重点讲解swoole_client的异步回调模式。
创建一个异步client的代码如下:
~~~
$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
~~~
其中,**SWOOLE_SOCK_ASYNC**选项即表明创建一个异步client。
既然是异步,那当然需要回调函数。swoole_client一共有四个回调函数,如下:
~~~
$client->on("connect", function($cli) {
$cli->send("hello world\n");
});
$client->on("receive", function($cli, $data){
echo "Received: ".$data."\n";
});
$client->on("error", function($cli){
echo "Connect failed\n";
});
$client->on("close", function($cli){
echo "Connection close\n";
});
~~~
这几个回调函数的作用基本和swoole_server类似,只有参数不同,因此不再赘述。 [点此查看完整示例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/02/swoole_async_client.php)
## **进阶:简易聊天室**
我用swoole扩展写了一个简单的聊天室Demo([点此查看](https://github.com/LinkedDestiny/swoole-doc/tree/master/src/Chatroom)) 这个Demo虽然用到了一些其他的架构,但是核心功能仍然是依托swoole扩展实现的。
* 通过onReceive回调接收数据,根据预先规定的字段找到对应的处理函数。
* 通过onTask处理发送数据、广播这样的耗时内容。
[Server.php](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/Chatroom/Server/app/socket/Server.php)是全部的Swoole回调函数实现的类。
环境搭建及扩展安装
最后更新于:2022-04-01 01:08:53
> 环境说明:
> 系统:Ubuntu14.04 (安装教程包括CentOS6.5)
> PHP版本:PHP-5.5.10
> swoole版本:1.7.6-stable
## PHP安装
要用swoole,首先需要有PHP环境。由于swoole的某些特性,最好是能够从源码编译安装PHP,这样在使用过程中可以避免很多不必要的错误。 PHP下载地址:[http://php.net/](http://php.net/) 在这里挑选你想用的版本即可。下载源码包后,解压至本地任意目录(保证读写权限),留待使用。 安装PHP前,需要安装编译环境和PHP的相关依赖。下面是相关命令: Ubuntu环境下:
~~~
sudo apt-get install build-essential gcc g++ autoconf libiconv-hook-dev libmcrypt-dev libxml2-dev libmysqlclient-dev libcurl4-openssl-dev libjpeg8-dev libpng12-dev libfreetype6-dev
~~~
CentOS环境下:
~~~
yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers gd gd2 gd-devel gd2-devel perl-CPAN pcre-devel
~~~
(注:以上命令是我在实际使用中验证过的可以使用的,可能会和其他教程提供的命令不同) 当上述命令执行后,即可开始安装PHP。命令如下:
~~~
cd php-5.5.10/
./configure --prefix=/usr/local/php --with-config-file-path=/etc/php --enable-fpm --enable-pcntl --enable-mysqlnd --enable-opcache --enable-sockets --enable-sysvmsg --enable-sysvsem --enable-sysvshm --enable-shmop --enable-zip --enable-ftp --enable-soap --enable-xml --enable-mbstring --disable-rpath --disable-debug --disable-fileinfo --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pcre-regex --with-iconv --with-zlib --with-mcrypt --with-gd --with-openssl --with-mhash --with-xmlrpc --with-curl --with-imap-ssl
sudo make
sudo make install
sudo cp php.ini-development /etc/php/
~~~
至此,PHP已经成功安装,但是此时在终端里是无法直接通过php --version查看php版本的还需要将PHP的可执行目录添加到环境变量中。 使用Vim/Sublime打开~/.bashrc,在末尾添加如下内容:
~~~
export PATH=/usr/local/php/bin:$PATH
export PATH=/usr/local/php/sbin:$PATH
~~~
保存后,终端输入命令:
~~~
source ~/.bashrc
~~~
此时即可通过php --version查看php版本,看到如下内容:
~~~
PHP 5.5.10 (cli) (built: Apr 26 2014 09:46:14)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
~~~
即说明安装成功。
## Swoole安装
安装完PHP后,即可安装swoole扩展。 swoole扩展下载地址:[https://github.com/swoole/swoole-src/releases](https://github.com/swoole/swoole-src/releases) 尽量选择stable版本,alpha版本最好仅用于实验新特性。 解压源码至任意目录,执行如下命令:
~~~
cd swoole-src-swoole-1.7.6-stable/
phpize
./configure --enable-async-mysql
sudo make
sudo make install
~~~
(注:swoole的./configure有很多额外参数,可以通过./configure --help命令查看,这里仅开启其中async-mysql项,其他均选择默认项) 安装完成后,进入/etc/php目录下,打开php.ini文件,在其中加上如下一句:
~~~
extension=swoole.so
~~~
随后在终端中输入命令
~~~
php -m
~~~
查看扩展安装情况。如果在列出的扩展中看到了swoole,则说明安装成功。
## 基本实例
下面贴一个基本的基于swoole的echo服务器
~~~
// Server
class Server
{
private $serv;
public function __construct() {
$this->serv = new swoole_server("0.0.0.0", 9501);
$this->serv->set(array(
'worker_num' => 8,
'daemonize' => false,
'max_request' => 10000,
'dispatch_mode' => 2,
'debug_mode'=> 1
));
$this->serv->on('Start', array($this, 'onStart'));
$this->serv->on('Connect', array($this, 'onConnect'));
$this->serv->on('Receive', array($this, 'onReceive'));
$this->serv->on('Close', array($this, 'onClose'));
$this->serv->start();
}
public function onStart( $serv ) {
echo "Start\n";
}
public function onConnect( $serv, $fd, $from_id ) {
$serv->send( $fd, "Hello {$fd}!" );
}
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
echo "Get Message From Client {$fd}:{$data}\n";
}
public function onClose( $serv, $fd, $from_id ) {
echo "Client {$fd} close connection\n";
}
}
// 启动服务器
$server = new Server();
~~~
从代码中可以看出,创建一个swoole_server基本分三步:
1. 通过构造函数创建swoole_server对象
2. 调用**set**函数设置swoole_server的相关配置选项
3. 调用**on**函数设置相关回调函数 关于set配置选项以及on回调函数的具体说明,请参考我整理的swoole文档( [配置选项](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md))
这里只给出简单介绍。onStart回调在server运行前被调用,onConnect在有新客户端连接过来时被调用,onReceive函数在有数据发送到server时被调用,onClose在有客户端断开连接时被调用。 这里就可以大概看出如何使用swoole:在onConnect处监听新的连接;在onReceive处接收数据并处理,然后可以调用send函数将处理结果发送出去;在onClose处处理客户端下线的事件。
下面贴出客户端的代码:
~~~
<?php
class Client
{
private $client;
public function __construct() {
$this->client = new swoole_client(SWOOLE_SOCK_TCP);
}
public function connect() {
if( !$this->client->connect("127.0.0.1", 9501 , 1) ) {
echo "Error: {$fp->errMsg}[{$fp->errCode}]\n";
}
$message = $this->client->recv();
echo "Get Message From Server:{$message}\n";
fwrite(STDOUT, "请输入消息:");
$msg = trim(fgets(STDIN));
$this->client->send( $msg );
}
}
$client = new Client();
$client->connect();
~~~
这里,通过swoole_client创建一个基于TCP的客户端实例,并调用connect函数向指定的IP及端口发起连接请求。随后即可通过recv()和send()两个函数来接收和发送请求。需要注意的是,这里我使用了默认的同步阻塞客户端,因此recv和send操作都会产生网络阻塞。
(以上两段代码均以上传git,地址:[https://github.com/LinkedDestiny/swoole-doc/tree/master/src/01)](https://github.com/LinkedDestiny/swoole-doc/tree/master/src/01%EF%BC%89)
写在前面
最后更新于:2022-04-01 01:08:51
## 作者
* Lancelot(李丹阳) from **LinkedDestiny**(**牵机工作室**)
* reallovelei (张磊)
## 描述
用于swoole新手入门的教程以及swoole的文档整理,由于个人能力所限,其中难免有不全或错误的地方。如果你发现了这些错误和不全,请联系我并可以做出改正。
## 邮箱
* [simonarthur2012@gmail.com](mailto:simonarthur2012@gmail.com) (Lancelot)
* [83212019@qq.com](mailto:83212019@qq.com) (reallovelei)
## 写在前面的废话
《swoole源码分析》已经写了13章,整个swoole的核心架构基本都分析的差不多了。于是心里一直以来想整理swoole的文档并写一份教程的想法就再度浮了出来。实话说,我接触swoole乃至接触PHP都仅有9个月的时间,而自7月份以来一直在公司做Android开发,也有没有了使用swoole的机会。所以,现在我只能写出一份入门级教程,帮助刚刚接触swoole的人理解和使用swoole写一些简单的例子,从而初步掌握-swoole的用法。
Git地址:[https://github.com/LinkedDestiny/swoole-doc](https://github.com/LinkedDestiny/swoole-doc)