Swoole
初步了解
Swoole
这个名字不是一个英文单词,是由我创造的一个音近字。
我最早想到的名字是叫做sword-server
,寓意是为广大PHPer创造一把锋利的剑,
后来联想到swoole
。 <韩天峰>
安装
PECL
一键安装
1 | pecl install swoole |
- 源码安装
1 | // 下载源码并解压 |
配置 php.ini
1 | extension=swoole.so |
基础实例
服务端
1 | class Server |
解析
从代码中可以看出,创建一个swoole_server
基本分三步:
- 通过构造函数创建
swoole_server
对象 - 调用set函数设置
swoole_server
的相关配置选项 - 调用on函数设置相关回调函数
onStart
回调在server
运行前被调用,onConnect
在有新客户端连接过来时被调用,onReceive
函数在有数据发送到server
时被调用onClose
在有客户端断开连接时被调用。
配置选项说明
worker_num
: 指定启动的worker
进程数。max_request
: 每个worker
进程允许处理的最大任务数。max_conn
: 服务器允许维持的最大TCP
连接数ipc_mode
: 设置进程间的通信方式。1
=> 使用unix socket通信2
=> 使用消息队列通信3
=> 使用消息队列通信,并设置为争抢模式
dispatch_mode
: 指定数据包分发策略。1
=> 轮循模式,收到会轮循分配给每一个worker
进程2
=> 固定模式,根据连接的文件描述符分配worker
。这样可以保证同一个连接发来的数据只会被同一个worker
处理3
=> 抢占模式,主进程会根据Worker
的忙闲状态选择投递,只会投递给处于闲置状态的Worker
task_worker_num
: 服务器开启的task
进程数。设置此参数后,必须要给
swoole_server
设置onTask/onFinish
两个回调函数,否则启动服务器会报错。task_max_request
: 每个task
进程允许处理的最大任务数。task_ipc_mode
: 设置task
进程与worker
进程之间通信的方式。daemonize
: 设置程序进入后台作为守护进程运行。说明:长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当ssh终端退出后,程序将被终止运行。
启用守护进程后,标准输入和输出会被重定向到log_file
,如果log_file
未设置,则所有输出会被丢弃。log_file
: 指定日志文件路径说明:在
swoole
运行期发生的异常信息会记录到这个文件中。默认会打印到屏幕。
注意log_file
不会自动切分文件,所以需要定期清理此文件。heartbeat_check_interval
: 设置心跳检测间隔说明:此选项表示每隔多久轮循一次,单位为秒。每次检测时遍历所有连接,
如果某个连接在间隔时间内没有数据发送,则强制关闭连接(会有onClose
回调)。heartbeat_idle_time
: 设置某个连接允许的最大闲置时间。说明:该参数配合
heartbeat_check_interval
使用。每次遍历所有连接时,如果某个连接在heartbeat_idle_time
时间内没有数据发送,
则强制关闭连接。默认设置为heartbeat_check_interval * 2
。open_eof_check
: 打开eof检测功能说明:与
package_eof
配合使用。此选项将检测客户端连接发来的数据,当数据包结尾是指定的package_eof
字符串时
才会将数据包投递至Worker
进程,否则会一直拼接数据包直到缓存溢出或超时才会终止。
一旦出错,该连接会被判定为恶意连接,数据包会被丢弃并强制关闭连接。package_eof
: 设置EOF字符串说明:
package_eof
最大只允许传入8
个字节的字符串open_length_check
: 打开包长检测说明:包长检测提供了
固定包头+包体
这种格式协议的解析,。
启用后,可以保证Worker
进程onReceive
每次都会收到一个完整的数据包。package_length_offset
: 包头中第几个字节开始存放了长度字段说明:配合
open_length_check
使用,用于指明长度字段的位置。package_body_offset
: 从第几个字节开始计算长度说明:配合
open_length_check
使用,用于指明包头的长度。package_length_type
: 指定包长字段的类型s
=> int16_t 机器字节序S
=> uint16_t 机器字节序n
=> uint16_t 大端字节序N
=> uint32_t 大端字节序L
=> uint32_t 机器字节序l
=> int 机器字节序说明:配合open_length_check使用,指定长度字段的类型,参数如下:
package_max_length
: 设置最大数据包尺寸说明:该值决定了数据包缓存区的大小。如果缓存的数据超过了该值,
则会引发错误。具体错误处理由开启的协议解析的类型决定。open_cpu_affinity
: 启用CPU
亲和性设置说明:在多核的硬件平台中,启用此特性会将
swoole
的reactor
线程/worker
进程绑定到固定的一个核上。
可以避免进程/线程的运行时在多个核之间互相切换,提高CPU Cache
的命中率。open_tcp_nodelay
: 启用open_tcp_nodelay
说明:开启后
TCP
连接发送数据时会无关闭Nagle
合并算法,
立即发往客户端连接。在某些场景下,如http
服务器,可以提升响应速度。tcp_defer_accept
: 启用tcp_defer_accept
特性说明:启动后,只有一个
TCP
连接有数据发送时才会触发accept
。ssl_cert_file
/ssl_key_file
: 设置SSL隧道加密说明:设置值为一个文件名字符串,指定
cert
证书和key
的路径。open_tcp_keepalive
: 打开TCP
的KEEP_ALIVE
选项说明:使用TCP内置的
keep_alive
属性,用于保证连接不会因为长时闲置而被关闭。tcp_keepidle
: 指定探测间隔。说明:配合
open_tcp_keepalive
使用,如果某个连接在tcp_keepidle
内没有任何数据来往,则进行探测。tcp_keepinterval
: 指定探测时的发包间隔说明:配合
open_tcp_keepalive
使用tcp_keepcount
: 指定探测的尝试次数说明:配合
open_tcp_keepalive
使用,若tcp_keepcount
次尝试后仍无响应,则判定连接已关闭backlog
: 指定Listen
队列长度说明:此参数将决定最多同时有多少个等待
accept
的连接。reactor_num
: 指定Reactor
线程数说明:设置主进程内事件处理线程的数量,默认会启用CPU核数相同的数量,
一般设置为CPU核数的1-4倍,最大不得超过CPU核数*4。task_tmpdir
: 设置task的数据临时目录说明:在
swoole_server
中,如果投递的数据超过8192
字节,
将启用临时文件来保存数据。这里的task_tmpdir
就是用来设置临时文件保存的位置。
客户端
1 |
|
这里,通过
swoole_client
创建一个基于TCP
的客户端实例,
并调用connect
函数向指定的IP
及端口发起连接请求。随后即可通过recv()
和send()
两个函数来接收和发送请求。
需要注意的是,这里我使用了默认的同步阻塞客户端,因此recv
和send
操作都会产生网络阻塞。
Swoole
异步任务Task
Task
简介
Swoole
的业务逻辑部分是同步阻塞运行的,如果遇到一些耗时较大的操作,例如访问数据库、广播消息等,就会影响服务器的响应速度。
因此Swoole
提供了Task
功能,将这些耗时操作放到另外的进程去处理,当前进程继续执行后面的逻辑。
开启Task
功能
开启Task
功能只需要在swoole_server
的配置项中添加task_worker_num
一项即可,如下:
1 | $serv->set(array( |
即可开启task
功能。此外,必须给swoole_server
绑定两个回调函数:onTask
和onFinish
。
这两个回调函数分别用于执行Task
任务和处理Task
任务的返回结果。
使用Task
首先是发起一个Task
,代码如下:
1 | public function onReceive (swoole_server $serv, $fd, $from_id, $data ) { |
可以看到,发起一个任务时,只需通过swoole_server
对象调用task
函数即可发起一个任务。swoole
内部会将这个请求投递给task_worker
,而当前Worker
进程会继续执行。
当一个任务发起后,task_worker
进程会响应onTask
回调函数,如下:
1 | public function onTask($serv, $task_id, $from_id, $data) |
这里我用
sleep
函数和循环模拟了一个长耗时任务。在onTask
回调中,我们通过task_id
和from_id
(也就是worker_id
)来区分不同进程投递的不同task
。
当一个task
执行结束后,通过return
一个字符串将执行结果返回给Worker
进程。Worker
进程将通过onFinish
回调函数接收这个处理结果。
1 | public function onFinish($serv, $task_id, $data) |
在onFinish
回调中,会接收到Task
任务的处理结果$data
。在这里处理这个返回结果即可。
swoole_client
在这里讲解如何使用
swoole_client
是因为,在写服务端代码的时候,不可避免的需要用到客户端来进行测试。swoole
提供了swoole_client
用于编写测试客户端,下面我将讲解如何使用这个工具。
swoole_client
有两种工作模式:同步阻塞模式和异步回调模式
其中,同步阻塞模式在上一章中已经给出示例,其使用和一般的socket
基本无异。
因此,我将重点讲解swoole_client
的异步回调模式。
- 创建一个异步
client
的代码如下:
1 | $client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC); |
其中,SWOOLE_SOCK_ASYNC选项即表明创建一个异步client。
既然是异步,那当然需要回调函数。swoole_client一共有四个回调函数,如下:
1 | $client->on("connect", function ($cli) { |
这几个回调函数的作用基本和swoole_server
类似,只有参数不同,因此不再赘述。
Timer
计时器、心跳检测、Task
进阶
Timer定时器
在实际应用中,往往会遇到需要每隔一段时间重复做一件事,比如心跳检测、订阅消息、数据库备份等工作。
通常,我们会借助PHP的time()
以及相关函数自己实现一个定时器,或者使用crontab
工具来实现。
但是,自定义的定时器容易出错,而使用crontab
则需要编写额外的脚本文件,无论是迁移还是调试都比较麻烦。因此,
Swoole
提供了一个内置的Timer
定时器功能,通过函数addtimer
即可在Swoole
中添加一个定时器,
该定时器会在建立之后,按照预先设定好的时间间隔,每到对应的时间就会调用一次回调函数onTimer
通知Server
。
1 |
|
可以看到,在onWorkerStart
回调函数中,通过addtimer
添加了三个定时器,时间间隔分别为500
、1000
、1500
。
而在onTimer
回调中,正好通过间隔的不同来区分不同的定时器回调,从而执行不同的操作。
需要注意的是,在上述示例中,当1000ms的定时器被触发时,500ms的定时器同样会被触发,
但是不能保证会在1000ms定时器前触发还是后触发,因此需要注意,定时器中的操作不能依赖其他定时器的执行结果。
心跳检测
使用Timer
定时器功能可以实现发送心跳包的功能。事实上,Swoole
已经内置了心跳检测功能,能自动close
掉长时间没有数据来往的连接。
而开启心跳检测功能,只需要设置heartbeat_check_interval
和heartbeat_idle_time
即可。如下:
1 | $this->serv->set( |
Task
进阶:MySQL
连接池
在
PHP
中,访问MySQL
数据库往往是性能提升的瓶颈。而MySQL
连接池我想大家都不陌生,这是一个很好的提升数据库访问性能的方式。
传统的MySQL
连接池,是预先申请一定数量的连接,每一个新的请求都会占用其中一个连接,请求结束后再将连接放回池中,
如果所有连接都被占用,新来的连接则会进入等待状态。知道了
MySQL
连接池的实现原理,那我们来看如何使用Swoole
实现一个连接池。
首先,Swoole
允许开启一定量的Task Worker
进程,我们可以让每个进程都拥有一个MySQL
连接,并保持这个连接,这样,我们就创建了一个连接池。其次,设置
swoole
的dispatch_mode
为抢占模式(主进程会根据Worker
的忙闲状态选择投递,
只会投递给处于闲置状态的Worker
)。这样,每个task
都会被投递给闲置的Task Worker
。
这样,我们保证了每个新的task
都会被闲置的Task Worker
处理,如果全部Task Worker
都被占用,则会进入等待队列。
下面直接上关键代码:
1 | public function onWorkerStart($serv, $worker_id) |
首先,在每个Task Worker
进程中,创建一个MySQL
连接。这里我选用了PDO
扩展。
1 | public function onReceive(swoole_server $serv, $fd, $from_id, $data) { |
其次,在需要的时候,通过task函数投递一个任务(也就是发起一次SQL
请求)
1 | public function onTask($serv,$task_id,$from_id, $data) |
最后,在
onTask
回调中,根据请求过来的SQL语句以及相应的参数,发起一次MySQL
请求,
并将获取到的结果通过send
发送给客户端(或者通过return
返回给Worker
进程)。
而且,这样的一次MySQL
请求还不会阻塞Worker
进程,Worker
进程可以继续处理其他的逻辑。
Swoole
多端口监听、热重启以及Timer
进阶:简单crontab
多端口监听
1 | $serv = new swoole_server("192.168.1.1", 9501); // 监听外网的9501端口 |
此时,
swoole_server
就会同时监听两个host
下的两个端口。这里要注意的是,
来自两个端口的数据会在同一个onReceive
中获取到,这时就要用到swoole
的另一个成员函数connection_info
,
通过这个函数获取到fd
的from_port
,就可以判定消息的类型
1 | $info = $serv->connection_info($fd, $from_id); |
服务器热重启
所谓热重启,就是当服务器相关代码有所变动之后,无需停止服务,而是在服务器仍然运行的状态下更新文件。
Swoole
通过内置的reload
函数以及两个自定义信号量实现了这一功能
Swoole
可用的三个信号:SIGTERM
,SIGUSR1
,SIGUSR2
SIGTERM
用于停止服务器SIGUSR1
用于重启全部的Worker
进程SIGUSR2
用于重启全部的Task Worker
进程
那要如何实现热更新代码文件呢?
Swoole的回调函数中有这个一个回调onWorkerStart
;该回调会在Worker
进程启动时被调用。
因此,当swoole_server
收到SIGUSR1
信号并重启全部Worker
进程后,onWorkerStart
就会被调用。如果在onWorkerStart
中require
全部的代码文件,
每次onWorkerStart
后都会重新require
一次php文件,这样就能实现代码文件的热更新。
1 | public function onStart( $serv ) |
首先,在onStart
回调函数中通过php的cli_set_process_title
函数设置进程名。
在onWorkerStart中
,require
相关的php文件。 然后,新建一个reload.sh
文件,输入如下内容:
1 | echo "Reloading..." |
这样,就可以通过执行这个脚本重启服务器了
Timer
补充:after
函数
在swoole-1.7.7stable
版本中,Timer
新增了一个函数after
。
该函数的作用是在指定的时间间隔后执行回调函数,并且只执行一次。
1 | $serv->after( 1000 , array($this, 'onAfter') , $str ); |
这里指定在1000ms
后,执行onAfter
回调函数,函数参数为$str
Swoole
的自定义协议功能的使用
为什么要提供自定义协议 ?
熟悉TCP通信的朋友都会知道,TCP是一个流式协议。客户端向服务器发送的一段数据,可能并不会被服务器一次就完整的收到;
客户端向服务器发送的多段数据,可能服务器一次就收到了全部的数据。
而实际应用中,我们希望在服务器端能一次接收一段完整的数据,不多也不少
传统的TCP服务器中,往往需要由程序员维护一个缓存区,先将读到的数据放进缓存区中,然后再通过预先设定好的协议内容,来区分一段完整数据的开头、长度和结尾,
并将一段完整的数据交给逻辑部分处理。这就是自定义协议的功能。
而在Swoole
中,已经在底层实现了一个数据缓存区,并内置好了几个常用的协议类型,
直接在底层做好了数据的拆分,保证了在onReceive
回调函数中,一定能收到一个(或数个)完整的数据段。
数据缓存区的大小可以通过配置选项package_max_length
来控制。
下面我就将讲解如何使用这些内置协议。
EOF
标记型协议
第一个比较常用的协议就是EOF标记协议。协议的内容是通过规定一个一定不会出现在正常数据中的字符或者字符串,
用这个来标记一段完整数据的结尾
这样,只要发现这个结尾,就可以认定之前的数据已经结束,可以开始接收一个新的数据段了。
在Swoole
中,可以通过open_eof_check
和package_eof
两个配置项来开启。
其中,open_eof_check
指定开启了EOF
检测,package_eof
指定了具体的EOF
标记
通过这两个选项,Swoole底层就会自动根据EOF标记来缓存和拆分收到的数据包
1 | $this->serv->set(array( |
就这样,swoole
就已经开启了EOF标记协议的解析。那么让我们来测试一下效果:
- 服务器
1 | // server |
- 客户端
1 | $msg_eof = "This is a Msg\r\n"; |
然后运行一下,你会发现:哎不对啊,为什么还是一次收到了好多数据啊!
这是因为,在Swoole中,采用的不是遍历识别的方法,而只是简单的检查每一次接收到的数据的末尾是不是定义好的EOF标记。因此,在开启EOF检测后,onReceive回调中还是可能会一次收到多个数据包。
这要怎么办?你会发现,虽然是多个数据包,但是实际上收到的是N个完整的数据片段,那就只需要根据EOF把每个包再拆出来,一个个处理就好啦。
- 修改后的服务器端代码如下:
1 | // server |
固定包头类型协议
固定包头协议是在实际应用中最常用的协议。
该协议的内容是规定一个固定长度的包头,在包头的固定位置有一个指定好的字段存放了后续数据的实际长度
这样,服务器端可以先读取固定长度的数据,从中提取出长度,然后再读取指定长度的数据,即可获取一段完整的数据。
在Swoole中,同样提供了固定包头的协议格式。
需要注意的是,Swoole
只允许二进制形式的包头,因此,需要使用pack
、unpack
来打包、解包。
通过设置
open_length_check
选项,即可打开固定包头协议解析功能
。此外还有package_length_offset
,package_body_offset
和package_length_type
三个配置项用于控制解析功能。
package_length_offset
规定了包头中第几个字节开始是长度字段,package_body_offset
规定了包头的长度,package_length_type
规定了长度字段的类型。
1 | $this->serv->set(array( |
实例
- 服务器端
1 | public function onReceive(swoole_server $serv, $fd, $from_id, $data) |
- 客户端
1 | $msg_lenght = pack("N", strlen($msg_normal)).$msg_normal; |
特别篇:Http
协议-Swoole
内置的http_server
从Swoole-1.7.7-stable
开始,Swoole
在内部封装并实现了一个Http
服务器。
是的,没错,再也不用在PHP
层缓存和解析http
协议了,Swoole
直接内置Http
服务器了。
swoole_http_server
1 | $http = new swoole_http_server("127.0.0.1", 9501); |
只需创建一个
swoole_http_server
对象并设置onRequest
回调函数,即可实现一个http
服务器。
在onRequest回调中,共有两个参数。
- 参数
$request
存放了来自客户端的请求,包括Http
请求的头部信息、Http
请求相关的服务器信息、Http
请求的GET
和POST
参数以及HTTP
请求携带的COOKIE
信息。 - 参数
$response
用于发送数据给客户端,可以通过该参数设置HTTP响应的Header
信息、cookie
信息和状态码。
此外,swoole_http_server
还提供WebSocket
功能,使用此功能需要设置onMessage
回调函数,如下:
1 | $http_server->on('message', function(swoole_http_request $request, swoole_http_response $response) { |
通过$request->message
获取WebSocket
发送来的消息,再通过$response->message()
回复消息即可。