PHP IO编程epoll实现方案

13 篇文章 0 订阅

什么是EPOll,PHP如何实现epoll 模式的IO?

epoll:https://zhuanlan.zhihu.com/p/361750240

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

<?php

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模型通信
拓展地址:https://pecl.php.net/package/event
官网地址:https://bitbucket.org/osmanov/pecl-event/src/master/examples

拓展安装

# 下载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
 
./configure --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

<?php
$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 直接创建服务即可

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值