Swoole 简介

swoole是运行在PHP下的一个extesion扩展,实际上与普通的扩展不同。普通的扩展只是提供一个库函数。而swoole扩展在运行后会接管PHP的控制权,进入事件循环。当IO事件发生后,swoole会自动回调指定的PHP函数。

PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。

Swoole可以广泛应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。

Swoole底层内置了异步非阻塞、多线程的网络IO服务器。PHP程序员仅需处理事件回调即可,无需关心底层。与Nginx/Tornado/Node.js等全异步的框架不同,Swoole既支持全异步,也支持同步。

Swoole是开源免费的自由软件,授权协议是Apache2.0。企业和个人开发者均可免费使用Swoole的代码,并且在Swoole之上所作的修改可用于商业产品,无需开源(注:必须保留原作者的版权声明)。

Swoole的实现

swoole使用纯C编写,不依赖其他第三方库。

  • swoole并没有用libevent,所以不需要安装libevent
  • swoole并不依赖php的stream/sockets/pcntl/posix/sysvmsg等扩展

socket部分

swoole使用底层的socket系统调用。参见 sys/socket.h

IO事件循环

  • 主进程的事件循环使用select/poll,因为主线程中的文件描述符只有几个,使用select/poll即可
  • reactor线程/worker进程中使用epoll/kqueue
  • task进程没有事件循环,进程会循环阻塞读取管道

有很多人使用strace -p去查看swoole主进程只能看到poll系统调用。正确的查看方法是strace -f -p

多进程/多线程

  • 多进程使用fork()系统调用
  • 多线程使用pthread线程库

EventFd

Swoole中使用了eventfd作为线程/进程间消息通知的机制。

Timerfd

Swoole使用timerfd来实现定时器

SIgnalfd

swoole中使用了signalfd来实现对信号的屏蔽和处理。可以有效地避免线程/进程被信号打断,系统调用restart的问题。在主进程中reactor线程不会接受任何信号。


1.8.7或更高版本已完全兼容PHP7

swoole_server

强大的TCP/UDP Server框架,多线程,EventLoop,事件驱动,异步,Worker进程组,Task异步任务,毫秒定时器,SSL/TLS隧道加密。

  • swoole_http_server是swoole_server的子类,内置了Http的支持
  • swoole_websocket_server是swoole_http_server的子类,内置了WebSocket的支持

swoole_client

TCP/UDP客户端,支持同步并发调用,也支持异步事件驱动。

swoole_event

EventLoop API,让用户可以直接操作底层的事件循环,将socket,stream,管道等Linux文件加入到事件循环中。

eventloop接口仅可用于socket类型的文件描述符,不能用于磁盘文件读写

swoole_async

异步IO接口,提供了 异步文件系统IO,异步DNS查询,异步MySQL等API。包括2个重要的子模块:

  • swoole_timer,异步毫秒定时器,可以实现间隔时间或一次性的定时任务
  • file,文件系统操作的异步接口

swoole_process

进程管理模块,可以方便的创建子进程,进程间通信,进程管理。

swoole_buffer

强大的内存区管理工具,像C一样进行指针计算,又无需关心内存的申请和释放,而且不用担心内存越界,底层全部做好了。

swoole_table

基于共享内存和自旋锁实现的超高性能内存表。彻底解决线程,进程间数据共享,加锁同步等问题。

swoole_table的性能可以达到单线程每秒读写50W次

swoole 环境依赖

  • 仅支持Linux,FreeBSD,MacOS,3类操作系统
  • Linux内核版本2.3.32以上
  • PHP5.3.10以上版本,包括PHP7
  • gcc4.4以上版本或者clang
  • cmake2.4+,编译为libswoole.so作为C/C++库时需要使用cmake

PHP版本依赖

  • swoole仅支持PHP5.3.10或更高版本,建议使用PHP5.4+
  • swoole不依赖php的stream、sockets、pcntl、posix、sysvmsg等扩展。PHP只需安装最基本的扩展即可

推荐使用的Linux发行版

  • CentOS6.2+
  • Ubuntu12+
  • Debian6+

ARM平台(树莓派Raspberry PI)

  • 请使用swoole-1.7.10或更高版本
  • 使用GCC交叉编译
  • 在编译Swoole时,需要手工修改Makefile去掉-O2编译参数

MIPS平台(OpenWrt路由器)

  • 请使用swoole-1.7.21或更高版本
  • 使用GCC交叉编译

CygWin环境支持(Windows系统)

swoole-1.7.7增加了对cygwin环境的支持,在Windows环境下,可以直接使用cygwin + php 来跑swoole程序。

  • 安装cygwin,并安装gcc、make、autoconf、php 4个包
  • 下载swoole源码,在cygwin-shell中进行phpize/configure/make/make install
  • 修改php.ini,加入swoole.so

cygwin模式下需要对PHP进行简化,去掉不使用的扩展,避免进程占用内存过大,导致Fork操作失败

BashOnWindows

Windows 10系统增加了Linux子系统支持,BashOnWindows环境下也可以使用swoole

  • BashOnWindows环境下必须关闭daemonize选项

Swoole编译安装步骤

Swoole扩展是按照php标准扩展构建的。使用phpize来生成php编译配置,./configure来做编译配置检测,make进行编译,make install进行安装。

  • 请下载releases版本的swoole,直接从github主干上拉取最新代码可能会编译不过
  • 如果当前用户不是root,可能没有php目录的写权限,安装时需要sudo或者su
  • 如果是在git分支上直接git pull更新代码,重新编译前务必要执行make clean

安装准备

安装swoole前必须保证系统已经安装了下列软件

php-5.3.10 或更高版本gcc-4.4 或更高版本makeautoconf

下载地址

下载源代码包后,在终端进入源码目录,执行下面的命令进行编译和安装

cd swoolephpize./configuremake sudo make install

(注:swoole的./configure有很多额外参数,可以通过./configure --help命令查看,这里仅开启其中async-mysql项,其他均选择默认项) 这里是./configure编译配置的额外参数,用于开启某些特性

1.8.7或更高版本不再需要设置--enable-async-mysql和--enable-async-httpclient,async_mysql和async_httpclient改为内置
--enable-swoole-debug
打开调试日志,开启此选项后swoole将打印各类细节的调试日志。生产环境不要启用。
--enable-sockets
增加对sockets资源的支持,依赖sockets扩展。开启此参数,swoole_event_add就可以添加sockets扩展创建的连接到swoole的事件循环中。
--enable-async-mysql
增加异步mysql支持, 依赖mysqli和mysqlnd扩展。
--enable-async-redis
增加异步Redis客户端支持, 依赖hiredis库
--enable-async-httpclient
增加异步Http和WebSocket客户端支持
--enable-ringbuffer
开启RingBuffer内存池
此设置为试验性质,主要用于提升性能,生产环境请不要开启
--enable-openssl
启用SSL支持

PECL

swoole项目已收录到PHP官方扩展库,除了手工下载编译外,还可以通过PHP官方提供的pecl命令,一键下载安装swoole

pecl install swoole

配置php.ini

编译安装成功后,修改php.ini加入

extension=swoole.so

通过php -mphpinfo()来查看是否成功加载了swoole,如果没有可能是php.ini的路径不对,可以使用php -i |grep php.ini来定位到php.ini的绝对路径。


安装成功后通过phpinfo()查看到的信息:

安装常见错误

make或make install无法执行或编译错误

NOTICE: PHP message: PHP Warning: PHP Startup: swoole: Unable to initialize module
Module compiled with module API=20090626
PHP compiled with module API=20121212
These options need to match
in Unknown on line 0

php版本和编译时使用的phpize和php-config不对应,需要使用绝对路径来进行编译。使用绝对路径执行PHP。

/usr/local/php-5.4.17/bin/phpize./configure --with-php-config=/usr/local/php-5.4.17/bin/php-config/usr/local/php-5.4.17/bin/php server.php

缺少mysql头文件

php_mysqli_structs.h:64:23: fatal error: my_global.h: No such file or directory

没有找到mysqlclient的头文件,需要安装mysqlclient-dev

建议自行编译php,不要使用Linux包管理系统自带的php版本

缺少pcre.h头文件

fatal error: pcre.h: No such file or directory

原因是缺少pcre,需要安装libpcre

Cannot find autoconf

phpize命令需要autoconf工具,请先安装它。

make install失败

make install需要root权限,如果不是以root用户登录的,请用sudo或su,再进行安装。

修改了php.ini后,php -m或phpinfo中没有swoole

php -i|grep php.ini

查看加载的php.ini路径,确认加载了正确的php.ini。

修改php.ini,打开错误显示,查看是否存在启动时错误。

display_errors => On  display_startup_errors => On

error: too many arguments to function 'zend_exception_error'

你的PHP版本低于PHP-5.3.10,请升级PHP版本。

如果还是编译失败了怎么办?

不要气馁,加入我们的开发组QQ群:495864936,你的问题会在24小时内被解决。

构建一个Swoole基本实例

下面贴一个基本的基于swoole的echo服务器

// Serverclass 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
";    }    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}
";    }    public function onClose( $serv, $fd, $from_id ) {        echo "Client {$fd} close connection
";    }}// 启动服务器$server = new Server();

从代码中可以看出,创建一个swoole_server基本分三步: 1. 通过构造函数创建swoole_server对象 2. 调用set函数设置swoole_server的相关配置选项 3. 调用on函数设置相关回调函数 关于set配置选项以及on回调函数的具体说明,请参考我整理的swoole文档( 配置选项)

这里只给出简单介绍。onStart回调在server运行前被调用,onConnect在有新客户端连接过来时被调用,onReceive函数在有数据发送到server时被调用,onClose在有客户端断开连接时被调用。 这里就可以大概看出如何使用swoole:在onConnect处监听新的连接;在onReceive处接收数据并处理,然后可以调用send函数将处理结果发送出去;在onClose处处理客户端下线的事件。

下面贴出客户端的代码:

<?phpclass 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}]
";        }        $message = $this->client->recv();        echo "Get Message From Server:{$message}
";        fwrite(STDOUT, "请输入消息:");          $msg = trim(fgets(STDIN));        $this->client->send( $msg );    }}$client = new Client();$client->connect();

这里,通过swoole_client创建一个基于TCP的客户端实例,并调用connect函数向指定的IP及端口发起连接请求。随后即可通过recv()和send()两个函数来接收和发送请求。需要注意的是,这里我使用了默认的同步阻塞客户端,因此recv和send操作都会产生网络阻塞。

编程须知

这个频道内会详细介绍异步编程与同步编程的不同之处以及需要注意的事项。

注意事项

  • 不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞
  • exit/die是危险的,会导致worker进程退出
  • 可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些请求工作
  • PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出
  • swoole不支持set_exception_handler,必须使用try/catch方式处理异常
  • Worker进程不得共用同一个RedisMySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中。

类/函数重复定义

新手非常容易犯这个错误,由于swoole是常驻内存的,所以加载类/函数定义的文件后不会释放。因此引入类/函数的php文件时必须要使用include_oncerequire_once,否会发生cannot redeclare function/class 的致命错误。

内存管理

PHP守护进程与普通Web程序的变量生命周期、内存管理方式完全不同。请参考 swoole_server内存管理 页面。编写swoole_server或其他常驻进程时需要特别注意。

进程隔离

进程隔离也是很多新手经常遇到的问题。修改了全局变量的值,为什么不生效,原因就是全局变量在不同的进程,内存空间是隔离的,所以无效。所以使用swoole开发Server程序需要了解进程隔离问题。

  • 不同的进程中PHP变量不是共享,即使是全局变量,在A进程内修改了它的值,在B进程内是无效的
  • 如果需要在不同的Worker进程内共享数据,可以用RedisMySQL文件SwooleTableAPCushmget等工具实现
  • 不同进程的文件句柄是隔离的,所以在A进程创建的Socket连接或打开的文件,在B进程内是无效,即使是将它的fd发送到B进程也是不可用的

sleep/usleep的影响

在异步IO的程序中,不得使用sleep/usleep/time_sleep_until/time_nanosleep。(下文中使用sleep泛指所有睡眠函数)

  • sleep函数会使进程陷入睡眠阻塞
  • 直到指定的时间后操作系统才会重新唤醒当前的进程
  • sleep过程中,只有信号可以打断
  • 由于swoole的信号处理是基于signalfd实现的,所以即使发送信号也无法中断sleep

swoole提供的swoole_event_addswoole_timer_tickswoole_timer_afterswoole_process::signal异步swoole_client 在进程sleep后会停止工作。swoole_server也无法再处理新的请求。

实例程序

$serv = new swoole_server("127.0.0.1", 9501);$serv->on('receive', function ($serv, $fd, $from_id, $data) {    sleep(100);    $serv->send($fd, 'Swoole: '.$data);});$serv->start();

onReceive事件中执行了sleep函数,server在100秒内无法再收到任何客户端请求。


exit/die函数的影响

在swoole程序中禁止使用exit/die,如果PHP代码中有exit/die,当前工作的Worker进程、Task进程、User进程、以及swoole_process进程会立即退出。

建议使用try/catch的方式替换exit/die,实现中断执行跳出PHP函数调用栈。

