PHP IO编程epoll实现方案
来源:http://a.nxw.so/21owAg
什么是EPOll
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
一、PHP 原生socket 实现IO(select模式)
现阶段php原生方法是没有办法使用epoll模型的,原生实现只能使用select模型,主要步骤如下:
//1:创建一个socket监听,参数ip:端口
$socket = stream_socket_server($socket_address);
//2:设置为非阻塞
stream_set_blocking($socket , 0);s
//3:监听socket 整个进程阻塞在这里,持续监听可读事件
//此处参数均为引用传递,在函数中会改变传值,第一个为要监听可读的socket数组,第二为可写的sockets,接口详情参考https://www.php.net/manual/zh/function.stream-select.php
while(true) {
stream_select($sockets, [],[], 60);
foreach ($sockets as $index => $socket) {
//TODO 处理有数据的socket
}
}
优点: 方便快捷,轻量化,不用引用依赖库
缺点 : 只能使用select IO 模型,单线程最大只能打开1024 个文件,Select 模型性能比较差,对并发比较高的场景不适用
完整demo
class SocketServer{
//监听socket
protected $socket = NULL;
//所有的socket连接
protected $sockets = array();
//连接事件回调
public $onConnect = NULL;
//断线事件回调
public $onClose = NULL;
//接收消息事件回调
public $onMessage = NULL;
public function __construct($socket_address) {
//创建一个socket监听
$this->socket = stream_socket_server($socket_address);
//设置为非阻塞
stream_set_blocking($this->socket, 0);
//将socket监听加入allSockets
$this->sockets[(int)$this->socket] = $this->socket;
}
public function run() {
while(true) {
//不监听可写事件与带外数据事件
$write = $except = array();
//监听所有的socket事件
$read = $this->sockets;
//整个进程阻塞在这里,持续监听可读事件
//此处参数均为引用传递,在函数中会改变传值
stream_select($read, $write, $except, 60);
//处理所有可读事件
foreach ($read as $index => $socket) {
//如果是监听socket,此处表示有新的连接
if ($socket === $this->socket) {
//通过stream_socket_accept获取新的连接
$new_conn_socket = stream_socket_accept($socket);
if ($this->onConnect) {
//触发连接事件的回调,并将当前连接传递给回掉函数
call_user_func($this->onConnect, $socket);
}
//记录此socket连接,以便于sream_select监听可读事件
$this->sockets[(int)$new_conn_socket] = $new_conn_socket;
} else
//如果可读事件不为监听socket,则表示对应客户端有数据发过来
{
//从连接中读取数据
$buffer = fread($socket, 65535);
//如果数据为空,表示客户端已经断开连接
if ('' === $buffer || false === $buffer) {
//尝试触发onClose回调
if ($this->onClose) {
call_user_func($this->onClose, $socket);
}
fclose($socket);
//关闭socket连接并从allSockets中删除
unset($this->sockets[(int)$socket]);
continue;
}
//表示一个正常的连接,已经读取到消息,交给回掉函数处理
if ($this->onMessage) {
call_user_func($this->onMessage, $socket, $buffer);
}
}
}
}
}
}
$server = new SocketServer('tcp://0.0.0.0:9501');
$server->onConnect = function ($conn) {
echo 'connect';
};
$server->onClose = function ($conn) {
echo 'close';
};
$server->onMessage = function ($conn, $message) {
$http_resonse = "HTTP/1.1 200 OK\r\n";
$http_resonse .= "Connection: keep-alive\r\n";
$http_resonse .= "Server: php socket server\r\n";
$http_resonse .= "Content-length: 11\r\n\r\n";
$http_resonse .= "hello world";
fwrite($conn, $http_resonse);
};
$server->run();
二、使用event扩展实现epoll(或者Libeven 拓展,两者选一个)
php event就是一个事件库,对市面上各种常用的IO复用技术的统一封装,通过它可以实现epoll io模型通信
拓展安装
# 下载event
wget https://pecl.php.net/get/event-3.0.3.tgz
# 解压文件
tar -xf event-3.0.3.tgz
# 进入目录
cd event-3.0.3
# 执行phpize
/www/server/php/72/bin/phpize
--with-php-config=/www/server/php/72/bin/php-config
# 安装
make && make install
#修改php.ini配置
extension = /www/server/php/72/lib/php/extensions/no-debug-non-zts-20170718/event.so
Demo 使用:https://cloud.tencent.com/developer/article/1586427
$s_host = '0.0.0.0';
$i_port = 9501;
$r_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_set_option( $r_listen_socket, SOL_SOCKET, SO_REUSEADDR, 1 );
socket_bind( $r_listen_socket, $s_host, $i_port );
socket_listen( $r_listen_socket );
// 将$listen_socket设置为非阻塞IO
socket_set_nonblock( $r_listen_socket );
$a_event_array = array();
$a_client_array = array();
// 创建event-base
$o_event_base = new EventBase();
$s_method_name = $o_event_base->getMethod();
if ( 'epoll' != $s_method_name ) {
exit( "not epoll" );
}
function read_callback( $r_connection_socket, $i_event_flag, $o_event_base ) {
$s_content = socket_read( $r_connection_socket, 1024 );
echo "接受到:".$s_content;
// 在这个客户端连接socket上添加 读事件
// 当这个客户端连接socket一旦满足可写条件,我们就可以向socket中写数据了
global $a_event_array;
global $a_client_array;
$o_write_event = new Event( $o_event_base, $r_connection_socket, Event::WRITE | Event::PERSIST, 'write_callback', array(
'content' => $s_content,
) );
$o_write_event->add();
$a_event_array[ intval( $r_connection_socket ) ]['write'] = $o_write_event;
}
function write_callback( $r_connection_socket, $i_event_flag, $a_data ) {
global $a_event_array;
global $a_client_array;
$s_content = $a_data['content'];
foreach( $a_client_array as $r_target_socket ) {
if ( intval( $r_target_socket ) != intval( $r_connection_socket ) ) {
socket_write( $r_target_socket, $s_content, strlen( $s_content ) );
}
}
$o_event = $a_event_array[ intval( $r_connection_socket ) ]['write'];
$o_event->del();
unset( $a_event_array[ intval( $r_connection_socket ) ]['write'] );
}
function accept_callback( $r_listen_socket, $i_event_flag, $o_event_base ) {
global $a_event_array;
global $a_client_array;
// socket_accept接受连接,生成一个新的socket,一个客户端连接socket
$r_connection_socket = socket_accept( $r_listen_socket );
$a_client_array[] = $r_connection_socket;
// 在这个客户端连接socket上添加 读事件
// 也就说 要从客户端连接上读取消息
$o_read_event = new Event( $o_event_base, $r_connection_socket, Event::READ | Event::PERSIST, 'read_callback', $o_event_base );
$o_read_event->add();
$a_event_array[ intval( $r_connection_socket ) ]['read'] = $o_read_event;
}
// 在$listen_socket上添加一个 读事件
// 为啥是读事件?
// 因为$listen_socket上发生事件就是:客户端建立连接
// 所以,应该是读事件
$o_event = new Event( $o_event_base, $r_listen_socket, Event::READ | Event::PERSIST, 'accept_callback', $o_event_base );
$o_event->add();
//$a_event_array[] = $o_event;
$o_event_base->loop();
优点: 支持epoll模型,并发性能比较好,足够灵活,适合写框架使用
缺点:文档,教程比较少,很多靠自己摸索,封装度不高,在项目中使用需要二次封装
三、Swoole
官网:https://www.swoole.com/
文档:https://wiki.swoole.com/#/
Swoole 高度封装了各种通信服务 如:tcp,http,webscoket等,使用简单方便,内部都是使用epoll调用
$server = new Swoole\Server('127.0.0.1', 9503);
$server->on('start', function ($server) {
echo "TCP Server is started at tcp://127.0.0.1:9503\n";
});
$server->on('connect', function ($server, $fd){
echo "connection open: {$fd}\n";
});
$server->on('receive', function ($server, $fd, $reactor_id, $data) {
$server->send($fd, "Swoole: {$data}");
});
$server->on('close', function ($server, $fd) {
echo "connection close: {$fd}\n";
});
$server->start();
优点:可以使用epoll模型,效率高,封装度高,使用方便,文档完善
缺点:框架庞大,与传统FPM 编程完全不同,很多操作容易引起程序异常,入门门槛比较高
总结
现阶段php原生不支持epoll模式的IO调用,可以通过三方拓展Event实现epoll调用,或者使用swoole 直接创建服务即可
简单分享快乐学习,如有错误请多包涵!
PS:如果没有你的关注,那我所做的将毫无意义!
欢迎分享,点赞,在看。