升级第2代,区别在于:
放弃select方式,采用epoll。epoll应用在nginx等软件,成为高并发优秀软件的基础。epoll无需轮询,也无需在用户空间和内核之间切换,属于事件通知。好比后台系统采用先进的监听方式,并能快速的通知客服处理哪个。这里用的是swoole的event,基于epoll。
多进程,一个客服处理速度是有限的,那么就两个或者多个客服一同处理,后台会平均分配任务给客服。而多进程用的php扩展pcntl模块,另外套接字等等函数如果陌生的话,nginx系列也有类似的文章会比较详细,但最重要的是翻php手册。
废话不多说,扔下代码慢慢体会。
代码
<?php
class EpollServer
{
protected $_workerPids=[]; //子进程pid
protected $_workerNum=2; //子进程数
protected $_sAddr;
protected $_masterPid;//父进程pid
protected $onConnect; //链接事件回调
protected $onReceive; //消息事件回调
protected $onClose;//关闭事件回调
public function __construct($socket_addr){
$this->_sAddr = $socket_addr;
$this->_masterPid = posix_getpid();//当前进程pid 因在主进程空间故为主进程pid
var_dump('master pid:',$this->_masterPid);
}
public function start(){
$this->forkWorker($this->_workerNum);
}
public function forkWorker($worker_num)
{
for ($i=0; $i < $worker_num; $i++)
{
$pid=pcntl_fork();
if($pid<0){
exit('fork fail!');
}else if($pid>0){
//父进程空间
$this->_workerPids[]=$pid;
}else{
//子进程空间
$this->accept();//处理连接
exit;
}
}
var_dump('worker pids:',$this->_workerPids);
for ($i=0; $i < $worker_num; $i++) {
$status=0;
$work_pid=pcntl_wait($status); //阻塞
echo '回收'.$work_pid.PHP_EOL;
}
}
public function accept()
{
$connext_option=[
'socket'=>[
'backlog'=>10240, //等待处理连接的队列
],
];
$connext=stream_context_create($connext_option);
stream_context_set_option($connext,'socket','so_reuseaddr',1);
stream_context_set_option($connext,'socket','so_reuseport',1);
$main_sock=stream_socket_server($this->_sAddr,$errno,$errstr,STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,$connext);
swoole_event_add($main_sock,function($sfd){
$clnt_sock=stream_socket_accept($sfd);
if($sfd && is_callable($this->onConnect)){
call_user_func($this->onConnect,$sfd,$clnt_sock);
}
swoole_event_add($clnt_sock,function($cfd) use ($sfd){
$buf=fread($cfd,1024);
if(empty($buf)){
if(!is_resource($cfd) || feof($cfd) ){
fclose($cfd);
if(is_callable($this->onClose)){
call_user_func($this->onClose,$sfd,$cfd);
}
}
}else{
if(is_callable($this->onReceive)){
call_user_func($this->onReceive,$sfd,$cfd,$buf);
}
}
});
});
}
public function on($event,$closure)
{
if(empty($event) && is_callable($closure)){
echo "parameters error!".PHP_EOL;
exit;
}
$event=lcfirst($event);
switch ($event) {
case 'connect':
$this->onConnect = $closure;
break;
case 'receive':
$this->onReceive = $closure;
break;
case 'close':
$this->onClose = $closure;
break;
default:
echo "invalid event!".PHP_EOL;
exit;
break;
}
}
}
$epoll_server=new EpollServer('tcp://0.0.0.0:6001');
$epoll_server->on('connect',function($server,$fd){
echo 'client_no:'.(int)$fd.' has connected!'.PHP_EOL;
});
$epoll_server->on('receive',function($server,$fd,$data){
echo $data.PHP_EOL;
fwrite($fd, 'response');
});
$epoll_server->on('close',function($server,$fd){
echo 'client_no:'.(int)$fd.' has closed!'.PHP_EOL;
});
$epoll_server->start();
关于进程结构
对应的进程结构
请求
客户端
<?php
$client = new swoole_client(SWOOLE_SOCK_TCP);
$client->connect('127.0.0.1', 6001, -1);
for ($i=0; $i < 10 ; $i++) {
$client->send('hello!');
echo $client->recv().PHP_EOL;
}
$client->close();
服务端打印
压测
同样关闭所有窗口输出
本地压测
可以tcp压测:
for ($i=0; $i < 10000 ; $i++) {....
http本地压测:
$epoll_server->on('receive',function($server,$fd,$data){
$content="response";
$http_resonse = "HTTP/1.1 200 OK\r\n";
$http_resonse .= "Content-Type: text/html;charset=UTF-8\r\n";
$http_resonse .= "Connection: keep-alive\r\n"; //连接保持
$http_resonse .= "Server: php socket server\r\n";
$http_resonse .= "Content-length: ".strlen($content)."\r\n\r\n";
$http_resonse .= $content;
fwrite($fd, $http_resonse);
});
ab -n 100000 -c 10000 -k http://127.0.0.1:6001/
一般是没有压力的
外网压测:
ab -n 1025 -c 1025 -k http://39.xxx.xx.xx:6001/
响应速度一般(当然同样参数压测百度更糟糕),压测本身无太大意义,但是超过select系统最大支持的1024个长连接,epoll理论上是没有限制的。