vlambda博客
学习文章列表

PHP网络编程之抽象一个event-loop(十八节)

みなさんこんにちは「老李」,既知の「谢顶道人」。


事情是这样的,做为一个跟我旗鼓相当的「谢顶法师」--- 老赵跟我说:



一部分格局比较大的泥腿子估计内心早就已经预见到了这种「风口」,虽然可能没有做好迎接这种「风口」的物理准备,但也至少做好了心理准备,没关系迟早还会📈回来的。


最近的瓜都比较带有黑色幽默的味道,比如「影视表演艺术家」的粉丝们开团群挑一些文学创作网站并成功使之被加入到404名单,其规模、其气势、其打法之凌厉、其战略之大手笔,堪称典范以至于发展到后面,已经冲破了双方所能掌控的范围。


  • 首先是我几乎(注意是几乎)只发技术类文章,人生导师、职场教育、副业赚钱、年薪百万的我资历尚浅都讲不了

  • 其次是写技术文章也还是挺麻烦的,一是技术本身是否讲解到位,二是文案助攻是否给力,最后还得考虑配图


今天我们说event-loop,确切说是总结并抽象event-loop,谢顶道人老李带着大家已经从socket基础到select IO复用再到epoll(实际上是event扩展),基本上把基础从头到尾撸了一边,还顺带搞定了同步、异步、阻塞、非阻塞等概念,你要说你没任何收获:


至于你信不信,我反正不信


众所周知,Workerman实际上是一个大一统的结合体,整合了TCP、UDP等一坨协议,纠集了进程控制与管理,囊括了各式各样的event类库,总之就是各种两开花。然而有时候,我们需要独立可拆分的组件,比如啥时候呢?就比如说今年年底,在贵司平台治理部门总包工头轩辕秃顶同志的牵头下,平台治理部联合基础平台部以及安全审计部等数个部门展开了「迎新春、庆鼠年」的服务组件拆分两开花特别行动,而你 --- 欧阳将负责将PHP语言的Event-Loop抽象并组件化,最终成绩算入年底OKR考核并与年终奖直接挂钩。


这就是明摆着老板原上草决意送大家免费福报,而你也决定「多快好省」地完成任务,于是你瞄准了github上赫赫有名的Reactphp:


PHP网络编程之抽象一个event-loop(十八节)


ReactPHP是如下图这样shai儿得,TA把event-loop直接抽象出来作为了一个底层基础组件,这样基于这个event-loop组件可以快速实现stream服务、http服务、socket服务等...



所以今天欧阳的任务就是综合一下之前的章节,抽象出一个event-loop组件。文件目录树是这样的:


-Event/               // Event目录|------Libevent.php   // 本次基于Event扩展实现的|------Select.php     // 课后作业?|------Libev.php // 课后作业?|------Factory.php // 课后作业?-index.php            // 入口文件


Event/Libevent.php:


<?phpnamespace Event;class Libevent { const EV_ALL = 1; const EV_READ = 2; const EV_WRITE = 4; const EV_EXCEPTION = 8; public $o_event_config = null;    public $o_event_base   = null; /* * @desc : 保存事件event * */ public $a_event = array();    public $a_client = array(); public function __construct() { $o_event_config = new \EventConfig(); $o_event_base = new \EventBase( $o_event_config ); $this->o_event_config = $o_event_config; $this->o_event_base = $o_event_base;    } /* * @desc : 添加一个事件 * @param : socket fd * @param : event type, EV_READ EV_WRITE * @param : callback * */ public function add( $r_fd, $i_event_type, $f_callback ) { $i_event_flag = $i_event_type == self::EV_READ ? \Event::READ | \Event::PERSIST : \Event::WRITE | \Event::PERSIST ; $o_event = new \Event( $this->o_event_base, $r_fd, $i_event_flag, $f_callback ); $o_event->add(); $i_fd = intval( $r_fd ); $this->a_event[ $i_fd ][ $i_event_type ] = $o_event; $this->a_client[ $i_fd ] = $r_fd; //echo json_encode( $this->a_client )." | ".json_encode( $this->a_event ).PHP_EOL;    } /* * @desc : 删除一个事件 * */ public function del( $r_fd, $i_event_type ) { $i_fd = intval( $r_fd ); // 删除所有事件类型 if ( self::EV_ALL === $i_event_type ) { if ( isset( $this->a_event[ $i_fd ] ) ) { if ( isset( $this->a_event[ $i_fd ][ self::EV_WRITE ] ) ) { echo "del write".PHP_EOL; $o_event = $this->a_event[ $i_fd ][ self::EV_WRITE ]; $o_event->free(); unset( $this->a_event[ $i_fd ][ self::EV_WRITE ] ); } if ( isset( $this->a_event[ $i_fd ][ self::EV_READ ] ) ) { echo "del read".PHP_EOL; $o_event = $this->a_event[ $i_fd ][ self::EV_READ ]; $o_event->free(); unset( $this->a_event[ $i_fd ][ self::EV_READ ] ); } if ( isset( $this->a_event[ $i_fd ][ self::EV_EXCEPTION ] ) ) { echo "del exception".PHP_EOL; $o_event = $this->a_event[ $i_fd ][ self::EV_EXCEPTION ]; $o_event->free(); unset( $this->a_event[ $i_fd ][ self::EV_EXCEPTION ] ); } } } // 删除指定事件类型 else { if ( isset( $this->a_event[ $i_fd ] ) ) { if ( isset( $this->a_event[ $i_fd ][ $i_event_type ] ) ) { $o_event = $this->a_event[ $i_fd ][ $i_event_type ]; $o_event->free(); unset( $this->a_event[ $i_fd ][ $i_event_type ] ); } } } unset( $this->a_client[ $i_fd ] );    } /* * @desc : 陷入事件循环 * */ public function loop() { $this->o_event_base->loop();    }}


index.php:


<?phpdefine( "DS", DIRECTORY_SEPARATOR );define( "ROOT", __DIR__ );spl_autoload_register( function( $s_class_name ) { $s_path = str_replace( "\\", "/", $s_class_name ); $s_file = ROOT.DS.$s_path.'.php'; require_once $s_file;} );

// 创建一个非阻塞的listen-socket资源$r_listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_set_option( $r_listen_socket, SOL_SOCKET, SO_REUSEADDR, 1 );socket_set_option( $r_listen_socket, SOL_SOCKET, SO_REUSEPORT, 1 );socket_bind( $r_listen_socket, '0.0.0.0', 6666 );socket_listen( $r_listen_socket );socket_set_nonblock( $r_listen_socket );

// 创建一个Event-Loop// 然后将上面的listen-socket加入到Event-Loop中// 这里就已经比较直白了,只要你要创建一个资源// 这个资源可以是socket、可以是stream等等// 因为Event-Loop已经被我们抽象称了模块// 所以你创建的资源只要支持Event,那么都很方便地通过Event-Loop模块写出来$o_event_loop = new Event\Libevent();$o_event_loop->add( $r_listen_socket, Event\Libevent::EV_READ, function() use( $r_listen_socket, $o_event_loop ) { $r_connection_socket = socket_accept( $r_listen_socket ); $o_event_loop->add( $r_connection_socket, Event\Libevent::EV_WRITE, function() use ( $r_connection_socket, $o_event_loop ) { $s_data = "Hello HTTP World!"; $s_html = "HTTP/1.1 200 OK\r\nContent-Length: ".strlen( $s_data )."\r\n\r\n{$s_data}\n"; $i_written = socket_write( $r_connection_socket, $s_html, strlen( $s_html ) ); $o_event_loop->del( $r_connection_socket, Event\Libevent::EV_ALL ); });} );$o_event_loop->loop();


流程和逻辑上是不是清爽无比?说下大概逻辑:

  • 第一步:在PHP里各种能支持Event-Loop的资源,比如socket、比如stream

  • 第二步:初始化Event-Loop,然后将第一步里创建好的资源扔到Event-Loop里

  • 第三步:完成


我留了一些课外作业,主要是我太懒了懒得弄,一个完整的Event-Loop模块还差一个PHP Interface,然后Libevent、Select、Libev都实现这个Interface,而Factory.php则系统则根据系统当前已经具备的IO复用自动选择最好的IO复用方法并初始化Event-Loop对象!


有了这样一个Event-Loop组件,你可以很快基于TA实现一个「基于事件监听的异步非阻塞高性能」HTTP服务器、TCP服务器、PHP版的Redis服务器、PHP版本的Memcache服务器,这叫啥?这就叫「高内聚、低耦合」,玩的就是专业,周杰棍是怎么唱的来着?


一段我写好的库文件,github一放好多年,TA一直在身边


我写的是越来越省劲轻松,当然我也希望你们也越看越轻松。