function swoole_exit($msg){    //php-fpm的环境    if (ENV=='php')    {        exit($msg);    }    //swoole的环境    else    {        throw new SwooleExitException($msg);    }}

异常处理的方式比exit/die更友好,因为异常是可控的,exit/die不可控。在最外层进行try/catch即可捕获异常,仅终止当前的任务。Worker进程可以继续处理新的请求,而exit/die会导致进程直接退出,当前进程保存的所有变量和资源都会被销毁。如果进程内还有其他任务要处理,遇到exit/die也将全部丢弃。


while循环的影响

异步程序如果遇到死循环,事件将无法触发。异步IO程序使用Reactor模型,运行过程中必须在reactor->wait处轮询。如果遇到死循环,那么程序的控制权就在while中了,reactor无法得到控制权,无法检测事件,所以IO事件回调函数也将无法触发。

密集运算的代码不是阻塞

实例程序

$serv = new swoole_server("127.0.0.1", 9501);$serv->on('receive', function ($serv, $fd, $from_id, $data) {    while(1)    {        $i ++;    }    $serv->send($fd, 'Swoole: '.$data);});$serv->start();

onReceive事件中执行了死循环,server在无法再收到任何客户端请求,必须等待循环结束才能继续处理新的事件。



swoole优化内核参数调整

ulimit设置

ulimit -n 要调整为100000甚至更大。 命令行下执行 ulimit -n 100000即可修改。如果不能修改,需要设置 /etc/security/limits.conf,加入

* soft nofile 262140* hard nofile 262140root soft nofile 262140root hard nofile 262140* soft core unlimited* hard core unlimitedroot soft core unlimitedroot hard core unlimited

内核设置

net.unix.max_dgram_qlen = 100

swoole使用unix socket dgram来做进程间通信,如果请求量很大,需要调整此参数。系统默认为10,可以设置为100或者更大。
或者增加worker进程的数量,减少单个worker进程分配的请求量。

net.core.wmem_max

修改此参数增加socket缓存区的内存大小

net.ipv4.tcp_mem  =   379008       505344  758016net.ipv4.tcp_wmem = 4096        16384   4194304net.ipv4.tcp_rmem = 4096          87380   4194304net.core.wmem_default = 8388608net.core.rmem_default = 8388608net.core.rmem_max = 16777216net.core.wmem_max = 16777216

net.ipv4.tcp_tw_reuse

是否socket reuse,此函数的作用是Server重启时可以快速重新使用监听的端口。如果没有设置此参数,会导致server重启时发生端口未及时释放而启动失败

net.ipv4.tcp_tw_recycle

使用socket快速回收,短连接Server需要开启此参数

消息队列设置

当使用消息队列作为进程间通信方式时,需要调整此内核参数

  • kernel.msgmnb = 4203520,消息队列的最大字节数
  • kernel.msgmni = 64,最多允许创建多少个消息队列
  • kernel.msgmax = 8192,消息队列单条数据最大的长度

FreeBSD/MacOS

  • sysctl -w net.local.dgram.maxdgram=8192
  • sysctl -w net.local.dgram.recvspace=200000 修改Unix Socket的buffer区尺寸

开启CoreDump

设置内核参数

kernel.core_pattern = /data/core_files/core-%e-%p-%t

通过ulimit -c命令查看当前coredump文件的限制

ulimit -c

如果为0,需要修改/etc/security/limits.conf,进行limit设置。

开启core-dump后,一旦程序发生异常,会将进程导出到文件。对于调查程序问题有很大的帮助

其他重要配置

  • net.ipv4.tcp_syncookies=1
  • net.ipv4.tcp_max_syn_backlog=81920
  • net.ipv4.tcp_synack_retries=3
  • net.ipv4.tcp_syn_retries=3
  • net.ipv4.tcp_fin_timeout = 30
  • net.ipv4.tcp_keepalive_time = 300
  • net.ipv4.tcp_tw_reuse = 1
  • net.ipv4.tcp_tw_recycle = 1
  • net.ipv4.ip_local_port_range = 20000 65000
  • net.ipv4.tcp_max_tw_buckets = 200000
  • net.ipv4.route.max_size = 5242880

查看配置是否生效

如:修改net.unix.max_dgram_qlen = 100后,通过

cat /proc/sys/net/unix/max_dgram_qlen

如果修改成功,这里是新设置的值。


在swoole中,一个swoole_server的相关属性可以通过

$serv->set( $array configs );

函数来配置,这些配置选项使得swoole更加灵活。 示例:

$serv = new swoole_server("0.0.0.0", 9501);$serv->set(array(    'worker_num' => 8,    'max_request' => 10000,    'max_conn' => 100000,    'dispatch_mode' => 2,    'debug_mode'=> 1    'daemonize' => false,));

配置选项以及相关介绍如下:

1.worker_num

描述:指定启动的worker进程数。
说明:swoole是master-> n * worker的模式,开启的worker进程数越多,server负载能力越大,但是相应的server占有的内存也会更多。同时,当worker进程数过多时,进程间切换带来的系统开销也会更大。因此建议开启的worker进程数为cpu核数的1-4倍。
示例:

'worker_num' => 8

2.max_request

描述:每个worker进程允许处理的最大任务数。
说明:设置该值后,每个worker进程在处理完max_request个请求后就会自动重启。设置该值的主要目的是为了防止worker进程处理大量请求后可能引起的内存溢出。
示例:

'max_request' => 10000

3.max_conn

描述:服务器允许维持的最大TCP连接数
说明:设置此参数后,当服务器已有的连接数达到该值时,新的连接会被拒绝。另外,该参数的值不能超过操作系统ulimit -n的值,同时此值也不宜设置过大,因为swoole_server会一次性申请一大块内存用于存放每一个connection的信息。
示例:

'max_conn' => 10000

4.ipc_mode

描述:设置进程间的通信方式。
说明:共有三种通信方式,参数如下:

  • 1 => 使用unix socket通信
  • 2 => 使用消息队列通信
  • 3 => 使用消息队列通信,并设置为争抢模式

示例:

'ipc_mode' => 1

5.dispatch_mode

描述:指定数据包分发策略。
说明:共有三种模式,参数如下:

  • 1 => 轮循模式,收到会轮循分配给每一个worker进程
  • 2 => 固定模式,根据连接的文件描述符分配worker。这样可以保证同一个连接发来的数据只会被同一个worker处理
  • 3 => 抢占模式,主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker

示例:

'dispatch_mode' => 2

6.task_worker_num

描述:服务器开启的task进程数。
说明:设置此参数后,服务器会开启异步task功能。此时可以使用task方法投递异步任务。

设置此参数后,必须要给swoole_server设置onTask/onFinish两个回调函数,否则启动服务器会报错。

示例:

'task_worker_num' => 8

7.task_max_request

描述:每个task进程允许处理的最大任务数。
说明:参考max_request task_worker_num
示例:

'task_max_request' => 10000

8.task_ipc_mode

描述:设置task进程与worker进程之间通信的方式。
说明:参考ipc_mode
示例:

'task_ipc_mode' => 2

9.daemonize

描述:设置程序进入后台作为守护进程运行。
说明:长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当ssh终端退出后,程序将被终止运行。启用守护进程后,标准输入和输出会被重定向到 log_file,如果 log_file未设置,则所有输出会被丢弃。
示例:

'daemonize' => 0

10.log_file

描述:指定日志文件路径
说明:在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。注意log_file 不会自动切分文件,所以需要定期清理此文件。
示例:

'log_file' => '/data/log/swoole.log'

11.heartbeat_check_interval

描述:设置心跳检测间隔
说明:此选项表示每隔多久轮循一次,单位为秒。每次检测时遍历所有连接,如果某个连接在间隔时间内没有数据发送,则强制关闭连接(会有onClose回调)。
示例:

'heartbeat_check_interval' => 60

12.heartbeat_idle_time

描述:设置某个连接允许的最大闲置时间。
说明:该参数配合heartbeat_check_interval使用。每次遍历所有连接时,如果某个连接在heartbeat_idle_time时间内没有数据发送,则强制关闭连接。默认设置为heartbeat_check_interval * 2。
示例:

'heartbeat_idle_time' => 600

13.open_eof_check

描述:打开eof检测功能
说明:与package_eof 配合使用。此选项将检测客户端连接发来的数据,当数据包结尾是指定的package_eof 字符串时才会将数据包投递至Worker进程,否则会一直拼接数据包直到缓存溢出或超时才会终止。一旦出错,该连接会被判定为恶意连接,数据包会被丢弃并强制关闭连接。

EOF检测不会从数据中间查找eof字符串,所以Worker进程可能会同时收到多个数据包,需要在应用层代码中自行explode(" ", $data) 来拆分数据包

示例:

'open_eof_check' => true

14.package_eof

描述:设置EOF字符串
说明:package_eof最大只允许传入8个字节的字符串
示例:

'package_eof ' => '/r/n'

15.open_length_check

描述:打开包长检测
说明:包长检测提供了固定包头+包体这种格式协议的解析,。启用后,可以保证Worker进程onReceive每次都会收到一个完整的数据包。
示例:

'open_length_check' => true

16.package_length_offset

描述:包头中第几个字节开始存放了长度字段
说明:配合open_length_check使用,用于指明长度字段的位置。
示例:

'package_length_offset' => 5

17.package_body_offset

描述:从第几个字节开始计算长度。
说明:配合open_length_check使用,用于指明包头的长度。
示例:

'package_body_offset' => 10

18.package_length_type

描述:指定包长字段的类型
说明:配合open_length_check使用,指定长度字段的类型,参数如下:

  • 's' => int16_t 机器字节序
  • 'S' => uint16_t 机器字节序
  • 'n' => uint16_t 大端字节序
  • ’N‘ => uint32_t 大端字节序
  • 'L' => uint32_t 机器字节序
  • 'l' => int 机器字节序

示例:

'package_length_type' => 'N'

19.package_max_length

描述:设置最大数据包尺寸
说明:该值决定了数据包缓存区的大小。如果缓存的数据超过了该值,则会引发错误。具体错误处理由开启的协议解析的类型决定。
示例:

'package_max_length' => 8192

20.open_cpu_affinity

描述:启用CPU亲和性设置
说明:在多核的硬件平台中,启用此特性会将swoole的reactor线程/worker进程绑定到固定的一个核上。可以避免进程/线程的运行时在多个核之间互相切换,提高CPU Cache的命中率。
示例:

'open_cpu_affinity' => true

21.open_tcp_nodelay

描述:启用open_tcp_nodelay
说明:开启后TCP连接发送数据时会无关闭Nagle合并算法,立即发往客户端连接。在某些场景下,如http服务器,可以提升响应速度。
示例:

'open_tcp_nodelay' => true

22.tcp_defer_accept

描述:启用tcp_defer_accept特性
说明:启动后,只有一个TCP连接有数据发送时才会触发accept。
示例:

'tcp_defer_accept' => true

23.ssl_cert_file和ssl_key_file

描述:设置SSL隧道加密
说明:设置值为一个文件名字符串,指定cert证书key的路径。
示例:

'ssl_cert_file' => '/config/ssl.crt','ssl_key_file' => '/config//ssl.key',

24.open_tcp_keepalive

描述:打开TCP的KEEP_ALIVE选项
说明:使用TCP内置的keep_alive属性,用于保证连接不会因为长时闲置而被关闭。
示例:

'open_tcp_keepalive' => true

25.tcp_keepidle

描述:指定探测间隔。
说明:配合open_tcp_keepalive使用,如果某个连接在tcp_keepidle内没有任何数据来往,则进行探测。
示例:

'tcp_keepidle' => 600

26.tcp_keepinterval

描述:指定探测时的发包间隔
说明:配合open_tcp_keepalive使用
示例:

'tcp_keepinterval' => 60

27.tcp_keepcount

描述:指定探测的尝试次数
说明:配合open_tcp_keepalive使用,若tcp_keepcount次尝试后仍无响应,则判定连接已关闭。
示例:

'tcp_keepcount' => 5

28.backlog

描述:指定Listen队列长度
说明:此参数将决定最多同时有多少个等待accept的连接。
示例:

'backlog' => 128

29.reactor_num

描述:指定Reactor线程数
说明:设置主进程内事件处理线程的数量,默认会启用CPU核数相同的数量, 一般设置为CPU核数的1-4倍,最大不得超过CPU核数*4。
示例:

'reactor_num' => 8

30.task_tmpdir

描述:设置task的数据临时目录
说明:在swoole_server中,如果投递的数据超过8192字节,将启用临时文件来保存数据。这里的task_tmpdir就是用来设置临时文件保存的位置。

需要swoole-1.7.7+

示例:

'task_tmpdir' => '/tmp/task/'

说明

除以上29个选项外,还有几个不常用或者已废弃的选项没有给出。以后如果有实际需求,会补充说明。

在swoole中,一个swoole_server的拥有若干回调函数,这些回调函数决定了swoole_server的相关功能。可以通过

$serv->on( string $event, mixed $callback );

函数来配置. 示例:

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();    }}

全部回调函数以及相关介绍如下:

[TOC]

1.onReceive

描述:接收数据的回调
函数原型:

function onReceive( swoole_server $serv, $fd, $from_id, $data );
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用
$data接收到的数据

说明:每当server接收到客户端发来的数据后,就会通过onReceive回调将数据投递给Worker。如果开启了协议检测,则会在收到完整数据包之后才会响应回调。注意,必须设置该回调函数,否则无法启动服务器。

2.onStart

描述:服务器启动的回调
函数原型:

function onStart( swoole_server $serv);
参数描述
$servswoole_server对象

说明:
onStart事件在Master进程的主线程中被调用。在此回调响应之前Swoole Server已进行了如下操作

  • 已创建了manager进程
  • 已创建了worker子进程
  • 已监听所有TCP/UDP端口
  • 已监听了定时器

接下来要执行

  • 主Reactor开始接收事件,客户端可以connect到Server

onStart回调中,仅允许echo、打印Log、修改进程名称。不得执行其他操作。onWorkerStart和onStart回调是在不同进程中并行执行的,不存在先后顺序。 可以在onStart回调中,将$serv->master_pid$serv->manager_pid的值保存到一个文件中。这样可以编写脚本,向这两个PID发送信号来实现关闭和重启的操作。
从1.7.5+ Master进程内不再支持定时器,onMasterConnect/onMasterClose2个事件回调也彻底移除。Master进程内不再保留任何PHP的接口。
在onStart中创建的全局资源对象不能在worker进程中被使用,因为发生onStart调用时,worker进程已经创建好了。新创建的对象在主进程内,worker进程无法访问到此内存区域,因此全局对象创建的代码需要放置在swoole_server_start之前。

3.onWorkerStart

描述:Worker进程启动的回调
函数原型:

function onWorkerStart( swoole_server $servint $worker_id);
参数描述
$servswoole_server对象
$worker_idWorker进程的id

说明:此事件在worker进程/task_worker启动时发生。

发生PHP致命错误或者代码中主动调用exit时,Worker/Task进程会退出,管理进程会重新创建新的进程 onWorkerStart/onStart是并发执行的,没有先后顺序

通过$worker_id参数的值来,判断worker是普通worker还是task_worker。$worker_id>= $serv->setting['worker_num'] 时表示这个进程是task_worker。
如果想使用swoole_server_reload实现代码重载入,必须在workerStart中require你的业务文件,而不是在文件头部。在onWorkerStart调用之前已包含的文件,不会重新载入代码。
可以将公用的,不易变的php文件放置到onWorkerStart之前。这样虽然不能重载入代码,但所有worker是共享的,不需要额外的内存来保存这些数据。
onWorkerStart之后的代码每个worker都需要在内存中保存一份 $worker_id是一个从0-$worker_num之间的数字,表示这个worker进程的ID $worker_id和进程PID没有任何关系

4.onConnect

描述:新连接接入时的回调
函数原型:

function onConnect( swoole_server $servint $fd, int $from_id);
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用

说明:有新的连接进入时,在worker进程中回调。onConnect/onClose这2个回调发生在worker进程内,而不是主进程。如果需要在主进程处理连接/关闭事件,请注册onMasterConnect/onMasterClose回调。onMasterConnect/onMasterClose回调总是先于onConnect/onClose被执行

5.onClose

描述:连接关闭时的回调
函数原型:

function onClose( swoole_server $servint $fd, int $from_id);
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用

说明:TCP客户端连接关闭后,在worker进程中回调此函数。无论close由客户端发起还是服务器端主动调用swoole_server_close关闭连接,都会触发此事件。 因此只要连接关闭,就一定会回调此函数。

6.onTask

描述:task_worker进程处理任务的回调
函数原型:

function onTask(swoole_server $serv, int $task_id, int $from_id, string $data);
参数描述
$servswoole_server对象
$task_id任务ID
$from_id来自于哪个worker进程
$data任务内容

说明:在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。可以直接将任务结果字符串通过return方式返回给worker进程。worker进程将在onFinish回调中收到结果。注:如果serv->set(array('task_worker_num' => 8)) task_id 并不是从1-8 而是递增的。

7.onFinish

描述:task_worker进程处理任务结束的回调
函数原型:

function onFinish(swoole_server $serv, int $task_id, string $data);
参数描述
$servswoole_server对象
$task_id任务ID
$data任务结果

说明:在此函数中会收到任务处理的结果,通过task_id和worker_id来区分不同的任务。

8.onTimer

描述:定时器触发的回调
函数原型:

function onTimer(swoole_server $serv, int $interval);
参数描述
$servswoole_server对象
$interval定时的间隔

说明:定时器被触发时,该函数被调用。通过interval来区分不同时间间隔的定时器。


swoole_server函数列表

Table of Contents


swoole_server::__construct

功能描述:创建一个swoole_server资源对象
函数原型

// 类成员函数public function swoole_server::__construct(string $host, int $port, int $mode = SWOOLE_PROCESS,    int $sock_type = SWOOLE_SOCK_TCP);// 公共函数function swoole_server_create(string $host, int $port, int $mode = SWOOLE_PROCESS,    int $sock_type = SWOOLE_SOCK_TCP);

返回:一个swoole_server对象
参数说明

参数说明
string host监听的IP地址
int port监听的端口号
int mode运行模式
int sock_type指定的socket类型

说明: host、port、socket_type的详细说明见swoole_server::addlistener
mode指定了swoole_server的运行模式,共有如下三种:

mode类型说明
SWOOLE_BASEBase模式传统的异步非阻塞Server。在Reactor内直接回调PHP的函数。如果回调函数中有阻塞操作会导致Server退化为同步模式。worker_num参数对与BASE模式仍然有效,swoole会启动多个Reactor进程
SWOOLE_THREAD线程模式(已废弃)多线程Worker模式,Reactor线程来处理网络事件轮询,读取数据。得到的请求交给Worker线程去处理。多线程模式比进程模式轻量一些,而且线程之间可以共享堆栈和资源。 访问共享内存时会有同步问题,需要使用Swoole提供的锁机制来保护数据。
SWOOLE_PROCESS进程模式(默认)Swoole提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行,适合业务逻辑非常复杂的场景。

样例:

$serv = new swoole_server("127.0.0.1" , 8888 , SWOOLE_PROCESS , SWOOLE_SOCK_TCP);

swoole_server::set

功能描述:设置swoole_server运行时的各项参数
函数原型

// 类成员函数public function swoole_server::set(array $setting);// 公共函数function swoole_server_set(swoole_server $server, array $setting);

返回:无
参数说明

参数说明
array setting配置选项数组,采用key-value形式

说明
该函数必须在swoole_server::start函数调用前调用。
全部swoole_server的配置参数点此查看
样例:

$serv->set(    array(        'worker_num' => 8,        'max_request' => 10000,        'max_conn' => 100000,        'dispatch_mode' => 2,        'debug_mode'=> 1        'daemonize' => false,    ));

swoole_server::on

功能描述:绑定swoole_server的相关回调函数
函数原型

// 类成员函数public function bool swoole_server->on(string $event, mixed $callback);

返回:设置成功返回true,否则返回false
参数说明

参数说明
string event回调的名称(大小写不敏感)
mixed callback回调的PHP函数,可以是函数名的字符串,类静态方法,对象方法数组,匿名函数

说明
该函数必须在swoole_server::start函数调用前调用。
此方法与swoole_server::handler功能相同,作用是与swoole_client风格保持一致。
swoole_server::on中事件名称字符串不要加on。
全部的回调函数列表点此查看
样例:

