基础知识
四种设置回调函数的方式
- 匿名函数
$server->on('Request', function ($req, $resp) use ($a, $b, $c) {
echo "hello world";
});
!> 可使用use
向匿名函数传递参数
- 类静态方法
class A
{
static function test($req, $resp)
{
echo "hello world";
}
}
$server->on('Request', 'A::Test');
$server->on('Request', array('A', 'Test'));
!> 对应的静态方法必须为public
- 函数
function my_onRequest($req, $resp)
{
echo "hello world";
}
$server->on('Request', 'my_onRequest');
- 对象方法
class A
{
function test($req, $resp)
{
echo "hello world";
}
}
$object = new A();
$server->on('Request', array($object, 'test'));
!> 对应的方法必须为public
同步IO/异步IO
在Swoole4+
下所有的业务代码都是同步写法(Swoole1.x
时代才支持异步写法,现在已经移除了异步客户端,对应的需求完全可以用协程客户端实现),完全没有心智负担,符合人类思维习惯,但同步的写法底层可能有同步IO/异步IO
之分。
无论是同步IO/异步IO,Swoole/Server
都可以维持大量TCP
客户端连接(参考SWOOLE_PROCESS模式)。你的服务是阻塞还是非阻塞不需要单独的配置某些参数,取决于你的代码里面是否有同步IO的操作。
什么是同步IO:
简单的例子就是执行到Mysql->query
的时候,这个进程什么事情都不做,等待Mysql返回结果,返回结果后再向下执行代码,所以同步IO的服务并发能力是很差的。
什么样的代码是同步IO:
- 没有开启一键协程化的时候,那么你的代码里面绝大部分涉及IO的操作都是同步IO的,协程化后,就会变成异步IO,进程不会傻等在那里,参考协程调度。
- 有些
IO
是没法一键协程化,没法将同步IO变为异步IO的,例如MongoDB
(相信Swoole
会解决这个问题),需要写代码时候注意。
!> 协程,是为了提高并发的,如果我的应用就没有高并发,或者必须要用某些无法异步化IO的操作(例如上文的MongoDB),那么你完全可以不开启一键协程化,
关闭enable_coroutine,多开一些Worker
进程,这就是和Fpm/Apache
是一样的模型了,值得一提的是由于Swoole
是常驻进程的,
即使同步IO性能也会有很大提升,实际应用中也有很多公司这样做。
同步IO转换成异步IO
上小节介绍了什么是同步/异步IO,在Swoole
下面,有些情况同步的IO
操作是可以转换成异步IO的。
//利用inotify监控文件变化
$fd = inotify_init();
//将$fd添加到Swoole的EventLoop
Swoole\Event::add($fd, function () use ($fd){
$var = inotify_read($fd);//文件发生变化后读取变化的文件。
var_dump($var);
});
上述代码如果不调用Swoole\Event::add
将IO异步化,直接inotify_read()
将阻塞Worker进程,其他的请求将得不到处理。
- 使用
Swoole\Server
的sendMessage()方法进行进程间通讯,默认sendMessage
是同步IO,但有些情况是会被Swoole
转换成异步IO,用User进程举例:
$serv = new Swoole\Server("0.0.0.0", 9501, SWOOLE_BASE);
$serv->set(array(
'worker_num' => 1,
));
$serv->on('pipeMessage', function ($serv, $src_worker_id, $data) {
echo "#{$serv->worker_id} message from #$src_worker_id: $data\n";
sleep(10);//不接收sendMessage发来的数据,缓冲区将很快写满
});
$serv->on('receive', function (swoole_server $serv, $fd, $reactor_id, $data) {
});
//情况1:同步IO(默认行为)
$userProcess = new Swoole\Process(function ($worker) use ($serv) {
while (1) {
var_dump($serv->sendMessage("big string", 0));//默认情况下,缓存区写满后,此处会阻塞
}
}, false);
//情况2:通过enable_coroutine参数开启UserProcess进程的协程支持,为了防止其他协程得不到 EventLoop 的调度,
//Swoole会把sendMessage转换成异步IO
$enable_coroutine = true;
$userProcess = new Swoole\Process(function ($worker) use ($serv) {
while (1) {
var_dump($serv->sendMessage("big string", 0));//缓存区写满后,不会阻塞进程,会报错
}
}, false, 1, $enable_coroutine);
//情况3:在UserProcess进程里面如果设置了异步回调(例如设置定时器、Swoole\Event::add等),
//为了防止其他回调函数得不到 EventLoop 的调度,Swoole会把sendMessage转换成异步IO
$userProcess = new Swoole\Process(function ($worker) use ($serv) {
swoole_timer_tick(2000, function ($interval) use ($worker, $serv) {
echo "timer\n";
});
while (1) {
var_dump($serv->sendMessage("big string", 0));//缓存区写满后,不会阻塞进程,会报错
}
}, false);
$serv->addProcess($userProcess);
$serv->start();
- 同理,Task进程通过
sendMessage()
进程间通讯是一样的,不同的是task进程开启协程支持是通过Server的task_enable_coroutine配置开启,并且不存在情况3
,也就是说task进程不会因为开启异步回调就将sendMessage异步IO。
什么是EventLoop
所谓EventLoop
,即事件循环,可以简单的理解为epoll_wait,我们会把所有要发生事件的句柄(fd)加入到epoll_wait
中,这些事件包括可读,可写,出错等。
我们的进程就阻塞在epoll_wait这个内核函数上,当发生了事件(或超时)后epoll_wait
这个函数就会结束阻塞返回结果,就可以回调相应的PHP函数,例如,收到客户端发来的数据,回调OnRecieve
回调函数。
当有大量的fd放入到了epoll_wait中,并且同时产生了大量的事件,epoll_wait函数返回的时候我们就会挨个调用相应的回调函数,叫做一轮事件循环,即IO多路复用,然后再次阻塞调用epoll_wait
进行下一轮事件循环。
TCP粘包问题
在没有并发的情况下快速启动中的代码可以正常运行,但是并发高了就会有粘包问题,TCP
协议在底层机制上解决了UDP
协议的顺序和丢包重传问题。但相比UDP
又带来了新的问题,TCP
协议是流式的,数据包没有边界。应用程序使用TCP
通信就会面临这些难题。
因为TCP
通信是流式的,在接收1
个大数据包时,可能会被拆分成多个数据包发送。多次Send
底层也可能会合并成一次进行发送。这里就需要2个操作来解决:
- 分包:
Server
收到了多个数据包,需要拆分数据包 - 合包:
Server
收到的数据只是包的一部分,需要缓存数据,合并成完整的包
所以TCP网络通信时需要设定通信协议。常见的TCP通用网络通信协议有HTTP
、HTTPS
、FTP
、SMTP
、POP3
、IMAP
、SSH
、Redis
、Memcache
、MySQL
。
值得一提的是,Swoole内置了很多常见通用协议的解析,来解决这些协议的服务器的粘包问题,只需要简单的配置即可,参考open_http_protocol/open_http2_protocol/open_websocket_protocol/open_mqtt_protocol
除了通用协议外还可以自定义协议。Swoole
支持了2
种类型的自定义网络通信协议。
- EOF结束符协议
EOF
协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如Memcache
、FTP
、SMTP
都使用\r\n
作为结束符。发送数据时只需要在包末尾增加\r\n
即可。使用EOF
协议处理,一定要确保数据包中间不会出现EOF
,否则会造成分包错误。
在Server
和Client
的代码中只需要设置2
个参数就可以使用EOF
协议处理。
$server->set(array(
'open_eof_split' => true,
'package_eof' => "\r\n",
));
$client->set(array(
'open_eof_split' => true,
'package_eof' => "\r\n",
));
但上述EOF
的配置性能会比较差,Swoole会遍历每个字节,查看数据是否是"\r\n",除了上述方式还可以这样设置。
$server->set(array(
'open_eof_check' => true,
'package_eof' => "\r\n",
));
$client->set(array(
'open_eof_check' => true,
'package_eof' => "\r\n",
));
这组配置性能会好很多,不用遍历数据,但是只能解决分包
问题,没法解决合包
问题,也就是说可能onReceive
一下收到客户端发来的好几个请求,需要自行分包,例如explode("\r\n", $data)
,
这组配置的最大用途是,如果请求应答式的服务(例如终端敲命令),无需考虑拆分数据的问题。原因是客户端在发起一次请求后,必须等到服务器端返回当前请求的响应数据,才会发起第二次请求,不会同时发送2
个请求。
- 固定包头+包体协议
固定包头的方法非常通用,在服务器端程序中经常能看到。这种协议的特点是一个数据包总是由包头+包体2
部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用2
字节/4
字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就是完整的数据包。Swoole
的配置可以很好的支持这种协议,可以灵活地设置4
项参数应对所有情况。
Server
在onReceive回调函数中处理数据包,当设置了协议处理后,只有收到一个完整数据包时才会触发onReceive事件。客户端在设置了协议处理后,调用 $client->recv() 不再需要传入长度,recv
函数在收到完整数据包或发生错误后返回。
$server->set(array(
'open_length_check' => true,
'package_max_length' => 81920,
'package_length_type' => 'n', //see php pack()
'package_length_offset' => 0,
'package_body_offset' => 2,
));
!> 具体每个配置的含义参见服务端/客户端
章节的配置小节
什么是IPC
同一台主机上两个进程间通信(简称IPC)的方式有很多种,在Swoole下我们使用了2种方式Unix Socket和sysvmsg,下面分别介绍:
-
Unix Socket
全名 UNIX Domain Socket, 简称
UDS
, 使用套接字的API(socket,bind,listen,connect,read,write,close等),和TCP/IP不同的是不需要指定ip和port,而是通过一个文件名来表示(例如FPM和Nginx之间的/tmp/php-fcgi.sock
),UDS是Linux内核实现的全内存通信,无任何IO
消耗。在1
进程write
,1
进程read
,每次读写1024
字节数据的测试中,100
万次通信仅需1.02
秒,而且功能非常的强大,Swoole
下默认用的就是这种IPC方式。-
SOCK_STREAM
和SOCK_DGRAM
Swoole
下面使用UDS
通讯有两种类型,SOCK_STREAM
和SOCK_DGRAM
,可以简单的理解为TCP和UDP的区别,当使用SOCK_STREAM
类型的时候同样需要考虑TCP粘包问题。- 当使用
SOCK_DGRAM
类型的时候不需要考虑粘包问题,每个send()
的数据都是有边界的,发送多大的数据接收的时候就收到多大的数据,没有传输过程中的丢包、乱序问题,send
写入和recv
读取的顺序是完全一致的。send
返回成功后一定是可以recv
到。
在IPC传输的数据比较小时非常适合用
SOCK_DGRAM
这种方式,由于IP
包每个最大有64k的限制,所以用SOCK_DGRAM
进行IPC时候单次发送数据不能大于64k,同时要注意收包速度太慢操作系统缓冲区满了会丢弃包,因为UDP是允许丢包的,可以适当调大缓冲区。 -
-
sysvmsg
即Linux提供的
消息队列
,这种IPC
方式通过一个文件名来作为key
进行通讯,这种方式非常的不灵活,实际项目使用的并不多,不做过多介绍。-
此种IPC方式只有两个场景下有用:
- 防止丢数据,如果整个服务都挂掉,再次启动队列中的消息也在,可以继续消费,但同样有脏数据的问题。
- 可以外部投递数据,比如Swoole下的
Worker进程
通过消息队列给Task进程
投递任务,第三方的进程也可以投递任务到队列里面让Task消费,甚至可以在命令行手动添加消息到队列。
-