$serv->on('connect', function ($serv, $fd){    echo "Client:Connect.
";});$serv->on('receive', array( $myclass, 'onReceive' ) ); // onReceive是myclass的成员函数

swoole_server::addlistener

功能描述:给swoole_server增加一个监听的地址和端口
函数原型

// 类成员函数public function swoole_server::addlistener(string $host, int $port, $type = SWOOLE_SOCK_TCP);// 公共函数function swoole_server_addlisten(swoole_server $serv, string $host, int $port, $type = SWOOLE_SOCK_TCP);

返回:无
参数说明

参数说明
string host监听的IP地址
int port监听的端口号
int sock_type指定的socket类型

说明: swoole支持如下socket类型:

sock_type说明
SWOOLE_TCP/SWOOLE_SOCK_TCPTCP IPv4 Socket
SWOOLE_TCP6/SWOOLE_SOCK_TCP6TCP IPv6 Socket
SWOOLE_UDP/SWOOLE_SOCK_UDPUDP IPv4 Socket
SWOOLE_UDP6/SWOOLE_SOCK_UDP6UDP IPv4 Socket
SWOOLE_UNIX_DGRAMUnix UDP Socket
SWOOLE_UNIX_STREAMUnix TCP Socket

Unix Socket仅在1.7.1+后可用,此模式下host参数必须填写可访问的文件路径,port参数忽略
Unix Socket模式下,客户端fd将不再是数字,而是一个文件路径的字符串
SWOOLE_TCP等是1.7.0+后提供的简写方式,与1.7.0前的SWOOLE_SOCK_TCP是等同的

样例:

$serv->addlistener("127.0.0.1", 9502, SWOOLE_SOCK_TCP);$serv->addlistener("192.168.1.100", 9503, SWOOLE_SOCK_TCP);$serv->addlistener("0.0.0.0", 9504, SWOOLE_SOCK_UDP);$serv->addlistener("/var/run/myserv.sock", 0, SWOOLE_UNIX_STREAM);swoole_server_addlisten($serv, "127.0.0.1", 9502, SWOOLE_SOCK_TCP);

swoole_server::handler

功能描述:设置Server的事件回调函数
函数原型

// 类成员函数public function swoole_server::handler(string $event_name, mixed $event_callback_function);// 公共函数function swoole_server_handler(swoole_server $serv, string $event_name, mixed $event_callback_function);

返回:设置成功返回true,否则返回false
参数说明

参数说明
string event_name回调的名称(大小写不敏感)
mixed event_callback_function回调的PHP函数,可以是函数名的字符串,类静态方法,对象方法数组,匿名函数

说明: 该函数必须在swoole_server::start函数调用前调用。
事件名称字符串要加on。
全部的回调函数列表点此查看

onConnect/onClose/onReceive这3个回调函数必须设置。其他事件回调函数可选
如果设定了timer定时器,onTimer事件回调函数也必须设置
如果启用了task_worker,onTask/onFinish事件回调函数必须设置

样例:

$serv->handler('onStart', 'my_onStart');$serv->handler('onStart', array($this, 'my_onStart'));$serv->handler('onStart', 'myClass::onStart');

swoole_server::start

功能描述:启动server,开始监听所有TCP/UDP端口
函数原型

// 类成员函数public function swoole_server::start()

返回:启动成功返回true,否则返回false
参数说明:无
说明
启动成功后会创建worker_num+2个进程:Master进程+Manager进程+worker_num 个 Worker进程。
另外。启用task_worker会增加task_worker_num个Worker进程
三种进程的说明如下:

进程类型说明
Master进程主进程内有多个Reactor线程,基于epoll/kqueue进行网络事件轮询。收到数据后转发到Worker进程去处理
Manager进程对所有Worker进程进行管理,Worker进程生命周期结束或者发生异常时自动回收,并创建新的Worker进程
Worker进程对收到的数据进行处理,包括协议解析和响应请求

样例:

$serv->start();

swoole_server::reload

功能描述:重启所有worker进程。
函数原型

// 类成员函数public function swoole_server::reload()

返回:调用成功返回true,否则返回false
参数说明:无
说明
调用后会向Manager发送一个SIGUSR1信号,平滑重启全部的Worker进程(所谓平滑重启,是指重启动作会在Worker处理完正在执行的任务后发生,并不会中断正在运行的任务。)

小技巧:在onWorkerStart回调中require相应的php文件,当这些文件被修改后,只需要通过SIGUSR1信号即可实现服务器热更新。

1.7.7版本增加了仅重启task_worker的功能。只需向服务器发送SIGUSR2即可
样例:

$serv->reload();

swoole_server::shutdown

功能描述:关闭服务器。
函数原型

// 类成员函数public function swoole_server::shutdown()

返回:调用成功返回true,否则返回false
参数说明:无
说明
此函数可以用在worker进程内,平滑关闭全部的Worker进程。
也可向Master进程发送SIGTERM信号关闭服务器。
样例:

$serv->shutdown();

swoole_server::addtimer

功能描述:设置一个固定间隔的定时器
函数原型

// 类成员函数public function swoole_server::addtimer(int $interval);// 公共函数function swoole_server_addtimer(swoole_server $serv, int $interval);

返回:设置成功返回true,否则返回false
参数说明

参数说明
int interval定时器的时间间隔,单位为毫秒ms

说明
swoole定时器的最小颗粒是1毫秒,支持多个不同间隔的定时器。
注意不能存在2个相同间隔时间的定时器。
使用多个定时器时,其他定时器必须为最小定时器时间间隔的整数倍。

该函数只能在onWorkerStart/onConnect/onReceive/onClose回调函数中调用。
增加定时器后需要为Server设置onTimer回调函数,否则Server将无法启动。

样例:

$serv->addtimer(1000);              //1sswoole_server_addtimer($serv,20);   //20ms

swoole_server::deltimer

功能描述:删除指定的定时器。
函数原型

// 类成员函数public function swoole_server::deltimer(int $interval);

返回:无
参数说明

参数说明
int interval定时器的时间间隔,单位为毫秒ms

说明
删除间隔为interval的定时器

样例:

$serv->deltimer(1000);

swoole_server::after

功能描述:在指定的时间后执行函数
函数原型

// 类成员函数public function swoole_server::after(int $after_time_ms, mixed $callback_function, mixed params);// 公共函数function swoole_timer_after<

Swoole 简介

swoole是运行在PHP下的一个extesion扩展,实际上与普通的扩展不同。普通的扩展只是提供一个库函数。而swoole扩展在运行后会接管PHP的控制权,进入事件循环。当IO事件发生后,swoole会自动回调指定的PHP函数。

PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。

Swoole可以广泛应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。

Swoole底层内置了异步非阻塞、多线程的网络IO服务器。PHP程序员仅需处理事件回调即可,无需关心底层。与Nginx/Tornado/Node.js等全异步的框架不同,Swoole既支持全异步,也支持同步。

Swoole是开源免费的自由软件,授权协议是Apache2.0。企业和个人开发者均可免费使用Swoole的代码,并且在Swoole之上所作的修改可用于商业产品,无需开源(注:必须保留原作者的版权声明)。

Swoole的实现

swoole使用纯C编写,不依赖其他第三方库。

  • swoole并没有用libevent,所以不需要安装libevent
  • swoole并不依赖php的stream/sockets/pcntl/posix/sysvmsg等扩展

socket部分

swoole使用底层的socket系统调用。参见 sys/socket.h

IO事件循环

  • 主进程的事件循环使用select/poll,因为主线程中的文件描述符只有几个,使用select/poll即可
  • reactor线程/worker进程中使用epoll/kqueue
  • task进程没有事件循环,进程会循环阻塞读取管道

有很多人使用strace -p去查看swoole主进程只能看到poll系统调用。正确的查看方法是strace -f -p

多进程/多线程

  • 多进程使用fork()系统调用
  • 多线程使用pthread线程库

EventFd

Swoole中使用了eventfd作为线程/进程间消息通知的机制。

Timerfd

Swoole使用timerfd来实现定时器

SIgnalfd

swoole中使用了signalfd来实现对信号的屏蔽和处理。可以有效地避免线程/进程被信号打断,系统调用restart的问题。在主进程中reactor线程不会接受任何信号。


1.8.7或更高版本已完全兼容PHP7

swoole_server

强大的TCP/UDP Server框架,多线程,EventLoop,事件驱动,异步,Worker进程组,Task异步任务,毫秒定时器,SSL/TLS隧道加密。

  • swoole_http_server是swoole_server的子类,内置了Http的支持
  • swoole_websocket_server是swoole_http_server的子类,内置了WebSocket的支持

swoole_client

TCP/UDP客户端,支持同步并发调用,也支持异步事件驱动。

swoole_event

EventLoop API,让用户可以直接操作底层的事件循环,将socket,stream,管道等Linux文件加入到事件循环中。

eventloop接口仅可用于socket类型的文件描述符,不能用于磁盘文件读写

swoole_async

异步IO接口,提供了 异步文件系统IO,异步DNS查询,异步MySQL等API。包括2个重要的子模块:

  • swoole_timer,异步毫秒定时器,可以实现间隔时间或一次性的定时任务
  • file,文件系统操作的异步接口

swoole_process

进程管理模块,可以方便的创建子进程,进程间通信,进程管理。

swoole_buffer

强大的内存区管理工具,像C一样进行指针计算,又无需关心内存的申请和释放,而且不用担心内存越界,底层全部做好了。

swoole_table

基于共享内存和自旋锁实现的超高性能内存表。彻底解决线程,进程间数据共享,加锁同步等问题。

swoole_table的性能可以达到单线程每秒读写50W次

swoole 环境依赖

  • 仅支持Linux,FreeBSD,MacOS,3类操作系统
  • Linux内核版本2.3.32以上
  • PHP5.3.10以上版本,包括PHP7
  • gcc4.4以上版本或者clang
  • cmake2.4+,编译为libswoole.so作为C/C++库时需要使用cmake

PHP版本依赖

  • swoole仅支持PHP5.3.10或更高版本,建议使用PHP5.4+
  • swoole不依赖php的stream、sockets、pcntl、posix、sysvmsg等扩展。PHP只需安装最基本的扩展即可

推荐使用的Linux发行版

  • CentOS6.2+
  • Ubuntu12+
  • Debian6+

ARM平台(树莓派Raspberry PI)

  • 请使用swoole-1.7.10或更高版本
  • 使用GCC交叉编译
  • 在编译Swoole时,需要手工修改Makefile去掉-O2编译参数

MIPS平台(OpenWrt路由器)

  • 请使用swoole-1.7.21或更高版本
  • 使用GCC交叉编译

CygWin环境支持(Windows系统)

swoole-1.7.7增加了对cygwin环境的支持,在Windows环境下,可以直接使用cygwin + php 来跑swoole程序。

  • 安装cygwin,并安装gcc、make、autoconf、php 4个包
  • 下载swoole源码,在cygwin-shell中进行phpize/configure/make/make install
  • 修改php.ini,加入swoole.so

cygwin模式下需要对PHP进行简化,去掉不使用的扩展,避免进程占用内存过大,导致Fork操作失败

BashOnWindows

Windows 10系统增加了Linux子系统支持,BashOnWindows环境下也可以使用swoole

  • BashOnWindows环境下必须关闭daemonize选项

Swoole编译安装步骤

Swoole扩展是按照php标准扩展构建的。使用phpize来生成php编译配置,./configure来做编译配置检测,make进行编译,make install进行安装。

  • 请下载releases版本的swoole,直接从github主干上拉取最新代码可能会编译不过
  • 如果当前用户不是root,可能没有php目录的写权限,安装时需要sudo或者su
  • 如果是在git分支上直接git pull更新代码,重新编译前务必要执行make clean

安装准备

安装swoole前必须保证系统已经安装了下列软件

php-5.3.10 或更高版本gcc-4.4 或更高版本makeautoconf

下载地址

下载源代码包后,在终端进入源码目录,执行下面的命令进行编译和安装

cd swoolephpize./configuremake sudo make install

(注:swoole的./configure有很多额外参数,可以通过./configure --help命令查看,这里仅开启其中async-mysql项,其他均选择默认项) 这里是./configure编译配置的额外参数,用于开启某些特性

1.8.7或更高版本不再需要设置--enable-async-mysql和--enable-async-httpclient,async_mysql和async_httpclient改为内置
--enable-swoole-debug
打开调试日志,开启此选项后swoole将打印各类细节的调试日志。生产环境不要启用。
--enable-sockets
增加对sockets资源的支持,依赖sockets扩展。开启此参数,swoole_event_add就可以添加sockets扩展创建的连接到swoole的事件循环中。
--enable-async-mysql
增加异步mysql支持, 依赖mysqli和mysqlnd扩展。
--enable-async-redis
增加异步Redis客户端支持, 依赖hiredis库
--enable-async-httpclient
增加异步Http和WebSocket客户端支持
--enable-ringbuffer
开启RingBuffer内存池
此设置为试验性质,主要用于提升性能,生产环境请不要开启
--enable-openssl
启用SSL支持

PECL

swoole项目已收录到PHP官方扩展库,除了手工下载编译外,还可以通过PHP官方提供的pecl命令,一键下载安装swoole

pecl install swoole

配置php.ini

编译安装成功后,修改php.ini加入

extension=swoole.so

通过php -mphpinfo()来查看是否成功加载了swoole,如果没有可能是php.ini的路径不对,可以使用php -i |grep php.ini来定位到php.ini的绝对路径。


安装成功后通过phpinfo()查看到的信息:

安装常见错误

make或make install无法执行或编译错误

NOTICE: PHP message: PHP Warning: PHP Startup: swoole: Unable to initialize module
Module compiled with module API=20090626
PHP compiled with module API=20121212
These options need to match
in Unknown on line 0

php版本和编译时使用的phpize和php-config不对应,需要使用绝对路径来进行编译。使用绝对路径执行PHP。

/usr/local/php-5.4.17/bin/phpize./configure --with-php-config=/usr/local/php-5.4.17/bin/php-config/usr/local/php-5.4.17/bin/php server.php

缺少mysql头文件

php_mysqli_structs.h:64:23: fatal error: my_global.h: No such file or directory

没有找到mysqlclient的头文件,需要安装mysqlclient-dev

建议自行编译php,不要使用Linux包管理系统自带的php版本

缺少pcre.h头文件

fatal error: pcre.h: No such file or directory

原因是缺少pcre,需要安装libpcre

Cannot find autoconf

phpize命令需要autoconf工具,请先安装它。

make install失败

make install需要root权限,如果不是以root用户登录的,请用sudo或su,再进行安装。

修改了php.ini后,php -m或phpinfo中没有swoole

php -i|grep php.ini

查看加载的php.ini路径,确认加载了正确的php.ini。

修改php.ini,打开错误显示,查看是否存在启动时错误。

display_errors => On  display_startup_errors => On

error: too many arguments to function 'zend_exception_error'

你的PHP版本低于PHP-5.3.10,请升级PHP版本。

如果还是编译失败了怎么办?

不要气馁,加入我们的开发组QQ群:495864936,你的问题会在24小时内被解决。

构建一个Swoole基本实例

下面贴一个基本的基于swoole的echo服务器

// Serverclass 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
";    }    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}
";    }    public function onClose( $serv, $fd, $from_id ) {        echo "Client {$fd} close connection
";    }}// 启动服务器$server = new Server();

从代码中可以看出,创建一个swoole_server基本分三步: 1. 通过构造函数创建swoole_server对象 2. 调用set函数设置swoole_server的相关配置选项 3. 调用on函数设置相关回调函数 关于set配置选项以及on回调函数的具体说明,请参考我整理的swoole文档( 配置选项)

这里只给出简单介绍。onStart回调在server运行前被调用,onConnect在有新客户端连接过来时被调用,onReceive函数在有数据发送到server时被调用,onClose在有客户端断开连接时被调用。 这里就可以大概看出如何使用swoole:在onConnect处监听新的连接;在onReceive处接收数据并处理,然后可以调用send函数将处理结果发送出去;在onClose处处理客户端下线的事件。

下面贴出客户端的代码:

<?phpclass 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}]
";        }        $message = $this->client->recv();        echo "Get Message From Server:{$message}
";        fwrite(STDOUT, "请输入消息:");          $msg = trim(fgets(STDIN));        $this->client->send( $msg );    }}$client = new Client();$client->connect();

这里,通过swoole_client创建一个基于TCP的客户端实例,并调用connect函数向指定的IP及端口发起连接请求。随后即可通过recv()和send()两个函数来接收和发送请求。需要注意的是,这里我使用了默认的同步阻塞客户端,因此recv和send操作都会产生网络阻塞。

编程须知

这个频道内会详细介绍异步编程与同步编程的不同之处以及需要注意的事项。

注意事项

  • 不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞
  • exit/die是危险的,会导致worker进程退出
  • 可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些请求工作
  • PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出
  • swoole不支持set_exception_handler,必须使用try/catch方式处理异常
  • Worker进程不得共用同一个RedisMySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中。

类/函数重复定义

新手非常容易犯这个错误,由于swoole是常驻内存的,所以加载类/函数定义的文件后不会释放。因此引入类/函数的php文件时必须要使用include_oncerequire_once,否会发生cannot redeclare function/class 的致命错误。

内存管理

PHP守护进程与普通Web程序的变量生命周期、内存管理方式完全不同。请参考 swoole_server内存管理 页面。编写swoole_server或其他常驻进程时需要特别注意。

进程隔离

进程隔离也是很多新手经常遇到的问题。修改了全局变量的值,为什么不生效,原因就是全局变量在不同的进程,内存空间是隔离的,所以无效。所以使用swoole开发Server程序需要了解进程隔离问题。

  • 不同的进程中PHP变量不是共享,即使是全局变量,在A进程内修改了它的值,在B进程内是无效的
  • 如果需要在不同的Worker进程内共享数据,可以用RedisMySQL文件SwooleTableAPCushmget等工具实现
  • 不同进程的文件句柄是隔离的,所以在A进程创建的Socket连接或打开的文件,在B进程内是无效,即使是将它的fd发送到B进程也是不可用的

sleep/usleep的影响

在异步IO的程序中,不得使用sleep/usleep/time_sleep_until/time_nanosleep。(下文中使用sleep泛指所有睡眠函数)

  • sleep函数会使进程陷入睡眠阻塞
  • 直到指定的时间后操作系统才会重新唤醒当前的进程
  • sleep过程中,只有信号可以打断
  • 由于swoole的信号处理是基于signalfd实现的,所以即使发送信号也无法中断sleep

swoole提供的swoole_event_addswoole_timer_tickswoole_timer_afterswoole_process::signal异步swoole_client 在进程sleep后会停止工作。swoole_server也无法再处理新的请求。

实例程序

$serv = new swoole_server("127.0.0.1", 9501);$serv->on('receive', function ($serv, $fd, $from_id, $data) {    sleep(100);    $serv->send($fd, 'Swoole: '.$data);});$serv->start();

onReceive事件中执行了sleep函数,server在100秒内无法再收到任何客户端请求。


exit/die函数的影响

在swoole程序中禁止使用exit/die,如果PHP代码中有exit/die,当前工作的Worker进程、Task进程、User进程、以及swoole_process进程会立即退出。

建议使用try/catch的方式替换exit/die,实现中断执行跳出PHP函数调用栈。

function swoole_exit($msg){    //php-fpm的环境    if (ENV=='php')    {        exit($msg);    }    //swoole的环境    else    {        throw new SwooleExitException($msg);    }}

异常处理的方式比exit/die更友好,因为异常是可控的,exit/die不可控。在最外层进行try/catch即可捕获异常,仅终止当前的任务。Worker进程可以继续处理新的请求,而exit/die会导致进程直接退出,当前进程保存的所有变量和资源都会被销毁。如果进程内还有其他任务要处理,遇到exit/die也将全部丢弃。


while循环的影响

异步程序如果遇到死循环,事件将无法触发。异步IO程序使用Reactor模型,运行过程中必须在reactor->wait处轮询。如果遇到死循环,那么程序的控制权就在while中了,reactor无法得到控制权,无法检测事件,所以IO事件回调函数也将无法触发。

密集运算的代码不是阻塞

实例程序

$serv = new swoole_server("127.0.0.1", 9501);$serv->on('receive', function ($serv, $fd, $from_id, $data) {    while(1)    {        $i ++;    }    $serv->send($fd, 'Swoole: '.$data);});$serv->start();

onReceive事件中执行了死循环,server在无法再收到任何客户端请求,必须等待循环结束才能继续处理新的事件。



swoole优化内核参数调整

ulimit设置

ulimit -n 要调整为100000甚至更大。 命令行下执行 ulimit -n 100000即可修改。如果不能修改,需要设置 /etc/security/limits.conf,加入

* soft nofile 262140* hard nofile 262140root soft nofile 262140root hard nofile 262140* soft core unlimited* hard core unlimitedroot soft core unlimitedroot hard core unlimited

内核设置

net.unix.max_dgram_qlen = 100

swoole使用unix socket dgram来做进程间通信,如果请求量很大,需要调整此参数。系统默认为10,可以设置为100或者更大。
或者增加worker进程的数量,减少单个worker进程分配的请求量。

net.core.wmem_max

修改此参数增加socket缓存区的内存大小

net.ipv4.tcp_mem  =   379008       505344  758016net.ipv4.tcp_wmem = 4096        16384   4194304net.ipv4.tcp_rmem = 4096          87380   4194304net.core.wmem_default = 8388608net.core.rmem_default = 8388608net.core.rmem_max = 16777216net.core.wmem_max = 16777216

net.ipv4.tcp_tw_reuse

是否socket reuse,此函数的作用是Server重启时可以快速重新使用监听的端口。如果没有设置此参数,会导致server重启时发生端口未及时释放而启动失败

net.ipv4.tcp_tw_recycle

使用socket快速回收,短连接Server需要开启此参数

消息队列设置

当使用消息队列作为进程间通信方式时,需要调整此内核参数

  • kernel.msgmnb = 4203520,消息队列的最大字节数
  • kernel.msgmni = 64,最多允许创建多少个消息队列
  • kernel.msgmax = 8192,消息队列单条数据最大的长度

FreeBSD/MacOS

  • sysctl -w net.local.dgram.maxdgram=8192
  • sysctl -w net.local.dgram.recvspace=200000 修改Unix Socket的buffer区尺寸

开启CoreDump

设置内核参数

kernel.core_pattern = /data/core_files/core-%e-%p-%t

通过ulimit -c命令查看当前coredump文件的限制

ulimit -c

如果为0,需要修改/etc/security/limits.conf,进行limit设置。

开启core-dump后,一旦程序发生异常,会将进程导出到文件。对于调查程序问题有很大的帮助

其他重要配置

  • net.ipv4.tcp_syncookies=1
  • net.ipv4.tcp_max_syn_backlog=81920
  • net.ipv4.tcp_synack_retries=3
  • net.ipv4.tcp_syn_retries=3
  • net.ipv4.tcp_fin_timeout = 30
  • net.ipv4.tcp_keepalive_time = 300
  • net.ipv4.tcp_tw_reuse = 1
  • net.ipv4.tcp_tw_recycle = 1
  • net.ipv4.ip_local_port_range = 20000 65000
  • net.ipv4.tcp_max_tw_buckets = 200000
  • net.ipv4.route.max_size = 5242880

查看配置是否生效

如:修改net.unix.max_dgram_qlen = 100后,通过

cat /proc/sys/net/unix/max_dgram_qlen

如果修改成功,这里是新设置的值。


在swoole中,一个swoole_server的相关属性可以通过

$serv->set( $array configs );

函数来配置,这些配置选项使得swoole更加灵活。 示例:

$serv = new swoole_server("0.0.0.0", 9501);$serv->set(array(    'worker_num' => 8,    'max_request' => 10000,    'max_conn' => 100000,    'dispatch_mode' => 2,    'debug_mode'=> 1    'daemonize' => false,));

配置选项以及相关介绍如下:

1.worker_num

描述:指定启动的worker进程数。
说明:swoole是master-> n * worker的模式,开启的worker进程数越多,server负载能力越大,但是相应的server占有的内存也会更多。同时,当worker进程数过多时,进程间切换带来的系统开销也会更大。因此建议开启的worker进程数为cpu核数的1-4倍。
示例:

'worker_num' => 8

2.max_request

描述:每个worker进程允许处理的最大任务数。
说明:设置该值后,每个worker进程在处理完max_request个请求后就会自动重启。设置该值的主要目的是为了防止worker进程处理大量请求后可能引起的内存溢出。
示例:

'max_request' => 10000

3.max_conn

描述:服务器允许维持的最大TCP连接数
说明:设置此参数后,当服务器已有的连接数达到该值时,新的连接会被拒绝。另外,该参数的值不能超过操作系统ulimit -n的值,同时此值也不宜设置过大,因为swoole_server会一次性申请一大块内存用于存放每一个connection的信息。
示例:

'max_conn' => 10000

4.ipc_mode

描述:设置进程间的通信方式。
说明:共有三种通信方式,参数如下:

  • 1 => 使用unix socket通信
  • 2 => 使用消息队列通信
  • 3 => 使用消息队列通信,并设置为争抢模式

示例:

'ipc_mode' => 1

5.dispatch_mode

描述:指定数据包分发策略。
说明:共有三种模式,参数如下:

  • 1 => 轮循模式,收到会轮循分配给每一个worker进程
  • 2 => 固定模式,根据连接的文件描述符分配worker。这样可以保证同一个连接发来的数据只会被同一个worker处理
  • 3 => 抢占模式,主进程会根据Worker的忙闲状态选择投递,只会投递给处于闲置状态的Worker

示例:

'dispatch_mode' => 2

6.task_worker_num

描述:服务器开启的task进程数。
说明:设置此参数后,服务器会开启异步task功能。此时可以使用task方法投递异步任务。

设置此参数后,必须要给swoole_server设置onTask/onFinish两个回调函数,否则启动服务器会报错。

示例:

'task_worker_num' => 8

7.task_max_request

描述:每个task进程允许处理的最大任务数。
说明:参考max_request task_worker_num
示例:

'task_max_request' => 10000

8.task_ipc_mode

描述:设置task进程与worker进程之间通信的方式。
说明:参考ipc_mode
示例:

'task_ipc_mode' => 2

9.daemonize

描述:设置程序进入后台作为守护进程运行。
说明:长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当ssh终端退出后,程序将被终止运行。启用守护进程后,标准输入和输出会被重定向到 log_file,如果 log_file未设置,则所有输出会被丢弃。
示例:

'daemonize' => 0

10.log_file

描述:指定日志文件路径
说明:在swoole运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。注意log_file 不会自动切分文件,所以需要定期清理此文件。
示例:

'log_file' => '/data/log/swoole.log'

11.heartbeat_check_interval

描述:设置心跳检测间隔
说明:此选项表示每隔多久轮循一次,单位为秒。每次检测时遍历所有连接,如果某个连接在间隔时间内没有数据发送,则强制关闭连接(会有onClose回调)。
示例:

'heartbeat_check_interval' => 60

12.heartbeat_idle_time

描述:设置某个连接允许的最大闲置时间。
说明:该参数配合heartbeat_check_interval使用。每次遍历所有连接时,如果某个连接在heartbeat_idle_time时间内没有数据发送,则强制关闭连接。默认设置为heartbeat_check_interval * 2。
示例:

'heartbeat_idle_time' => 600

13.open_eof_check

描述:打开eof检测功能
说明:与package_eof 配合使用。此选项将检测客户端连接发来的数据,当数据包结尾是指定的package_eof 字符串时才会将数据包投递至Worker进程,否则会一直拼接数据包直到缓存溢出或超时才会终止。一旦出错,该连接会被判定为恶意连接,数据包会被丢弃并强制关闭连接。

EOF检测不会从数据中间查找eof字符串,所以Worker进程可能会同时收到多个数据包,需要在应用层代码中自行explode(" ", $data) 来拆分数据包

示例:

'open_eof_check' => true

14.package_eof

描述:设置EOF字符串
说明:package_eof最大只允许传入8个字节的字符串
示例:

'package_eof ' => '/r/n'

15.open_length_check

描述:打开包长检测
说明:包长检测提供了固定包头+包体这种格式协议的解析,。启用后,可以保证Worker进程onReceive每次都会收到一个完整的数据包。
示例:

'open_length_check' => true

16.package_length_offset

描述:包头中第几个字节开始存放了长度字段
说明:配合open_length_check使用,用于指明长度字段的位置。
示例:

'package_length_offset' => 5

17.package_body_offset

描述:从第几个字节开始计算长度。
说明:配合open_length_check使用,用于指明包头的长度。
示例:

'package_body_offset' => 10

18.package_length_type

描述:指定包长字段的类型
说明:配合open_length_check使用,指定长度字段的类型,参数如下:

  • 's' => int16_t 机器字节序
  • 'S' => uint16_t 机器字节序
  • 'n' => uint16_t 大端字节序
  • ’N‘ => uint32_t 大端字节序
  • 'L' => uint32_t 机器字节序
  • 'l' => int 机器字节序

示例:

'package_length_type' => 'N'

19.package_max_length

描述:设置最大数据包尺寸
说明:该值决定了数据包缓存区的大小。如果缓存的数据超过了该值,则会引发错误。具体错误处理由开启的协议解析的类型决定。
示例:

'package_max_length' => 8192

20.open_cpu_affinity

描述:启用CPU亲和性设置
说明:在多核的硬件平台中,启用此特性会将swoole的reactor线程/worker进程绑定到固定的一个核上。可以避免进程/线程的运行时在多个核之间互相切换,提高CPU Cache的命中率。
示例:

'open_cpu_affinity' => true

21.open_tcp_nodelay

描述:启用open_tcp_nodelay
说明:开启后TCP连接发送数据时会无关闭Nagle合并算法,立即发往客户端连接。在某些场景下,如http服务器,可以提升响应速度。
示例:

'open_tcp_nodelay' => true

22.tcp_defer_accept

描述:启用tcp_defer_accept特性
说明:启动后,只有一个TCP连接有数据发送时才会触发accept。
示例:

'tcp_defer_accept' => true

23.ssl_cert_file和ssl_key_file

描述:设置SSL隧道加密
说明:设置值为一个文件名字符串,指定cert证书key的路径。
示例:

'ssl_cert_file' => '/config/ssl.crt','ssl_key_file' => '/config//ssl.key',

24.open_tcp_keepalive

描述:打开TCP的KEEP_ALIVE选项
说明:使用TCP内置的keep_alive属性,用于保证连接不会因为长时闲置而被关闭。
示例:

'open_tcp_keepalive' => true

25.tcp_keepidle

描述:指定探测间隔。
说明:配合open_tcp_keepalive使用,如果某个连接在tcp_keepidle内没有任何数据来往,则进行探测。
示例:

'tcp_keepidle' => 600

26.tcp_keepinterval

描述:指定探测时的发包间隔
说明:配合open_tcp_keepalive使用
示例:

'tcp_keepinterval' => 60

27.tcp_keepcount

描述:指定探测的尝试次数
说明:配合open_tcp_keepalive使用,若tcp_keepcount次尝试后仍无响应,则判定连接已关闭。
示例:

'tcp_keepcount' => 5

28.backlog

描述:指定Listen队列长度
说明:此参数将决定最多同时有多少个等待accept的连接。
示例:

'backlog' => 128

29.reactor_num

描述:指定Reactor线程数
说明:设置主进程内事件处理线程的数量,默认会启用CPU核数相同的数量, 一般设置为CPU核数的1-4倍,最大不得超过CPU核数*4。
示例:

'reactor_num' => 8

30.task_tmpdir

描述:设置task的数据临时目录
说明:在swoole_server中,如果投递的数据超过8192字节,将启用临时文件来保存数据。这里的task_tmpdir就是用来设置临时文件保存的位置。

需要swoole-1.7.7+

示例:

'task_tmpdir' => '/tmp/task/'

说明

除以上29个选项外,还有几个不常用或者已废弃的选项没有给出。以后如果有实际需求,会补充说明。

在swoole中,一个swoole_server的拥有若干回调函数,这些回调函数决定了swoole_server的相关功能。可以通过

$serv->on( string $event, mixed $callback );

函数来配置. 示例:

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();    }}

全部回调函数以及相关介绍如下:

[TOC]

1.onReceive

描述:接收数据的回调
函数原型:

function onReceive( swoole_server $serv, $fd, $from_id, $data );
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用
$data接收到的数据

说明:每当server接收到客户端发来的数据后,就会通过onReceive回调将数据投递给Worker。如果开启了协议检测,则会在收到完整数据包之后才会响应回调。注意,必须设置该回调函数,否则无法启动服务器。

2.onStart

描述:服务器启动的回调
函数原型:

function onStart( swoole_server $serv);
参数描述
$servswoole_server对象

说明:
onStart事件在Master进程的主线程中被调用。在此回调响应之前Swoole Server已进行了如下操作

  • 已创建了manager进程
  • 已创建了worker子进程
  • 已监听所有TCP/UDP端口
  • 已监听了定时器

接下来要执行

  • 主Reactor开始接收事件,客户端可以connect到Server

onStart回调中,仅允许echo、打印Log、修改进程名称。不得执行其他操作。onWorkerStart和onStart回调是在不同进程中并行执行的,不存在先后顺序。 可以在onStart回调中,将$serv->master_pid$serv->manager_pid的值保存到一个文件中。这样可以编写脚本,向这两个PID发送信号来实现关闭和重启的操作。
从1.7.5+ Master进程内不再支持定时器,onMasterConnect/onMasterClose2个事件回调也彻底移除。Master进程内不再保留任何PHP的接口。
在onStart中创建的全局资源对象不能在worker进程中被使用,因为发生onStart调用时,worker进程已经创建好了。新创建的对象在主进程内,worker进程无法访问到此内存区域,因此全局对象创建的代码需要放置在swoole_server_start之前。

3.onWorkerStart

描述:Worker进程启动的回调
函数原型:

function onWorkerStart( swoole_server $servint $worker_id);
参数描述
$servswoole_server对象
$worker_idWorker进程的id

说明:此事件在worker进程/task_worker启动时发生。

发生PHP致命错误或者代码中主动调用exit时,Worker/Task进程会退出,管理进程会重新创建新的进程 onWorkerStart/onStart是并发执行的,没有先后顺序

通过$worker_id参数的值来,判断worker是普通worker还是task_worker。$worker_id>= $serv->setting['worker_num'] 时表示这个进程是task_worker。
如果想使用swoole_server_reload实现代码重载入,必须在workerStart中require你的业务文件,而不是在文件头部。在onWorkerStart调用之前已包含的文件,不会重新载入代码。
可以将公用的,不易变的php文件放置到onWorkerStart之前。这样虽然不能重载入代码,但所有worker是共享的,不需要额外的内存来保存这些数据。
onWorkerStart之后的代码每个worker都需要在内存中保存一份 $worker_id是一个从0-$worker_num之间的数字,表示这个worker进程的ID $worker_id和进程PID没有任何关系

4.onConnect

描述:新连接接入时的回调
函数原型:

function onConnect( swoole_server $servint $fd, int $from_id);
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用

说明:有新的连接进入时,在worker进程中回调。onConnect/onClose这2个回调发生在worker进程内,而不是主进程。如果需要在主进程处理连接/关闭事件,请注册onMasterConnect/onMasterClose回调。onMasterConnect/onMasterClose回调总是先于onConnect/onClose被执行

5.onClose

描述:连接关闭时的回调
函数原型:

function onClose( swoole_server $servint $fd, int $from_id);
参数描述
$servswoole_server对象
$fd连接的描述符
$from_idreactor的id,无用

说明:TCP客户端连接关闭后,在worker进程中回调此函数。无论close由客户端发起还是服务器端主动调用swoole_server_close关闭连接,都会触发此事件。 因此只要连接关闭,就一定会回调此函数。

6.onTask

描述:task_worker进程处理任务的回调
函数原型:

function onTask(swoole_server $serv, int $task_id, int $from_id, string $data);
参数描述
$servswoole_server对象
$task_id任务ID
$from_id来自于哪个worker进程
$data任务内容

说明:在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。可以直接将任务结果字符串通过return方式返回给worker进程。worker进程将在onFinish回调中收到结果。注:如果serv->set(array('task_worker_num' => 8)) task_id 并不是从1-8 而是递增的。

7.onFinish

描述:task_worker进程处理任务结束的回调
函数原型:

function onFinish(swoole_server $serv, int $task_id, string $data);
参数描述
$servswoole_server对象
$task_id任务ID
$data任务结果

说明:在此函数中会收到任务处理的结果,通过task_id和worker_id来区分不同的任务。

8.onTimer

描述:定时器触发的回调
函数原型:

function onTimer(swoole_server $serv, int $interval);
参数描述
$servswoole_server对象
$interval定时的间隔

说明:定时器被触发时,该函数被调用。通过interval来区分不同时间间隔的定时器。


swoole_server函数列表

Table of Contents


swoole_server::__construct

功能描述:创建一个swoole_server资源对象
函数原型

// 类成员函数public function swoole_server::__construct(string $host, int $port, int $mode = SWOOLE_PROCESS,    int $sock_type = SWOOLE_SOCK_TCP);// 公共函数function swoole_server_create(string $host, int $port, int $mode = SWOOLE_PROCESS,    int $sock_type = SWOOLE_SOCK_TCP);

返回:一个swoole_server对象
参数说明

参数说明
string host监听的IP地址
int port监听的端口号
int mode运行模式
int sock_type指定的socket类型

说明: host、port、socket_type的详细说明见swoole_server::addlistener
mode指定了swoole_server的运行模式,共有如下三种:

mode类型说明
SWOOLE_BASEBase模式传统的异步非阻塞Server。在Reactor内直接回调PHP的函数。如果回调函数中有阻塞操作会导致Server退化为同步模式。worker_num参数对与BASE模式仍然有效,swoole会启动多个Reactor进程
SWOOLE_THREAD线程模式(已废弃)多线程Worker模式,Reactor线程来处理网络事件轮询,读取数据。得到的请求交给Worker线程去处理。多线程模式比进程模式轻量一些,而且线程之间可以共享堆栈和资源。 访问共享内存时会有同步问题,需要使用Swoole提供的锁机制来保护数据。
SWOOLE_PROCESS进程模式(默认)Swoole提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行,适合业务逻辑非常复杂的场景。

样例:

$serv = new swoole_server("127.0.0.1" , 8888 , SWOOLE_PROCESS , SWOOLE_SOCK_TCP);

swoole_server::set

功能描述:设置swoole_server运行时的各项参数
函数原型

// 类成员函数public function swoole_server::set(array $setting);// 公共函数function swoole_server_set(swoole_server $server, array $setting);

返回:无
参数说明

参数说明
array setting配置选项数组,采用key-value形式

说明
该函数必须在swoole_server::start函数调用前调用。
全部swoole_server的配置参数点此查看
样例:

$serv->set(    array(        'worker_num' => 8,        'max_request' => 10000,        'max_conn' => 100000,        'dispatch_mode' => 2,        'debug_mode'=> 1        'daemonize' => false,    ));

swoole_server::on

功能描述:绑定swoole_server的相关回调函数
函数原型

// 类成员函数public function bool swoole_server->on(string $event, mixed $callback);

返回:设置成功返回true,否则返回false
参数说明

参数说明
string event回调的名称(大小写不敏感)
mixed callback回调的PHP函数,可以是函数名的字符串,类静态方法,对象方法数组,匿名函数

说明
该函数必须在swoole_server::start函数调用前调用。
此方法与swoole_server::handler功能相同,作用是与swoole_client风格保持一致。
swoole_server::on中事件名称字符串不要加on。
全部的回调函数列表点此查看
样例:

$serv->on('connect', function ($serv, $fd){    echo "Client:Connect.
";});$serv->on('receive', array( $myclass, 'onReceive' ) ); // onReceive是myclass的成员函数

swoole_server::addlistener

功能描述:给swoole_server增加一个监听的地址和端口
函数原型

// 类成员函数public function swoole_server::addlistener(string $host, int $port, $type = SWOOLE_SOCK_TCP);// 公共函数function swoole_server_addlisten(swoole_server $serv, string $host, int $port, $type = SWOOLE_SOCK_TCP);

返回:无
参数说明

参数说明
string host监听的IP地址
int port监听的端口号
int sock_type指定的socket类型

说明: swoole支持如下socket类型:

sock_type说明
SWOOLE_TCP/SWOOLE_SOCK_TCPTCP IPv4 Socket
SWOOLE_TCP6/SWOOLE_SOCK_TCP6TCP IPv6 Socket
SWOOLE_UDP/SWOOLE_SOCK_UDPUDP IPv4 Socket
SWOOLE_UDP6/SWOOLE_SOCK_UDP6UDP IPv4 Socket
SWOOLE_UNIX_DGRAMUnix UDP Socket
SWOOLE_UNIX_STREAMUnix TCP Socket

Unix Socket仅在1.7.1+后可用,此模式下host参数必须填写可访问的文件路径,port参数忽略
Unix Socket模式下,客户端fd将不再是数字,而是一个文件路径的字符串
SWOOLE_TCP等是1.7.0+后提供的简写方式,与1.7.0前的SWOOLE_SOCK_TCP是等同的

样例:

$serv->addlistener("127.0.0.1", 9502, SWOOLE_SOCK_TCP);$serv->addlistener("192.168.1.100", 9503, SWOOLE_SOCK_TCP);$serv->addlistener("0.0.0.0", 9504, SWOOLE_SOCK_UDP);$serv->addlistener("/var/run/myserv.sock", 0, SWOOLE_UNIX_STREAM);swoole_server_addlisten($serv, "127.0.0.1", 9502, SWOOLE_SOCK_TCP);

swoole_server::handler

功能描述:设置Server的事件回调函数
函数原型

// 类成员函数public function swoole_server::handler(string $event_name, mixed $event_callback_function);// 公共函数function swoole_server_handler(swoole_server $serv, string $event_name, mixed $event_callback_function);

返回:设置成功返回true,否则返回false
参数说明

参数说明
string event_name回调的名称(大小写不敏感)
mixed event_callback_function回调的PHP函数,可以是函数名的字符串,类静态方法,对象方法数组,匿名函数

说明: 该函数必须在swoole_server::start函数调用前调用。
事件名称字符串要加on。
全部的回调函数列表点此查看

onConnect/onClose/onReceive这3个回调函数必须设置。其他事件回调函数可选
如果设定了timer定时器,onTimer事件回调函数也必须设置
如果启用了task_worker,onTask/onFinish事件回调函数必须设置

样例:

$serv->handler('onStart', 'my_onStart');$serv->handler('onStart', array($this, 'my_onStart'));$serv->handler('onStart', 'myClass::onStart');

swoole_server::start

功能描述:启动server,开始监听所有TCP/UDP端口
函数原型

// 类成员函数public function swoole_server::start()

返回:启动成功返回true,否则返回false
参数说明:无
说明
启动成功后会创建worker_num+2个进程:Master进程+Manager进程+worker_num 个 Worker进程。
另外。启用task_worker会增加task_worker_num个Worker进程
三种进程的说明如下:

进程类型说明
Master进程主进程内有多个Reactor线程,基于epoll/kqueue进行网络事件轮询。收到数据后转发到Worker进程去处理
Manager进程对所有Worker进程进行管理,Worker进程生命周期结束或者发生异常时自动回收,并创建新的Worker进程
Worker进程对收到的数据进行处理,包括协议解析和响应请求

样例:

$serv->start();

swoole_server::reload

功能描述:重启所有worker进程。
函数原型

// 类成员函数public function swoole_server::reload()

返回:调用成功返回true,否则返回false
参数说明:无
说明
调用后会向Manager发送一个SIGUSR1信号,平滑重启全部的Worker进程(所谓平滑重启,是指重启动作会在Worker处理完正在执行的任务后发生,并不会中断正在运行的任务。)

小技巧:在onWorkerStart回调中require相应的php文件,当这些文件被修改后,只需要通过SIGUSR1信号即可实现服务器热更新。

1.7.7版本增加了仅重启task_worker的功能。只需向服务器发送SIGUSR2即可
样例:

$serv->reload();

swoole_server::shutdown

功能描述:关闭服务器。
函数原型

// 类成员函数public function swoole_server::shutdown()

返回:调用成功返回true,否则返回false
参数说明:无
说明
此函数可以用在worker进程内,平滑关闭全部的Worker进程。
也可向Master进程发送SIGTERM信号关闭服务器。
样例:

$serv->shutdown();

swoole_server::addtimer

功能描述:设置一个固定间隔的定时器
函数原型

// 类成员函数public function swoole_server::addtimer(int $interval);// 公共函数function swoole_server_addtimer(swoole_server $serv, int $interval);

返回:设置成功返回true,否则返回false
参数说明

参数说明
int interval定时器的时间间隔,单位为毫秒ms

说明
swoole定时器的最小颗粒是1毫秒,支持多个不同间隔的定时器。
注意不能存在2个相同间隔时间的定时器。
使用多个定时器时,其他定时器必须为最小定时器时间间隔的整数倍。

该函数只能在onWorkerStart/onConnect/onReceive/onClose回调函数中调用。
增加定时器后需要为Server设置onTimer回调函数,否则Server将无法启动。

样例:

$serv->addtimer(1000);              //1sswoole_server_addtimer($serv,20);   //20ms

swoole_server::deltimer

功能描述:删除指定的定时器。
函数原型

// 类成员函数public function swoole_server::deltimer(int $interval);

返回:无
参数说明

参数说明
int interval定时器的时间间隔,单位为毫秒ms

说明
删除间隔为interval的定时器

样例:

$serv->deltimer(1000);

swoole_server::after

功能描述:在指定的时间后执行函数
函数原型

// 类成员函数public function swoole_server::after(int $after_time_ms, mixed $callback_function, mixed params);// 公共函数function swoole_timer_after<

Swoole Server介绍

创建一个异步服务器程序,支持TCP、UDP、UnixSocket 3种协议,支持IPv4和IPv6,支持SSL/TLS单向双向证书的隧道加密。使用者无需关注底层实现细节,仅需要设置网络事件的回调函数即可。

swoole_server只能用于php-cli环境,否则会抛出致命错误

构建Server对象

$serv = new swoole_server("127.0.0.1", 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP);

设置运行时参数

$serv->set(array(    'worker_num' => 4,    'daemonize' => true,    'backlog' => 128,));

注册事件回调函数

$serv->on('Connect', 'my_onConnect');$serv->on('Receive', 'my_onReceive');$serv->on('Close', 'my_onClose');

PHP中可以使用4种回调函数的风格

启动服务器

$serv->start();

属性列表

$serv->manager_pid;  //管理进程的PID,通过向管理进程发送SIGUSR1信号可实现柔性重启$serv->master_pid;  //主进程的PID,通过向主进程发送SIGTERM信号可安全关闭服务器$serv->connections; //当前服务器的客户端连接,可使用foreach遍历所有连接

运行流程图

Swoole扩展架构图

进程/线程结构图

Swoole进程/线程结构图

简单Swoole tcp server实例:

$serv = new SwooleServer("127.0.0.1", 9501);//设置服务器参数$serv->set(array(    'worker_num' => 8,   //工作进程数量    'daemonize' => true, //是否作为守护进程));//设置事件回调函数$serv->on('connect', function ($serv, $fd) {    echo "Client:Connect.
";});$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {    $serv->send($fd, 'Swoole: ' . $data);    $serv->close($fd);});$serv->on('close', function ($serv, $fd) {    echo "Client: Close.
";});//启动服务器$serv->start();

Swoole Client介绍

swoole_client提供了tcp/udp socket的客户端的封装代码,使用时仅需 new swoole_client即可。 swoole的socket client对比PHP提供的stream族函数有哪些好处:

  • stream函数存在超时设置的陷阱和Bug,一旦没处理好会导致Server端长时间阻塞
  • fread有8192长度限制,无法支持UDP的大包
  • swoole_client支持waitall,在知道包长度的情况下可以一次取完,不必循环取。
  • swoole_client支持UDP connect,解决了UDP串包问题
  • swoole_client是纯C的代码,专门处理socket,stream函数非常复杂。swoole_client性能更好

除了普通的同步阻塞+select的使用方法外,swoole_client还支持异步非阻塞回调。

同步阻塞客户端

$client = new swoole_client(SWOOLE_SOCK_TCP);if (!$client->connect('127.0.0.1', 9501, -1)){    exit("connect failed. Error: {$client->errCode}
");}$client->send("hello world
");echo $client->recv();$client->close();

php-fpm/apache环境下只能使用同步客户端
apache环境下仅支持prefork多进程模式,不支持prework多线程

异步非阻塞客户端

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);$client->on("connect", function(swoole_client $cli) {    $cli->send("GET / HTTP/1.1

");});$client->on("receive", function(swoole_client $cli, $data){    echo "Receive: $data";    $cli->send(str_repeat('A', 100)."
");    sleep(1);});$client->on("error", function(swoole_client $cli){    echo "error
";});$client->on("close", function(swoole_client $cli){    echo "Connection close
";});$client->connect('127.0.0.1', 9501);

异步客户端只能使用在cli命令行环境

Swoole HttpServer介绍

swoole-1.7.7增加了内置Http服务器的支持,通过几行代码即可写出一个异步非阻塞多进程的Http服务器。

常见使用场景:因为swoole是在cli命令下执行的,在传统通过nginx+fastcgi模式下很多root的shell无法执行,而使用这个swoole服务器就很好的控制rsync,git,svn等。

$http = new swoole_http_server("127.0.0.1", 9501);$http->on('request', function ($request, $response) {    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");});$http->start();

swoole_http_server对Http协议的支持并不完整,建议仅作为应用服务器。并且在前端增加Nginx作为代理

简单实例:

$serv = new SwooleHttpServer("127.0.0.1", 9502);$serv->on('Request', function($request, $response) {    var_dump($request->get);    var_dump($request->post);    var_dump($request->cookie);    var_dump($request->files);    var_dump($request->header);    var_dump($request->server);    $response->cookie("User", "Swoole");    $response->header("X-Server", "Swoole");    $response->end("<h1>Hello Swoole!</h1>");});$serv->start();

通过使用apache bench工具进行压力测试,在Inter Core-I5 4核 + 8G内存的普通PC机器上,swoole_http_server可以达到近11万QPS。远远超过php-fpm,golang自带http服务器,node.js自带http服务器。性能几乎接近与Nginx的静态文件处理。

ab -c 200 -n 200000 -k http://127.0.0.1:9501


使用Http2协议

  • 需要依赖nghttp2库,下载nghttp2后编译安装
  • 使用Http2协议必须开启openssl
  • 需要高版本openssl必须支持TLS1.2ALPNNPN
./configure --enable-openssl --enable-http2

设置http服务器的open_http2_protocoltrue

$serv->set([    'ssl_cert_file' => $ssl_dir . '/ssl.crt',    'ssl_key_file' => $ssl_dir . '/ssl.key',    'open_http2_protocol' => true,]);

nginx+swoole配置

server {    root /data/wwwroot/;    server_name local.swoole.com;    location / {        if (!-e $request_filename) {             proxy_pass http://127.0.0.1:9501;             proxy_http_version 1.1;             proxy_set_header Connection "keep-alive";        }    }}


异步Http/WebSocket客户端

Swoole-1.8.0版本增加了对异步Http/WebSocket客户端的支持。底层是用纯C编写,拥有超高的性能。

异步HTTP客户端目前仍在实验阶段,请谨慎使用

启用Http客户端

  • 1.8.6版本之前,需要在编译swoole时增加--enable-async-httpclient来开启此功能。
  • swoole_http_client不依赖任何第三方库
  • 支持Http-Chunk、Keep-Alive特性,暂不支持form-data格式
  • Http协议版本为HTTP/1.1
  • gzip压缩格式支持需要依赖zlib库

构造方法

function swoole_http_client->__construct(string $ip, int port, bool $ssl = false);
  • $ip 目标服务器的IP地址,可使用swoole_async_dns_lookup查询域名对应的IP地址
  • $port 目标服务器的端口,一般http为80,https为443
  • $ssl 是否启用SSL/TLS隧道加密,如果目标服务器是https必须设置$ssl参数为true


Swoole WebSocket介绍

swoole-1.7.9 增加了内置的websocket服务器支持,通过几行PHP代码就可以写出一个异步非阻塞多进程的WebSocket服务器。

常见使用场景:我们在使用php开发的时候,原生最不好用的是socket类库了,而在开发IM和及时通信项目是,我们现在有了新的选择后端使用php Swoole WebSocket + 前端 html5 WebSocket;

简单实例:

$server = new swoole_websocket_server("0.0.0.0", 9501);$server->on('open', function (swoole_websocket_server $server, $request) {    echo "server: handshake success with fd{$request->fd}
";});$server->on('message', function (swoole_websocket_server $server, $frame) {    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}
";    $server->push($frame->fd, "this is server");});$server->on('close', function ($ser, $fd) {    echo "client {$fd} closed
";});$server->start();

onRequest回调

swoole_websocket_server 继承自 swoole_http_server

  • 设置了onRequest回调,websocket服务器也可以同时作为http服务器
  • 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面

客户端

  • Chrome/Firefox/高版本IE/Safari等浏览器内置了JS语言的WebSocket客户端
  • 异步的PHP程序中可以使用SwooleHttpClient作为WebSocket客户端
  • apache/php-fpm或其他同步阻塞的PHP程序中可以使用swoole/framework提供的同步WebSocket客户端
  • 非WebSocket客户端不能与WebSocket服务器通信
甚至你还可以结合使用socket.io来配合开发

Swoole RedisServer异步客户端介绍

Swoole-1.8.14版本增加一个兼容Redis服务器端协议的Server框架,可基于此框架实现Redis协议的服务器程序。SwooleRedisServer继承自SwooleServer,可调用父类提供的所有方法。

RedisServer不需要设置onReceive回调。实例程序:https://github.com/swoole/swoole-src/blob/master/examples/redis/server.php

可用的客户端

  • 任意编程语言的redis客户端,包括PHP的redis扩展和phpredis库
  • Swoole扩展提供的异步Redis客户端
  • Redis提供的命令行工具,包括redis-cliredis-benchmark
注意:Swoole-1.8.0版本增加了对异步Redis客户端的支持,基于redis官方提供的hiredis库实现。Swoole提供了__call魔术方法,来映射绝大部分Redis指令。

编译安装hiredis

使用Redis客户端,需要安装hiredis库。下载hiredis源码后,执行

make -jsudo make installsudo ldconfig

启用异步Redis客户端

编译swoole是,在configure指令中加入--enable-async-redis

./configure --enable-async-redismake cleanmake -jsudo make install

简单实例:

$redis = new SwooleRedis;$redis->connect('127.0.0.1', 6379, function ($redis, $result) {    $redis->set('test_key', 'value', function ($redis, $result) {        $redis->get('test_key', function ($redis, $result) {            var_dump($result);        });    });});$cli = new SwooleHttpClient('127.0.0.1', 80);$cli->setHeaders(array('User-Agent' => 'swoole-http-client'));$cli->setCookies(array('test' => 'value'));$cli->post('/dump.php', array("test" => 'abc'), function ($cli) {    var_dump($cli->body);    $cli->get('/index.php', function ($cli) {        var_dump($cli->cookies);        var_dump($cli->headers);    });});

Swoole异步MySQL介绍

MySQL异步是指将MySQL连接事件驱动化,这样就编程了非阻塞IO。使用Swoole可以实现mysql异步链接,Mysql连接池等。
  • 异步MySQL客户端依赖PHP的 mysqlnd 和 mysqli 2个扩展,请使用php -mphpinfo确认PHP是否有这2个扩展。
  • 另外需要在编译swoole时制定--enable-async-mysql

1.8.6版本已移除对mysqlimysqlnd扩展的依赖,并改为内置,无需额外的编译参数开启

简单实例:

$db = new SwooleMySQL;$server = array(    'host' => '127.0.0.1',    'user' => 'test',    'password' => 'test',    'database' => 'test',);$db->connect($server, function ($db, $result) {    $db->query("show tables", function (SwooleMySQL $db, $result) {        if ($result === false) {            var_dump($db->error, $db->errno);        } elseif ($result === true) {            var_dump($db->affected_rows, $db->insert_id);        } else {            var_dump($result);            $db->close();        }    });});

MySQL长连接

MySQL短连接每次请求操作数据库都需要建立与MySQL服务器建立TCP连接,这是需要时间开销的。TCP连接需要3次网络通信。这样就增加了一定的延时和额外的IO消耗。请求结束后会关闭MySQL连接,还会发生3/4次网络通信。

close操作不会增加响应延时,原因是close后是由操作系统自动进行通信的,应用程序感知不到

长连接就可以避免每次请求都创建连接的开销,节省了时间和IO消耗。提升了PHP程序的性能。

断线重连

在cli环境下,PHP程序需要长时间运行,客户端与MySQL服务器之间的TCP连接是不稳定的。

  • MySQL-Server会在一定时间内自动切断连接
  • PHP程序遇到空闲期时长时间没有MySQL查询,MySQL-Server也会切断连接回收资源
  • 其他情况,在MySQL服务器中执行kill process杀掉某个连接,MySQL服务器重启

这时PHP程序中的MySQL连接就失效了。如果仍然执行mysql_query,就会报一个“MySQL server has gone away”的错误。程序处理不到就直接遇到致命错误并退出了。所以PHP程序中需要断线重连。

有很多人提出了mysql_ping的方案,每次mysql_query进行连接检测或者定时连接检测。这个方案不是最好的。原因是

  • mysql_ping需要主动侦测连接,带来了额外的消耗
  • 定时执行mysql_ping不能解决问题,如刚刚执行过mysql_ping检测之后,连接就关闭了

最佳的方案是,进行断线重连 。它的原理是:

  1. mysql_query执行后检测返回值
  2. 如果mysql_query返回失败,检测错误码发现为2006/2013(这2个错误表示连接失败),再执行一次mysql_connect
  3. 执行mysql_connect后,重新执行mysql_query,这时必然会成功,因为已经重新建立了连接
  4. 如果mysql_query返回成功,那么连接是有效的,这是一次正常的调用

可参考swoole_framework中的代码

MySQL异步

MySQL异步是指将MySQL连接事件驱动化,这样就编程了非阻塞IO。数据库操作并不会阻塞进程,在MySQL-Server返回结果时再执行对应的逻辑。

有几个点需要注意一下:

  • 异步MySQL并没有节省SQL执行的时间
  • 一个MySQL连接同时只能执行1个SQL,如果异步MySQL存在并发那么必须创建多个MySQL连接

异步回调程序中,异步MySQL并没有提升性能。异步最大的好处是可以高并发,如果并发1万个请求,那么就需要建立1万个MySQL连接,这会给MySQL-Server带来巨大的压力。

MySQL是根据连接数分配资源的,一个连接需要开启一个线程。1000连接那么需要维持1000线程才可以。线程数量增加后,线程间切换会占用大量CPU资源
MySQL短连接反而不会出现此问题,因为短连接在使用完后就释放了。不会占用MySQL-Server的连接资源

虽然应用层代码使用异步回调避免了自身的阻塞,实际上真正的瓶颈是数据库服务器。异步MySQL还带来了额外的编程复杂度,所以除非是特殊场景的需求,否则不建议使用异步MySQL。

如果程序中坚持要使用异步,那么必须是异步MySQL+连接池的形式。超过规定的MySQL最大连接后,应当对SQL请求进行排队,而不是创建新连接,避免大量并发请求导致MySQL服务器崩溃。

MySQL连接池

连接池是可以有效降低MySQL-Server负载的。原理是 连接池使用一个共享资源的模式,如并发100个请求,实际上并不是每个请求的所有时间都在执行SQL查询。这样100个请求,共享20个MySQL连接就可以满足需求了。当一个请求操作完数据库后,开始进入模板渲染等流程,这时就会释放数据库连接给其他的请求使用。

连接池仅在超大型应用中才有价值。普通的应用采用MySQL长连接方案,每个php-fpm创建一个MySQL连接,每台机器开启100个php-fpm进程。如果有10台机器,每台机器并发的请求为100。实际上只需要创建1000个MySQL连接就能满足需求,数据库的压力并不大。即使有100台机器,硬件配置好的存储服务器依然可以承受。

达到数百或者数千台应用服务器时,MySQL服务器就需要维持十万级的连接。这时数据库的压力就会非常大了。连接池技术就可以派上用场了,可以大大降低数据库连接数。

基于swoole的AsyncTask模块实现的连接池是完美方案,编程简单,没有数据同步和锁的问题。甚至可以多个服务共享连接池。缺点是1, 灵活性不如多线程连接池,无法动态增减连接。2, 有一次进程间通信的开销。
node.js/ngx_lua等在多进程的模式下,无法开发出真正的连接池,除非也像swoole_task这样来实现


Swoole AsyncIO异步文件读写介绍

swoole1.6.12后增加了异步文件读写,异步DNS等特性。自此建立了完整的异步并行API。

  • swoole_server的Task进程是同步阻塞的,没有EventLoop,因此无法使除定时器之外的用任何异步IO
  • signalfd是Linux2.6.27提供文件句柄方式处理信号特性,优点是可以将信号加入到EventLoop中,Reactor操作不会被信号打断提高了性能。缺点是有些同步阻塞的程序可能会出现问题,无法从阻塞中中断,可以使用swoole_async_set关闭signalfd特性


Swoole支持3种类型的异步文件读写IO,可以使用swoole_async_set来设置AIO模式。

swoole_async_set

此函数可以设置异步IO相关的选项。

swoole_async_set(array $setting);
  • thread_num 设置异步文件IO线程的数量
  • aio_mode 设置异步文件IO的操作模式,目前支持SWOOLE_AIO_BASE(使用类似于Node.js的线程池同步阻塞模拟异步)、SWOOLE_AIO_LINUX(Linux Native AIO) 2种模式
  • enable_signalfd 开启和关闭signalfd特性的使用
  • socket_buffer_size 设置SOCKET内存缓存区尺寸
  • socket_dontwait 在内存缓存区已满的情况下禁止底层阻塞等待

Linux Native AIO的优点是由内核支持是真正的异步文件IO,缺点是只支持DirectIO,无法利用到系统的PageCache

swoole_async模块目前为实验性质,不建议在生产环境使用,请使用PHP的文件读写函数。

Linux原生异步IO

基于Linux Native AIO系统调用,是真正的异步IO,并非阻塞模拟。

优点:

  • 所有操作均在一个线程内完成,不需要开线程池
  • 不依赖线程执行IO,所以并发可以非常大

缺点:

  • 只支持DriectIO,无法利用PageCache,所有对文件读写都会直接操作磁盘

线程池模式异步IO

基于线程池模拟实现,文件读写请求投递到任务队列,然后由AIO线程读写文件,完成后通知主线程。AIO线程本身是同步阻塞的。所以并非真正的异步IO。

优点:

  • 可以利用操作系统PageCache,读写热数据性能非常高,等于读内存

可修改thread_num项设置启用的AIO线程数量

缺点:

  • 并发较差,不支持同时读写大量文件,最大并发受限与AIO的线程数量

简单实例:

$fp = stream_socket_client("tcp://127.0.0.1:80", $code, $msg, 3);$http_request = "GET /index.html HTTP/1.1

";fwrite($fp, $http_request);SwooleEvent::add($fp, function($fp){    echo fread($fp, 8192);    swoole_event_del($fp);    fclose($fp);});SwooleTimer::after(2000, function() {    echo "2000ms timeout
";});SwooleTimer::tick(1000, function() {    echo "1000ms interval
";});



Swoole Task异步任务介绍

swoole 的异步任务task系统可以很方便的为我们在开发的过程中调用异步任务的执行,而无需等待。

常见使用场景:

task模块用来做一些异步的慢速任务,比如webim中发广播,发送邮件,异步订单处理、异步支付处理等。

  • task进程必须是同步阻塞的
  • task进程支持定时器

node.js 假如有10万个连接,要发广播时,那会循环10万次,这时候程序不能做任何事情,不能接受新的连接,也不能收包发包。

而swoole不同,丢给task进程之后,worker进程可以继续处理新的数据请求。任务完成后会异步地通知worker进程告诉它此任务已经完成。

当然task模块的作用还不仅如此,实现PHP的数据库连接池,异步队列等,还需要进一步挖掘。

简单实例:

$serv = new SwooleServer("127.0.0.1", 9502);$serv->set(array('task_worker_num' => 4));$serv->on('Receive', function($serv, $fd, $from_id, $data) {    $task_id = $serv->task("Async");    echo "Dispath AsyncTask: id=$task_id
";});$serv->on('Task', function ($serv, $task_id, $from_id, $data) {    echo "New AsyncTask[id=$task_id]".PHP_EOL;    $serv->finish("$data -> OK");});$serv->on('Finish', function ($serv, $task_id, $data) {    echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;});$serv->start();

swoole_server task异步任务介绍

投递一个异步任务到task_worker池中。此函数是非阻塞的,执行完毕会立即返回。worker进程可以继续处理新的请求。

int swoole_server::task(mixed $data, int $dst_worker_id = -1) $task_id = $serv->task("some data");//swoole-1.8.6或更高版本$serv->task("taskcallback", -1, function (swoole_server $serv, $task_id, $data) {    echo "Task Callback: ";    var_dump($task_id, $data);});
  • $data要投递的任务数据,可以为除资源类型之外的任意PHP变量
  • $dst_worker_id可以制定要给投递给哪个task进程,传入ID即可,范围是0 - (serv->task_worker_num -1)
  • 调用成功,返回值为整数$task_id,表示此任务的ID。如果有finish回应,onFinish回调中会携带$task_id参数
  • 调用失败,返回值为false
  • 未指定目标Task进程,调用task方法会判断Task进程的忙闲状态,底层只会向处于空闲状态的Task进程投递任务
  • 1.8.6版本增加了第三个参数,可以直接设置onFinish函数,如果任务设置了回调函数,Task返回结果时会直接执行制定的回调函数,不再执行Server的onFinish回调

$dst_worker_id在1.6.11+后可用,默认为随机投递
$task_id是从0-42亿的整数,在当前进程内是唯一的
task方法不能在task进程/用户自定义进程中调用

此功能用于将慢速的任务异步地去执行,比如一个聊天室服务器,可以用它来进行发送广播。当任务完成时,在task进程中调用$serv->finish("finish")告诉worker进程此任务已完成。当然swoole_server->finish是可选的。

task底层使用Unix Socket管道通信,是全内存的,没有IO消耗。单进程读写性能可达100万/s,不同的进程使用不同的管道通信,可以最大化利用多核。

AsyncTask功能在1.6.4版本增加,默认不启动task功能,需要在手工设置task_worker_num来启动此功能
task_worker的数量在swoole_server::set参数中调整,如task_worker_num => 64,表示启动64个进程来接收异步任务

配置参数

swoole_server->task/taskwait/finish 3个方法当传入的$data数据超过8K时会启用临时文件来保存。当临时文件内容超过server->package_max_length 时底层会抛出一个警告。

WARN: task package is too big.

server->package_max_length 默认为2M

注意事项

  • 使用swoole_server_task必须为Server设置onTask和onFinish回调,否则swoole_server->start会失败
  • task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求
  • 使用addProcess添加的用户进程中无法使用task投递任务,请使用sendMessage接口与工作进程通信

Swoole taskwait

函数原型:

string $result = swoole_server->taskwait(mixed $task_data, float $timeout = 0.5, int $dst_worker_id = -1);

taskwait与task方法作用相同,用于投递一个异步的任务到task进程池去执行。与task不同的是taskwait是阻塞等待的,直到任务完成或者超时返回。

$result为任务执行的结果,由$serv->finish函数发出。如果此任务超时,这里会返回false。

taskwait是阻塞接口,如果你的Server是全异步的请使用swoole_server::task和swoole_server::finish,不要使用taskwait
第3个参数可以制定要给投递给哪个task进程,传入ID即可,范围是0 - serv->task_worker_num
$dst_worker_id在1.6.11+后可用,默认为随机投递
taskwait方法不能在task进程中调用

onTask

在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task。

function onTask((swoole_server $serv, int $task_id, int $src_worker_id, string $data));
  • $task_id是任务ID,由swoole扩展内自动生成,用于区分不同的任务。$task_id和$src_worker_id组合起来才是全局唯一的,不同的worker进程投递的任务ID可能会有相同
  • $src_worker_id来自于哪个worker进程
  • $data 是任务的内容

返回执行结果到worker进程

1.7.2以上的版本,在onTask函数中 return字符串,表示将此内容返回给worker进程。worker进程中会触发onFinish函数,表示投递的task已完成。

  • return的变量可以是任意非null的PHP变量

1.7.2以前的版本,需要调用swoole_server->finish()函数将结果返回给worker进程


Swoole 2.0正式版发布了。2.0版本最大的更新是增加了对协程(Coroutine)的支持。正式版已同时支持PHP5和PHP7。基于Swoole2.0协程PHP开发者可以已同步的方式编写代码,底层自动进行协程调度,转变为异步IO。解决了传统异步编程嵌套回调的问题。

与Node.js(ES6+)、Python等语言使用yield/generator、async/await的实现方式相比,Swoole协程无需修改代码添加额外的关键词。

与Go语言的goroutine相比,Swoole协程是内置式的,应用层代码无需添加go关键词启动协程,只需要使用封装好的协程客户端即可,使用更简单。另外Swoole协程的IO组件在底层内置了超时机制,不需要使用复杂的select/chan/timer实现客户端超时。

目前Swoole底层内置的协程客户端组件包括:udpclient、tcpclient、httpclient、redisclient、mysqlclient,基本涵盖了开发者常用的几种通信协议。协程组件只能在服务器的onConnect、onRequest、onReceive、onMessage 回调函数中使用。

使用示例:

$server = new SwooleHttpServer('127.0.0.1', 9501);/*    触发on request事件时,SWOOLE会开辟一个协程栈,对协程栈进行初始化 */$server->on('Request', function ($request, $response) {    $tcp_cli = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);    /**        client在调用connect函数后,SWOOLE会将PHP上下文信息保存到当前栈内        然后将协程挂起,待确认连接成功后,触发epoll事件,然后协程切换        恢复PHP上下文信息,返回结果,继续执行PHP代码     */    if ($tcp_cli->connect('127.0.0.1', 9906) === false) {        $response->end("connect server failed.");        return;    }    $tcp_cli->send('test for the coro');    /*        client在调用recv函数后,SWOOLE会将PHP上下文信息保存到当前栈内        然后将协程挂起待后端svr回包,触发epoll事件,然后协程切换        恢复PHP上下文信息,返回结果,继续执行PHP代码        如果后端在设定的超时时间内,未能回包,返回false        client的errCode定为110     */    $ret = $tcp_cli->recv(100);    $tcp_cli->close();    if ($ret) {        $response->end(" swoole response is ok");    } else {        $response->end(" recv failed error : {$tcp_cli->errCode}");    }});$server->start();

UDP客户端

$udp_cli = new SwooleCoroutineClient(SWOOLE_SOCK_UDP);$ret = $udp_cli->connect('127.0.0.1', 9906);$udp_cli->send('test for the coro');$ret = $udp_cli->recv(100);$udp_cli->close();if ($ret){    $response->end("swoole response is ok");}else{    $response->end("recv failed error : {$client->errCode}");}

MySQL客户端

$swoole_mysql = new SwooleCoroutineMySQL();$swoole_mysql->connect([    'host' => '127.0.0.1',    'user' => 'user',    'password' => 'pass',    'database' => 'test']);$res = $swoole_mysql->query('select sleep(1)');

Redis客户端

$redis = new SwooleCoroutineRedis();$redis->connect('127.0.0.1', 6379);$val = $redis->get('key');

Http客户端

$cli = new SwooleCoroutineHttpClient('127.0.0.1', 80);$cli->setHeaders([    'Host' => "localhost",    "User-Agent" => 'Chrome/49.0.2587.3',    'Accept' => 'text/html,application/xhtml+xml,application/xml',    'Accept-Encoding' => 'gzip',]);$cli->set([ 'timeout' => 1]);$cli->get('/index.php');echo $cli->body;  $cli->close();

Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在名空间SwooleCoroutine*)。

2.0.2或更高版本已支持PHP7

协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势:

  1. 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。

  2. 同时由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率。

协程API目前针对了TCP,UDP等主流协议client的封装,包括:

  • UDP
  • TCP
  • HTTP
  • Mysql
  • Redis

可以满足大部分开发者的需求。对于私有协议,开发者可以使用协程的TCP或者UDP接口去方便的封装。

启用

Prerequisite:

  • PHP版本要求:>= 5.5,包括5.5、5.6、7.0、7.1
  • 基于swoole_server或者swoole_http_server进行开发,目前只支持在onRequetonReceiveonConnect事件回调函数中使用协程。

swoole2.0需要通过添加--enable-coroutine编译参数启用协程能力,示例如下:

phpize./configure --with-php-config={path-to-php-config}  --enable-coroutinemakemake install

添加编译参数,swoole server将切换到协程模式。

开启协程模式后,swoole_serverswoole_http_server将以为每一个请求创建对应的协程,开发者可以在onRequetonReceiveonConnect 3个事件回调中使用协程客户端。

相关配置

SwooleServer的set方法中增加了一个配置参数max_coro_num,用于配置一个worker进程最多同时处理的协程数目。因为随着worker进程处理的协程数目的增加,其占用的内存也会增加,为了避免超出php的memory_limit限制,请根据实际业务的压测结果设置该值,默认为3000。

使用示例


当代码执行到connect()和recv()函数时,swoole会触发进行协程切换,此时swoole可以去处理其他的事件或者接受新的请求。当此client连接成功或者后端服务回包后,swoole server会恢复协程上下文,代码逻辑继续从切换点开始恢复执行。开发者整个过程不需要关心整个切换过程。具体使用可以参考client的文档。

注意事项

  1. 全局变量:协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及static变量的一致性。
  2. 请勿在以下场景中触发协程切换:
    • 析构函数
    • 魔术方法__call()
  3. gcc 4.4下如果在编译swoole的时候(即make阶段),出现gcc warning:dereferencing pointer ‘v.327’ does break strict-aliasing rulesdereferencing type-punned pointer will break strict-aliasing rules 请手动编辑Makefile,将CFLAGS = -Wall -pthread -g -O2替换为CFLAGS = -Wall -pthread -g -O2 -fno-strict-aliasing,然后重新编译make clean;make;make install
  4. 与xdebug、xhprof等zend扩展不兼容,例如不能使用xhprof对协程server进行性能分析采样。
  5. 在PHP5中,原生的call_user_func和call_user_func_array中无法使用协程client,请使用SwooleCoroutine::call_user_func和SwooleCoroutine::call_user_func_array代替
  6. 在PHP7中可直接调用原生的call_user_func和call_user_func_array

方法列表

getDefer()

bool getDefer();
  • 返回值:返回当前设置的defer

setDefer()

bool setDefer([bool $is_defer = true]);
  • $is_defer:bool值,为true时,表明该Client要延迟收包,为false时,表明该Client非延迟收包,默认值为true
  • 返回值:设置成功返回true,否则返回false。只有一种情况会返回false,当设置defer(true)并发包后,尚未recv()收包,就设置defer(false),此时返回false。
  • 如果需要进行延迟收包,需要在发包之前调用

recv()

mixed recv();
  • 返回值:获取延迟收包的结果,当没有进行延迟收包或者收包超时,返回false。

并发调用

Client并发请求


在协程版本的Client中,实现了多个客户端并发的发包功能。

通常,如果一个业务请求中需要做一次redis请求和一次mysql请求,那么网络IO会是这样子:

redis发包->redis收包->mysql发包->mysql收包

以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间。

而对于协程版本的Client,网络IO可以是这样子:

redis发包->mysql发包->redis收包->mysql收包

以上流程网络IO的时间就接近于 MAX(redis网络IO时间, mysql网络IO时间)。

现在支持并发请求的Client有:

  • SwooleCoroutineClient
  • SwooleCoroutineRedis
  • SwooleCoroutineMySQL
  • SwooleCoroutineHttpClient

除了SwooleCoroutineClient,其他Client都实现了defer特性,用于声明延迟收包。

因为SwooleCoroutineClient的发包和收包方法是分开的,所以就不需要实现defer特性了,而其他Client的发包和收包都是在一个方法中,所以需要一个setDefer()方法声明延迟收包,然后通过recv()方法收包。


协程版本Client并发请求示例代码:

<?php$server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);$server->set([    'worker_num' => 1,]);$server->on('Request', function ($request, $response) {    $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);    $tcpclient->connect('127.0.0.1', 9501,0.5)    $tcpclient->send("hello world
");    $redis = new SwooleCoroutineRedis();    $redis->connect('127.0.0.1', 6379);    $redis->setDefer();    $redis->get('key');    $mysql = new SwooleCoroutineMySQL();    $mysql->connect([        'host' => '127.0.0.1',        'user' => 'user',        'password' => 'pass',        'database' => 'test',    ]);    $mysql->setDefer();    $mysql->query('select sleep(1)');    $httpclient = new SwooleCoroutineHttpClient('0.0.0.0', 9599);    $httpclient->setHeaders(['Host' => "api.mp.qq.com"]);    $httpclient->set([ 'timeout' => 1]);    $httpclient->setDefer();    $httpclient->get('/');    $tcp_res  = $tcpclient->recv();    $redis_res = $redis->recv();    $mysql_res = $mysql->recv();    $http_res  = $httpclient->recv();    $response->end('Test End');});$server->start();

实现原理

Swoole2.0基于setjmplongjmp实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。

示例代码

$server = new SwooleHttpServer('127.0.0.1', 9501, SWOOLE_BASE);#1$server->on('Request', function($request, $response) {    $mysql = new SwooleCoroutineMySQL();    #2    $res = $mysql->connect([        'host' => '127.0.0.1',        'user' => 'root',        'password' => 'root',        'database' => 'test',    ]);    #3    if ($res == false) {        $response->end("MySQL connect fail!");        return;    }    $ret = $mysql->query('show tables', 2);    $response->end("swoole response is ok, result=".var_export($ret, true));});$server->start();
  • 此程序仅启动了一个1个进程,就可以并发处理大量请求。
  • 程序的性能基本上与异步回调方式相同,但是代码完全是同步编写的

运行过程

  • 调用onRequest事件回调函数时,底层会调用C函数coro_create创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态和ZendVM stack信息。
  • 调用mysql->connect时发生IO操作,底层会调用C函数coro_save保存当前协程的状态,包括Zend VM上下文以及协程描述信息,并调用coro_yield让出程序控制权,当前的请求会挂起(#2位置)
  • 协程让出程序控制权后,会继续进入EventLoop处理其他事件,这时Swoole会继续去处理其他客户端发来的Request
  • IO事件完成后,MySQL连接成功或失败,底层调用C函数core_resume恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码(#3位置)
  • mysql->query的执行过程与mysql->connect一致,也会进行一次协程切换调度
  • 所有操作完成后,调用end方法返回结果,并销毁此协程

协程开销

相比普通的异步回调程序,协程多增加额外的内存占用。

  • Swoole2.0协程需要为每个并发保存zend stack栈内存并维护对应的虚拟机状态。如果程序并发很大可能会占用大量内存,取决于C函数、ZendVM 调用栈深度
  • 协程调度会增加额外的一些CPU开销

压力测试

  • 环境:Ubuntu16.04 + Core I5 4核 + 8G内存 PHP7.0.10
  • 脚本:ab -c 100 -n 10000 http://127.0.0.1:9501/

测试结果:

Server Software:        swoole-http-serverServer Hostname:        127.0.0.1Server Port:            9501Document Path:          /Document Length:        348 bytesConcurrency Level:      100Time taken for tests:   0.883 secondsComplete requests:      10000Failed requests:        168   (Connect: 0, Receive: 0, Length: 168, Exceptions: 0)Total transferred:      4914560 bytesHTML transferred:       3424728 bytesRequests per second:    11323.69 [#/sec] (mean)Time per request:       8.831 [ms] (mean)Time per request:       0.088 [ms] (mean, across all concurrent requests)Transfer rate:          5434.67 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        0    0   0.2      0       2Processing:     0    9   9.6      6      96Waiting:        0    9   9.6      6      96Total:          0    9   9.6      6      96Percentage of the requests served within a certain time (ms)  50%      6  66%      9  75%     11  80%     12  90%     19  95%     27  98%     43  99%     51 100%     96 (longest request)


Swoole Server介绍

创建一个异步服务器程序,支持TCP、UDP、UnixSocket 3种协议,支持IPv4和IPv6,支持SSL/TLS单向双向证书的隧道加密。使用者无需关注底层实现细节,仅需要设置网络事件的回调函数即可。

swoole_server只能用于php-cli环境,否则会抛出致命错误

构建Server对象

$serv = new swoole_server("127.0.0.1", 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP);

设置运行时参数

$serv->set(array(    'worker_num' => 4,    'daemonize' => true,    'backlog' => 128,));

注册事件回调函数

$serv->on('Connect', 'my_onConnect');$serv->on('Receive', 'my_onReceive');$serv->on('Close', 'my_onClose');

PHP中可以使用4种回调函数的风格

启动服务器

$serv->start();

属性列表

$serv->manager_pid;  //管理进程的PID,通过向管理进程发送SIGUSR1信号可实现柔性重启$serv->master_pid;  //主进程的PID,通过向主进程发送SIGTERM信号可安全关闭服务器$serv->connections; //当前服务器的客户端连接,可使用foreach遍历所有连接

运行流程图

Swoole扩展架构图

进程/线程结构图

Swoole进程/线程结构图

简单Swoole tcp server实例:

$serv = new SwooleServer("127.0.0.1", 9501);//设置服务器参数$serv->set(array(    'worker_num' => 8,   //工作进程数量    'daemonize' => true, //是否作为守护进程));//设置事件回调函数$serv->on('connect', function ($serv, $fd) {    echo "Client:Connect.
";});$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {    $serv->send($fd, 'Swoole: ' . $data);    $serv->close($fd);});$serv->on('close', function ($serv, $fd) {    echo "Client: Close.
";});//启动服务器$serv->start();

Swoole Client介绍

swoole_client提供了tcp/udp socket的客户端的封装代码,使用时仅需 new swoole_client即可。 swoole的socket client对比PHP提供的stream族函数有哪些好处:

  • stream函数存在超时设置的陷阱和Bug,一旦没处理好会导致Server端长时间阻塞
  • fread有8192长度限制,无法支持UDP的大包
  • swoole_client支持waitall,在知道包长度的情况下可以一次取完,不必循环取。
  • swoole_client支持UDP connect,解决了UDP串包问题
  • swoole_client是纯C的代码,专门处理socket,stream函数非常复杂。swoole_client性能更好

除了普通的同步阻塞+select的使用方法外,swoole_client还支持异步非阻塞回调。

同步阻塞客户端

$client = new swoole_client(SWOOLE_SOCK_TCP);if (!$client->connect('127.0.0.1', 9501, -1)){    exit("connect failed. Error: {$client->errCode}
");}$client->send("hello world
");echo $client->recv();$client->close();

php-fpm/apache环境下只能使用同步客户端
apache环境下仅支持prefork多进程模式,不支持prework多线程

异步非阻塞客户端

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);$client->on("connect", function(swoole_client $cli) {    $cli->send("GET / HTTP/1.1

");});$client->on("receive", function(swoole_client $cli, $data){    echo "Receive: $data";    $cli->send(str_repeat('A', 100)."
");    sleep(1);});$client->on("error", function(swoole_client $cli){    echo "error
";});$client->on("close", function(swoole_client $cli){    echo "Connection close
";});$client->connect('127.0.0.1', 9501);

异步客户端只能使用在cli命令行环境

Swoole HttpServer介绍

swoole-1.7.7增加了内置Http服务器的支持,通过几行代码即可写出一个异步非阻塞多进程的Http服务器。

常见使用场景:因为swoole是在cli命令下执行的,在传统通过nginx+fastcgi模式下很多root的shell无法执行,而使用这个swoole服务器就很好的控制rsync,git,svn等。

$http = new swoole_http_server("127.0.0.1", 9501);$http->on('request', function ($request, $response) {    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");});$http->start();

swoole_http_server对Http协议的支持并不完整,建议仅作为应用服务器。并且在前端增加Nginx作为代理

简单实例:

$serv = new SwooleHttpServer("127.0.0.1", 9502);$serv->on('Request', function($request, $response) {    var_dump($request->get);    var_dump($request->post);    var_dump($request->cookie);    var_dump($request->files);    var_dump($request->header);    var_dump($request->server);    $response->cookie("User", "Swoole");    $response->header("X-Server", "Swoole");    $response->end("<h1>Hello Swoole!</h1>");});$serv->start();

通过使用apache bench工具进行压力测试,在Inter Core-I5 4核 + 8G内存的普通PC机器上,swoole_http_server可以达到近11万QPS。远远超过php-fpm,golang自带http服务器,node.js自带http服务器。性能几乎接近与Nginx的静态文件处理。

ab -c 200 -n 200000 -k http://127.0.0.1:9501


使用Http2协议

  • 需要依赖nghttp2库,下载nghttp2后编译安装
  • 使用Http2协议必须开启openssl
  • 需要高版本openssl必须支持TLS1.2ALPNNPN
./configure --enable-openssl --enable-http2

设置http服务器的open_http2_protocoltrue

$serv->set([    'ssl_cert_file' => $ssl_dir . '/ssl.crt',    'ssl_key_file' => $ssl_dir . '/ssl.key',    'open_http2_protocol' => true,]);

nginx+swoole配置

server {    root /data/wwwroot/;    server_name local.swoole.com;    location / {        if (!-e $request_filename) {             proxy_pass http://127.0.0.1:9501;             proxy_http_version 1.1;             proxy_set_header Connection "keep-alive";        }    }}


异步Http/WebSocket客户端

Swoole-1.8.0版本增加了对异步Http/WebSocket客户端的支持。底层是用纯C编写,拥有超高的性能。

异步HTTP客户端目前仍在实验阶段,请谨慎使用

启用Http客户端

  • 1.8.6版本之前,需要在编译swoole时增加--enable-async-httpclient来开启此功能。
  • swoole_http_client不依赖任何第三方库
  • 支持Http-Chunk、Keep-Alive特性,暂不支持form-data格式
  • Http协议版本为HTTP/1.1
  • gzip压缩格式支持需要依赖zlib库

构造方法

function swoole_http_client->__construct(string $ip, int port, bool $ssl = false);
  • $ip 目标服务器的IP地址,可使用swoole_async_dns_lookup查询域名对应的IP地址
  • $port 目标服务器的端口,一般http为80,https为443
  • $ssl 是否启用SSL/TLS隧道加密,如果目标服务器是https必须设置$ssl参数为true


Swoole WebSocket介绍

swoole-1.7.9 增加了内置的websocket服务器支持,通过几行PHP代码就可以写出一个异步非阻塞多进程的WebSocket服务器。

常见使用场景:我们在使用php开发的时候,原生最不好用的是socket类库了,而在开发IM和及时通信项目是,我们现在有了新的选择后端使用php Swoole WebSocket + 前端 html5 WebSocket;

简单实例:

$server = new swoole_websocket_server("0.0.0.0", 9501);$server->on('open', function (swoole_websocket_server $server, $request) {    echo "server: handshake success with fd{$request->fd}
";});$server->on('message', function (swoole_websocket_server $server, $frame) {    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}
";    $server->push($frame->fd, "this is server");});$server->on('close', function ($ser, $fd) {    echo "client {$fd} closed
";});$server->start();

onRequest回调

swoole_websocket_server 继承自 swoole_http_server

  • 设置了onRequest回调,websocket服务器也可以同时作为http服务器
  • 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面

客户端

  • Chrome/Firefox/高版本IE/Safari等浏览器内置了JS语言的WebSocket客户端
  • 异步的PHP程序中可以使用SwooleHttpClient作为WebSocket客户端
  • apache/php-fpm或其他同步阻塞的PHP程序中可以使用swoole/framework提供的同步WebSocket客户端
  • 非WebSocket客户端不能与WebSocket服务器通信
甚至你还可以结合使用socket.io来配合开发

Swoole RedisServer异步客户端介绍

Swoole-1.8.14版本增加一个兼容Redis服务器端协议的Server框架,可基于此框架实现Redis协议的服务器程序。SwooleRedisServer继承自SwooleServer,可调用父类提供的所有方法。

RedisServer不需要设置onReceive回调。实例程序:https://github.com/swoole/swoole-src/blob/master/examples/redis/server.php

可用的客户端

  • 任意编程语言的redis客户端,包括PHP的redis扩展和phpredis库
  • Swoole扩展提供的异步Redis客户端
  • Redis提供的命令行工具,包括redis-cliredis-benchmark
注意:Swoole-1.8.0版本增加了对异步Redis客户端的支持,基于redis官方提供的hiredis库实现。Swoole提供了__call魔术方法,来映射绝大部分Redis指令。

编译安装hiredis

使用Redis客户端,需要安装hiredis库。下载hiredis源码后,执行

make -jsudo make installsudo ldconfig

启用异步Redis客户端

编译swoole是,在configure指令中加入--enable-async-redis

./configure --enable-async-redismake cleanmake -jsudo make install

简单实例:

$redis = new SwooleRedis;$redis->connect('127.0.0.1', 6379, function ($redis, $result) {    $redis->set('test_key', 'value', function ($redis, $result) {        $redis->get('test_key', function ($redis, $result) {            var_dump($result);        });    });});$cli = new SwooleHttpClient('127.0.0.1', 80);$cli->setHeaders(array('User-Agent' => 'swoole-http-client'));$cli->setCookies(array('test' => 'value'));$cli->post('/dump.php', array("test" => 'abc'), function ($cli) {    var_dump($cli->body);    $cli->get('/index.php', function ($cli) {        var_dump($cli->cookies);        var_dump($cli->headers);    });});

Swoole异步MySQL介绍

MySQL异步是指将MySQL连接事件驱动化,这样就编程了非阻塞IO。使用Swoole可以实现mysql异步链接,Mysql连接池等。
  • 异步MySQL客户端依赖PHP的 mysqlnd 和 mysqli 2个扩展,请使用php -mphpinfo确认PHP是否有这2个扩展。
  • 另外需要在编译swoole时制定--enable-async-mysql

1.8.6版本已移除对mysqlimysqlnd扩展的依赖,并改为内置,无需额外的编译参数开启

简单实例:

$db = new SwooleMySQL;$server = array(    'host' => '127.0.0.1',    'user' => 'test',    'password' => 'test',    'database' => 'test',);$db->connect($server, function ($db, $result) {    $db->query("show tables", function (SwooleMySQL $db, $result) {        if ($result === false) {            var_dump($db->error, $db->errno);        } elseif ($result === true) {            var_dump($db->affected_rows, $db->insert_id);        } else {            var_dump($result);            $db->close();        }    });});

MySQL长连接

MySQL短连接每次请求操作数据库都需要建立与MySQL服务器建立TCP连接,这是需要时间开销的。TCP连接需要3次网络通信。这样就增加了一定的延时和额外的IO消耗。请求结束后会关闭MySQL连接,还会发生3/4次网络通信。

close操作不会增加响应延时,原因是close后是由操作系统自动进行通信的,应用程序感知不到

长连接就可以避免每次请求都创建连接的开销,节省了时间和IO消耗。提升了PHP程序的性能。

断线重连

在cli环境下,PHP程序需要长时间运行,客户端与MySQL服务器之间的TCP连接是不稳定的。

  • MySQL-Server会在一定时间内自动切断连接
  • PHP程序遇到空闲期时长时间没有MySQL查询,MySQL-Server也会切断连接回收资源
  • 其他情况,在MySQL服务器中执行kill process杀掉某个连接,MySQL服务器重启

这时PHP程序中的MySQL连接就失效了。如果仍然执行mysql_query,就会报一个“MySQL server has gone away”的错误。程序处理不到就直接遇到致命错误并退出了。所以PHP程序中需要断线重连。

有很多人提出了mysql_ping的方案,每次mysql_query进行连接检测或者定时连接检测。这个方案不是最好的。原因是

  • mysql_ping需要主动侦测连接,带来了额外的消耗
  • 定时执行mysql_ping不能解决问题,如刚刚执行过mysql_ping检测之后,连接就关闭了

最佳的方案是,进行断线重连 。它的原理是:

  1. mysql_query执行后检测返回值
  2. 如果mysql_query返回失败,检测错误码发现为2006/2013(这2个错误表示连接失败),再执行一次mysql_connect
  3. 执行mysql_connect后,重新执行mysql_query,这时必然会成功,因为已经重新建立了连接
  4. 如果mysql_query返回成功,那么连接是有效的,这是一次正常的调用

可参考swoole_framework中的代码

MySQL异步

MySQL异步是指将MySQL连接事件驱动化,这样就编程了非阻塞IO。数据库操作并不会阻塞进程,在MySQL-Server返回结果时再执行对应的逻辑。

有几个点需要注意一下:

  • 异步MySQL并没有节省SQL执行的时间
  • 一个MySQL连接同时只能执行1个SQL,如果异步MySQL存在并发那么必须创建多个MySQL连接

异步回调程序中,异步MySQL并没有提升性能。异步最大的好处是可以高并发,如果并发1万个请求,那么就需要建立1万个MySQL连接,这会给MySQL-Server带来巨大的压力。

MySQL是根据连接数分配资源的,一个连接需要开启一个线程。1000连接那么需要维持1000线程才可以。线程数量增加后,线程间切换会占用大量CPU资源
MySQL短连接反而不会出现此问题,因为短连接在使用完后就释放了。不会占用MySQL-Server的连接资源

虽然应用层代码使用异步回调避免了自身的阻塞,实际上真正的瓶颈是数据库服务器。异步MySQL还带来了额外的编程复杂度,所以除非是特殊场景的需求,否则不建议使用异步MySQL。

如果程序中坚持要使用异步,那么必须是异步MySQL+连接池的形式。超过规定的MySQL最大连接后,应当对SQL请求进行排队,而不是创建新连接,避免大量并发请求导致MySQL服务器崩溃。

MySQL连接池

连接池是可以有效降低MySQL-Server负载的。原理是 连接池使用一个共享资源的模式,如并发100个请求,实际上并不是每个请求的所有时间都在执行SQL查询。这样100个请求,共享20个MySQL连接就可以满足需求了。当一个请求操作完数据库后,开始进入模板渲染等流程,这时就会释放数据库连接给其他的请求使用。

连接池仅在超大型应用中才有价值。普通的应用采用MySQL长连接方案,每个php-fpm创建一个MySQL连接,每台机器开启100个php-fpm进程。如果有10台机器,每台机器并发的请求为100。实际上只需要创建1000个MySQL连接就能满足需求,数据库的压力并不大。即使有100台机器,硬件配置好的存储服务器依然可以承受。

达到数百或者数千台应用服务器时,MySQL服务器就需要维持十万级的连接。这时数据库的压力就会非常大了。连接池技术就可以派上用场了,可以大大降低数据库连接数。

基于swoole的AsyncTask模块实现的连接池是完美方案,编程简单,没有数据同步和锁的问题。甚至可以多个服务共享连接池。缺点是1, 灵活性不如多线程连接池,无法动态增减连接。2, 有一次进程间通信的开销。
node.js/ngx_lua等在多进程的模式下,无法开发出真正的连接池,除非也像swoole_task这样来实现


Swoole AsyncIO异步文件读写介绍

swoole1.6.12后增加了异步文件读写,异步DNS等特性。自此建立了完整的异步并行API。

  • swoole_server的Task进程是同步阻塞的,没有EventLoop,因此无法使除定时器之外的用任何异步IO
  • signalfd是Linux2.6.27提供文件句柄方式处理信号特性,优点是可以将信号加入到EventLoop中,Reactor操作不会被信号打断提高了性能。缺点是有些同步阻塞的程序可能会出现问题,无法从阻塞中中断,可以使用swoole_async_set关闭signalfd特性


Swoole支持3种类型的异步文件读写IO,可以使用swoole_async_set来设置AIO模式。

swoole_async_set

此函数可以设置异步IO相关的选项。

swoole_async_set(array $setting);
  • thread_num 设置异步文件IO线程的数量
  • aio_mode 设置异步文件IO的操作模式,目前支持SWOOLE_AIO_BASE(使用类似于Node.js的线程池同步阻塞模拟异步)、SWOOLE_AIO_LINUX(Linux Native AIO) 2种模式
  • enable_signalfd 开启和关闭signalfd特性的使用
  • socket_buffer_size 设置SOCKET内存缓存区尺寸
  • socket_dontwait 在内存缓存区已满的情况下禁止底层阻塞等待

Linux Native AIO的优点是由内核支持是真正的异步文件IO,缺点是只支持DirectIO,无法利用到系统的PageCache

swoole_async模块目前为实验性质,不建议在生产环境使用,请使用PHP的文件读写函数。

Linux原生异步IO

基于Linux Native AIO系统调用,是真正的异步IO,并非阻塞模拟。

优点:

  • 所有操作均在一个线程内完成,不需要开线程池
  • 不依赖线程执行IO,所以并发可以非常大

缺点:

  • 只支持DriectIO,无法利用PageCache,所有对文件读写都会直接操作磁盘

线程池模式异步IO

基于线程池模拟实现,文件读写请求投递到任务队列,然后由AIO线程读写文件,完成后通知主线程。AIO线程本身是同步阻塞的。所以并非真正的异步IO。

优点:

  • 可以利用操作系统PageCache,读写热数据性能非常高,等于读内存

可修改thread_num项设置启用的AIO线程数量

缺点:

  • 并发较差,不支持同时读写大量文件,最大并发受限与AIO的线程数量

简单实例:

$fp = stream_socket_client("tcp://127.0.0.1:80", $code, $msg, 3);$http_request = "GET /index.html HTTP/1.1

";fwrite($fp, $http_request);SwooleEvent::add($fp, function($fp){    echo fread($fp, 8192);    swoole_event_del($fp);    fclose($fp);});SwooleTimer::after(2000, function() {    echo "2000ms timeout
";});SwooleTimer::tick(1000, function() {    echo "1000ms interval
";});



Swoole Task异步任务介绍

swoole 的异步任务task系统可以很方便的为我们在开发的过程中调用异步任务的执行,而无需等待。

常见使用场景:

task模块用来做一些异步的慢速任务,比如webim中发广播,发送邮件,异步订单处理、异步支付处理等。

  • task进程必须是同步阻塞的
  • task进程支持定时器

node.js 假如有10万个连接,要发广播时,那会循环10万次,这时候程序不能做任何事情,不能接受新的连接,也不能收包发包。

而swoole不同,丢给task进程之后,worker进程可以继续处理新的数据请求。任务完成后会异步地通知worker进程告诉它此任务已经完成。

当然task模块的作用还不仅如此,实现PHP的数据库连接池,异步队列等,还需要进一步挖掘。

简单实例:

$serv = new SwooleServer("127.0.0.1", 9502);$serv->set(array('task_worker_num' => 4));$serv->on('Receive', function($serv, $fd, $from_id, $data) {    $task_id = $serv->task("Async");    echo "Dispath AsyncTask: id=$task_id
";});$serv->on('Task', function ($serv, $task_id, $from_id, $data) {    echo "New AsyncTask[id=$task_id]".PHP_EOL;    $serv->finish("$data -> OK");});$serv->on('Finish', function ($serv, $task_id, $data) {    echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;});$serv->start();

swoole_server task异步任务介绍

投递一个异步任务到task_worker池中。此函数是非阻塞的,执行完毕会立即返回。worker进程可以继续处理新的请求。

int swoole_server::task(mixed $data, int $dst_worker_id = -1) $task_id = $serv->task("some data");//swoole-1.8.6或更高版本$serv->task("taskcallback", -1, function (swoole_server $serv, $task_id, $data) {    echo "Task Callback: ";    var_dump($task_id, $data);});
  • $data要投递的任务数据,可以为除资源类型之外的任意PHP变量
  • $dst_worker_id可以制定要给投递给哪个task进程,传入ID即可,范围是0 - (serv->task_worker_num -1)
  • 调用成功,返回值为整数$task_id,表示此任务的ID。如果有finish回应,onFinish回调中会携带$task_id参数
  • 调用失败,返回值为false
  • 未指定目标Task进程,调用task方法会判断Task进程的忙闲状态,底层只会向处于空闲状态的Task进程投递任务
  • 1.8.6版本增加了第三个参数,可以直接设置onFinish函数,如果任务设置了回调函数,Task返回结果时会直接执行制定的回调函数,不再执行Server的onFinish回调

$dst_worker_id在1.6.11+后可用,默认为随机投递
$task_id是从0-42亿的整数,在当前进程内是唯一的
task方法不能在task进程/用户自定义进程中调用

此功能用于将慢速的任务异步地去执行,比如一个聊天室服务器,可以用它来进行发送广播。当任务完成时,在task进程中调用$serv->finish("finish")告诉worker进程此任务已完成。当然swoole_server->finish是可选的。

task底层使用Unix Socket管道通信,是全内存的,没有IO消耗。单进程读写性能可达100万/s,不同的进程使用不同的管道通信,可以最大化利用多核。

AsyncTask功能在1.6.4版本增加,默认不启动task功能,需要在手工设置task_worker_num来启动此功能
task_worker的数量在swoole_server::set参数中调整,如task_worker_num => 64,表示启动64个进程来接收异步任务

配置参数

swoole_server->task/taskwait/finish 3个方法当传入的$data数据超过8K时会启用临时文件来保存。当临时文件内容超过server->package_max_length 时底层会抛出一个警告。

WARN: task package is too big.

server->package_max_length 默认为2M

注意事项

  • 使用swoole_server_task必须为Server设置onTask和onFinish回调,否则swoole_server->start会失败
  • task操作的次数必须小于onTask处理速度,如果投递容量超过处理能力,task会塞满缓存区,导致worker进程发生阻塞。worker进程将无法接收新的请求
  • 使用addProcess添加的用户进程中无法使用task投递任务,请使用sendMessage接口与工作进程通信

Swoole taskwait

函数原型:

string $result = swoole_server->taskwait(mixed $task_data, float $timeout = 0.5, int $dst_worker_id = -1);

taskwait与task方法作用相同,用于投递一个异步的任务到task进程池去执行。与task不同的是taskwait是阻塞等待的,直到任务完成或者超时返回。

$result为任务执行的结果,由$serv->finish函数发出。如果此任务超时,这里会返回false。

taskwait是阻塞接口,如果你的Server是全异步的请使用swoole_server::task和swoole_server::finish,不要使用taskwait
第3个参数可以制定要给投递给哪个task进程,传入ID即可,范围是0 - serv->task_worker_num
$dst_worker_id在1.6.11+后可用,默认为随机投递
taskwait方法不能在task进程中调用

onTask

在task_worker进程内被调用。worker进程可以使用swoole_server_task函数向task_worker进程投递新的任务。当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task。

function onTask((swoole_server $serv, int $task_id, int $src_worker_id, string $data));
  • $task_id是任务ID,由swoole扩展内自动生成,用于区分不同的任务。$task_id和$src_worker_id组合起来才是全局唯一的,不同的worker进程投递的任务ID可能会有相同
  • $src_worker_id来自于哪个worker进程
  • $data 是任务的内容

返回执行结果到worker进程

1.7.2以上的版本,在onTask函数中 return字符串,表示将此内容返回给worker进程。worker进程中会触发onFinish函数,表示投递的task已完成。

  • return的变量可以是任意非null的PHP变量

1.7.2以前的版本,需要调用swoole_server->finish()函数将结果返回给worker进程


Swoole 2.0正式版发布了。2.0版本最大的更新是增加了对协程(Coroutine)的支持。正式版已同时支持PHP5和PHP7。基于Swoole2.0协程PHP开发者可以已同步的方式编写代码,底层自动进行协程调度,转变为异步IO。解决了传统异步编程嵌套回调的问题。

与Node.js(ES6+)、Python等语言使用yield/generator、async/await的实现方式相比,Swoole协程无需修改代码添加额外的关键词。

与Go语言的goroutine相比,Swoole协程是内置式的,应用层代码无需添加go关键词启动协程,只需要使用封装好的协程客户端即可,使用更简单。另外Swoole协程的IO组件在底层内置了超时机制,不需要使用复杂的select/chan/timer实现客户端超时。

目前Swoole底层内置的协程客户端组件包括:udpclient、tcpclient、httpclient、redisclient、mysqlclient,基本涵盖了开发者常用的几种通信协议。协程组件只能在服务器的onConnect、onRequest、onReceive、onMessage 回调函数中使用。

使用示例:

$server = new SwooleHttpServer('127.0.0.1', 9501);/*    触发on request事件时,SWOOLE会开辟一个协程栈,对协程栈进行初始化 */$server->on('Request', function ($request, $response) {    $tcp_cli = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);    /**        client在调用connect函数后,SWOOLE会将PHP上下文信息保存到当前栈内        然后将协程挂起,待确认连接成功后,触发epoll事件,然后协程切换        恢复PHP上下文信息,返回结果,继续执行PHP代码     */    if ($tcp_cli->connect('127.0.0.1', 9906) === false) {        $response->end("connect server failed.");        return;    }    $tcp_cli->send('test for the coro');    /*        client在调用recv函数后,SWOOLE会将PHP上下文信息保存到当前栈内        然后将协程挂起待后端svr回包,触发epoll事件,然后协程切换        恢复PHP上下文信息,返回结果,继续执行PHP代码        如果后端在设定的超时时间内,未能回包,返回false        client的errCode定为110     */    $ret = $tcp_cli->recv(100);    $tcp_cli->close();    if ($ret) {        $response->end(" swoole response is ok");    } else {        $response->end(" recv failed error : {$tcp_cli->errCode}");    }});$server->start();

UDP客户端

$udp_cli = new SwooleCoroutineClient(SWOOLE_SOCK_UDP);$ret = $udp_cli->connect('127.0.0.1', 9906);$udp_cli->send('test for the coro');$ret = $udp_cli->recv(100);$udp_cli->close();if ($ret){    $response->end("swoole response is ok");}else{    $response->end("recv failed error : {$client->errCode}");}

MySQL客户端

$swoole_mysql = new SwooleCoroutineMySQL();$swoole_mysql->connect([    'host' => '127.0.0.1',    'user' => 'user',    'password' => 'pass',    'database' => 'test']);$res = $swoole_mysql->query('select sleep(1)');

Redis客户端

$redis = new SwooleCoroutineRedis();$redis->connect('127.0.0.1', 6379);$val = $redis->get('key');

Http客户端

$cli = new SwooleCoroutineHttpClient('127.0.0.1', 80);$cli->setHeaders([    'Host' => "localhost",    "User-Agent" => 'Chrome/49.0.2587.3',    'Accept' => 'text/html,application/xhtml+xml,application/xml',    'Accept-Encoding' => 'gzip',]);$cli->set([ 'timeout' => 1]);$cli->get('/index.php');echo $cli->body;  $cli->close();

Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在名空间SwooleCoroutine*)。

2.0.2或更高版本已支持PHP7

协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势:

  1. 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。

  2. 同时由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率。

协程API目前针对了TCP,UDP等主流协议client的封装,包括:

  • UDP
  • TCP
  • HTTP
  • Mysql
  • Redis

可以满足大部分开发者的需求。对于私有协议,开发者可以使用协程的TCP或者UDP接口去方便的封装。

启用

Prerequisite:

  • PHP版本要求:>= 5.5,包括5.5、5.6、7.0、7.1
  • 基于swoole_server或者swoole_http_server进行开发,目前只支持在onRequetonReceiveonConnect事件回调函数中使用协程。

swoole2.0需要通过添加--enable-coroutine编译参数启用协程能力,示例如下:

phpize./configure --with-php-config={path-to-php-config}  --enable-coroutinemakemake install

添加编译参数,swoole server将切换到协程模式。

开启协程模式后,swoole_serverswoole_http_server将以为每一个请求创建对应的协程,开发者可以在onRequetonReceiveonConnect 3个事件回调中使用协程客户端。

相关配置

SwooleServer的set方法中增加了一个配置参数max_coro_num,用于配置一个worker进程最多同时处理的协程数目。因为随着worker进程处理的协程数目的增加,其占用的内存也会增加,为了避免超出php的memory_limit限制,请根据实际业务的压测结果设置该值,默认为3000。

使用示例


当代码执行到connect()和recv()函数时,swoole会触发进行协程切换,此时swoole可以去处理其他的事件或者接受新的请求。当此client连接成功或者后端服务回包后,swoole server会恢复协程上下文,代码逻辑继续从切换点开始恢复执行。开发者整个过程不需要关心整个切换过程。具体使用可以参考client的文档。

注意事项

  1. 全局变量:协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及static变量的一致性。
  2. 请勿在以下场景中触发协程切换:
    • 析构函数
    • 魔术方法__call()
  3. gcc 4.4下如果在编译swoole的时候(即make阶段),出现gcc warning:dereferencing pointer ‘v.327’ does break strict-aliasing rulesdereferencing type-punned pointer will break strict-aliasing rules 请手动编辑Makefile,将CFLAGS = -Wall -pthread -g -O2替换为CFLAGS = -Wall -pthread -g -O2 -fno-strict-aliasing,然后重新编译make clean;make;make install
  4. 与xdebug、xhprof等zend扩展不兼容,例如不能使用xhprof对协程server进行性能分析采样。
  5. 在PHP5中,原生的call_user_func和call_user_func_array中无法使用协程client,请使用SwooleCoroutine::call_user_func和SwooleCoroutine::call_user_func_array代替
  6. 在PHP7中可直接调用原生的call_user_func和call_user_func_array

方法列表

getDefer()

bool getDefer();
  • 返回值:返回当前设置的defer

setDefer()

bool setDefer([bool $is_defer = true]);
  • $is_defer:bool值,为true时,表明该Client要延迟收包,为false时,表明该Client非延迟收包,默认值为true
  • 返回值:设置成功返回true,否则返回false。只有一种情况会返回false,当设置defer(true)并发包后,尚未recv()收包,就设置defer(false),此时返回false。
  • 如果需要进行延迟收包,需要在发包之前调用

recv()

mixed recv();
  • 返回值:获取延迟收包的结果,当没有进行延迟收包或者收包超时,返回false。

并发调用

Client并发请求


在协程版本的Client中,实现了多个客户端并发的发包功能。

通常,如果一个业务请求中需要做一次redis请求和一次mysql请求,那么网络IO会是这样子:

redis发包->redis收包->mysql发包->mysql收包

以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间。

而对于协程版本的Client,网络IO可以是这样子:

redis发包->mysql发包->redis收包->mysql收包

以上流程网络IO的时间就接近于 MAX(redis网络IO时间, mysql网络IO时间)。

现在支持并发请求的Client有:

  • SwooleCoroutineClient
  • SwooleCoroutineRedis
  • SwooleCoroutineMySQL
  • SwooleCoroutineHttpClient

除了SwooleCoroutineClient,其他Client都实现了defer特性,用于声明延迟收包。

因为SwooleCoroutineClient的发包和收包方法是分开的,所以就不需要实现defer特性了,而其他Client的发包和收包都是在一个方法中,所以需要一个setDefer()方法声明延迟收包,然后通过recv()方法收包。


协程版本Client并发请求示例代码:

<?php$server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);$server->set([    'worker_num' => 1,]);$server->on('Request', function ($request, $response) {    $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);    $tcpclient->connect('127.0.0.1', 9501,0.5)    $tcpclient->send("hello world
");    $redis = new SwooleCoroutineRedis();    $redis->connect('127.0.0.1', 6379);    $redis->setDefer();    $redis->get('key');    $mysql = new SwooleCoroutineMySQL();    $mysql->connect([        'host' => '127.0.0.1',        'user' => 'user',        'password' => 'pass',        'database' => 'test',    ]);    $mysql->setDefer();    $mysql->query('select sleep(1)');    $httpclient = new SwooleCoroutineHttpClient('0.0.0.0', 9599);    $httpclient->setHeaders(['Host' => "api.mp.qq.com"]);    $httpclient->set([ 'timeout' => 1]);    $httpclient->setDefer();    $httpclient->get('/');    $tcp_res  = $tcpclient->recv();    $redis_res = $redis->recv();    $mysql_res = $mysql->recv();    $http_res  = $httpclient->recv();    $response->end('Test End');});$server->start();

实现原理

Swoole2.0基于setjmplongjmp实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。

示例代码

$server = new SwooleHttpServer('127.0.0.1', 9501, SWOOLE_BASE);#1$server->on('Request', function($request, $response) {    $mysql = new SwooleCoroutineMySQL();    #2    $res = $mysql->connect([        'host' => '127.0.0.1',        'user' => 'root',        'password' => 'root',        'database' => 'test',    ]);    #3    if ($res == false) {        $response->end("MySQL connect fail!");        return;    }    $ret = $mysql->query('show tables', 2);    $response->end("swoole response is ok, result=".var_export($ret, true));});$server->start();
  • 此程序仅启动了一个1个进程,就可以并发处理大量请求。
  • 程序的性能基本上与异步回调方式相同,但是代码完全是同步编写的

运行过程

  • 调用onRequest事件回调函数时,底层会调用C函数coro_create创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态和ZendVM stack信息。
  • 调用mysql->connect时发生IO操作,底层会调用C函数coro_save保存当前协程的状态,包括Zend VM上下文以及协程描述信息,并调用coro_yield让出程序控制权,当前的请求会挂起(#2位置)
  • 协程让出程序控制权后,会继续进入EventLoop处理其他事件,这时Swoole会继续去处理其他客户端发来的Request
  • IO事件完成后,MySQL连接成功或失败,底层调用C函数core_resume恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码(#3位置)
  • mysql->query的执行过程与mysql->connect一致,也会进行一次协程切换调度
  • 所有操作完成后,调用end方法返回结果,并销毁此协程

协程开销

相比普通的异步回调程序,协程多增加额外的内存占用。

  • Swoole2.0协程需要为每个并发保存zend stack栈内存并维护对应的虚拟机状态。如果程序并发很大可能会占用大量内存,取决于C函数、ZendVM 调用栈深度
  • 协程调度会增加额外的一些CPU开销

压力测试

  • 环境:Ubuntu16.04 + Core I5 4核 + 8G内存 PHP7.0.10
  • 脚本:ab -c 100 -n 10000 http://127.0.0.1:9501/

测试结果:

Server Software:        swoole-http-serverServer Hostname:        127.0.0.1Server Port:            9501Document Path:          /Document Length:        348 bytesConcurrency Level:      100Time taken for tests:   0.883 secondsComplete requests:      10000Failed requests:        168   (Connect: 0, Receive: 0, Length: 168, Exceptions: 0)Total transferred:      4914560 bytesHTML transferred:       3424728 bytesRequests per second:    11323.69 [#/sec] (mean)Time per request:       8.831 [ms] (mean)Time per request:       0.088 [ms] (mean, across all concurrent requests)Transfer rate:          5434.67 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        0    0   0.2      0       2Processing:     0    9   9.6      6      96Waiting:        0    9   9.6      6      96Total:          0    9   9.6      6      96Percentage of the requests served within a certain time (ms)  50%      6  66%      9  75%     11  80%     12  90%     19  95%     27  98%     43  99%     51 100%     96 (longest request)