IO复用

IO复用

  • 概念介绍
       IO复用可以让单进程同时监听大量文件描述符的技术,非常适用于IO密集型应用.
  • 阻塞与非阻塞
        在介绍IO复用技术之前,先介绍一下阻塞和非阻塞,在我们前几节的WEB服务器中,调用socket_accept函数会使整个进程阻塞,直到有新连接,操作系统才唤醒进程继续执行。而非阻塞模式, stream_socket_accept的行为就不一样了,如果没有新连接,不会阻塞进程,而是马上返回false。
  •  select
        使用select会轮询连接池,当有连接可读或可写时,select函数返回可读写的连接数,然后再轮询一遍连接池,查找活动连接进行读写操作。
比较尴尬的是,socket_select只支持socket类型的资源,而不支持stream类型的资源,所以这里需要使用socket_create创建socket资源.


<?php
defined("PORT") or define("PORT", isset($argv[1]) ? $argv[1] : 8090);
/*=======================select model=======================*/

$stream = stream_socket_server("tcp://0.0.0.0:" . PORT, $errorNo, $errorMsg, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create([]));
stream_set_blocking($stream, 0);

$socket = socket_import_stream($stream);
@socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
@socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);

if (function_exists('stream_set_read_buffer')) {
    stream_set_read_buffer($stream, 0);
}

$read = $clients = $write = $except = array();

for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    if ($pid < 0) {
        exit(0);
    } elseif ($pid == 0) {
        while (true) {
            $read = array_merge($clients, array($stream));
            $num = stream_select($read, $write, $except, 0, 100000000);

            if ($num <= 0) {
                continue;
            }

            if (in_array($stream, $read)) {
                $client = stream_socket_accept($stream);
                stream_set_blocking($client, 0);
                $clients[] = $client; //此处必须把$client 另存到$clients数组中,不能直接放到$read 中,否则其他客户端连上,之前连上的客户端会全部断掉
                unset($read[array_search($stream, $read)]);
            }

            foreach ($clients as $key => $value) {
                $string = '';
                $data = @fread($value, 65535);
                if ($data === '' || $data === false) {
                    if ((feof($value) || !is_resource($value) || $data === false)) {
                        fclose($value);
                    }
                }
                $string .= $data;
                if ($string) {
                    echo $string . "\n";
                    fwrite($value, "hello\n");
                }
            }
        }
    }
}
exit(0);
select虽然可以监听多个连接,但是它最多只能监听1024个连接。这虽然在poll中得到了改进,但是select和poll本质上都是通过轮询的方式进行监听,这意味着当监听了上万连接时,就算只有一个连接是活动的,依然要把上万连接都遍历一次。显然,这无疑是极大的性能浪费,而epoll的出现彻底地解决了这个问题


epoll
epoll并不是只有一个函数来实现,而是多个函数。我们这里并不讨论epoll相关的函数,因为PHP并不提供相关的函数,但它提供了基于libevent库的libevent扩展,以及基于libevent库的event扩展。libevent库实现了Reactor模型,关于Reactor模型,这里只作简单的介绍


Reactor模型,包含了几个组件:句柄,事件分发器,事件处理器。


句柄,就是文件描述符,在Socket编程中,就是使用socket_create创建的socket资源.
事件分发器, 通过事件循环,事件循环是通过诸如epoll``Select``Poll等IO复用技术实现的,监听句柄期待的事件是否发生,发生了则将事件分发给事件处理器.
事件处理器,当事件发生时,处理相关的逻辑.
而libevent库已经实现了Reactor模型,我们可以开箱即用。下面,我们将通过libevent对我们的WEB服务器再次改造,使它的处理并发的能力再次提高


在此之前,我们需要安装event扩展,安装event扩展必须安装libevent库


sudo apt-get update
sudo apt-get install php5-dev
cd
wget https://github.com/downloads/libevent/libevent/libevent-2.0.20-stable.tar.gz
注意:若遇到链接无法访问的情况,请尝试使用这个地址:http://labfile.oss-cn.aliyuncs.com/courses/987/libevent-2.0.20-stable.tar.gz


解压并编译


tar -xvf libevent-2.0.20-stable.tar.gz
cd libevent-2.0.20-stable
./configure
make 
sudo make install
再通过pecl方式安装event,安装过程中,在询问要不要支持openssl时,输入no,其它都是默认直接回车


sudo pecl install event
安装完之后, sudo vim /etc/php5/cli/conf.d/20-event.ini


添加扩展extension=event.so,保存退出,运行以下代码进行验证是否安装成功


php -m | grep event
面向对象写法:
defined("PORT") or define("PORT", isset($argv[1]) ? $argv[1] : 8090);
/*=======================epoll model=======================*/


$stream = stream_socket_server("tcp://0.0.0.0:" . PORT, $errorNo, $errorMsg, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create([]));
stream_set_blocking($stream, 0);

$socket = socket_import_stream($stream);
@socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
@socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);

if (function_exists('stream_set_read_buffer')) {
    stream_set_read_buffer($stream, 0);
}

class myEvent
{
    protected $eventBase;
    protected $allEvents = [];

    public function __construct()
    {
        if (!extension_loaded('event')) {
            echo 'event extension is require' . PHP_EOL;
            exit(250);
        }
        $this->eventBase = new \EventBase();
    }

    public function add($fd, $flag, $func)
    {
        $fd_key = (int) $fd;
        $event = new \Event($this->eventBase, $fd, $flag | \Event::PERSIST, $func, $fd);
        if (!$event || !$event->add()) {
            return false;
        }
        $this->allEvents[$fd_key][$flag] = $event; // 此处有坑,必须把$event 存到不会销毁的变量中,否则事件轮询起不来
        return true;
    }
    public function loop()
    {
        $this->eventBase->loop();
    }
}


class myRead
{
    public $event = null;
    public function __construct($event)
    {
        $this->event = $event;
    }

    public function read($socket)
    {
        $string = '';
        $data = @fread($socket, 65535);
        if ($data === '' || $data === false) {
            if ((feof($socket) || !is_resource($socket) || $data === false)) {
                fclose($socket);
                return ;
            }
        }

        $string .= $data;
        if ($string) {
            echo $string . "\n";
            fwrite($socket, "hello\n");
        }
    }


    public function accept($socket)
    {
        $new_socket = @stream_socket_accept($socket, 0);
        stream_set_blocking($new_socket, 0);

        if (function_exists('stream_set_read_buffer')) {
            stream_set_read_buffer($new_socket, 0);
        }
        $this->event->add($new_socket, \Event::READ, [$this, 'read'], $new_socket);
    }
}


for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    if ($pid < 0) {
        exit(0);
    } elseif ($pid == 0) {
        $event = new MyEvent();
        $myRead = new myRead($event);
        $event->add($stream, \Event::READ, [$myRead, 'accept'], $stream);
        $event->loop();
    }
}
exit(0);


面向过程写法

defined("PORT") or define("PORT", isset($argv[1]) ? $argv[1] : 8090);
/*=======================epoll model=======================*/

$stream = stream_socket_server("tcp://0.0.0.0:" . PORT, $errorNo, $errorMsg, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create([]));
stream_set_blocking($stream, 0);

$socket = socket_import_stream($stream);
@socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
@socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);

if (function_exists('stream_set_read_buffer')) {
    stream_set_read_buffer($stream, 0);
}
$events = [];

function accept($fd, $flag, $eventBase)
{
    global $events;
    $new_socket = @stream_socket_accept($fd, 0);
    stream_set_blocking($new_socket, 0);

    if (function_exists('stream_set_read_buffer')) {
        stream_set_read_buffer($new_socket, 0);
    }
    $event = new Event($eventBase, $new_socket, Event::READ | Event::PERSIST, 'read', $new_socket);
    $event->add();
    $events[] = $event;
}

function read($fd)
{
    $string = '';
    $data = @fread($fd, 65535);
    if ($data === '' || $data === false) {
        if ((feof($fd) || !is_resource($fd) || $data === false)) {
            fclose($fd);
            return;
        }
    }

    $string .= $data;
    if ($string) {
        echo $string . "\n";
        fwrite($fd, "hello\n");
    }
}

$eventBase = new EventBase();
$event = new Event($eventBase, $stream, Event::READ | Event::PERSIST, 'accept', $eventBase);

//把事件注册到事件循环器中
$event->add();
//开始事件循环
$eventBase->loop();

流程和创建Reactor模型一致

  • 创建句柄
  • 创建事件循环器
  • 创建事件,并指定事件监听的事件类型及注册事件处理器
  • 向循环器中添加事件
  • 这里我们主要看Event类,看看它的构造函数原型:


public Event::__construct ( EventBase $base , mixed $fd , int $what , callable $cb [, mixed $arg = NULL ] )

  • base: EventBase类的实例
  • fd: 要监听的句柄
  • what: 要监听的事件类型
  • cb: 事件处理器,在PHP中就是回调函数
  • arg: 事件处理器的参数列表

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux IO复用是指在处理多个I/O事件时,通过一种机制使得一个进程可以同时监听多个I/O操作,从而提高程序的效率和性能。 在Linux系统中,常用的IO复用机制有三种:select、poll和epoll。 1. select:select函数是最早引入的IO复用机制之一,它通过传入一组文件描述符集合,来监听这些文件描述符上是否有事件发生。当其中任意一个文件描述符上有事件发生时,select函数就会返回,然后程序可以通过遍历文件描述符集合来判断哪些文件描述符上有事件发生。 2. poll:poll函数是对select的改进,其使用方式和select类似。不同的是,poll函数使用一个pollfd结构数组来存储待监听的文件描述符及其对应的感兴趣事件,通过调用poll函数时传入这个数组来实现IO复用。相对于select,poll没有最大文件描述符数量的限制,并且效率更高。 3. epoll:epoll是Linux下最新的IO复用机制,它提供了更加高效的IO事件通知机制。epoll使用一个文件描述符来管理被监听的其他文件描述符,通过调用epoll_ctl函数向这个文件描述符中注册或者删除需要监听的文件描述符。当某个文件描述符上有事件发生时,epoll_wait函数会返回该文件描述符的相关信息给程序处理。相对于select和poll,epoll在处理大量连接时具有更好的性能。 总结来说,Linux IO复用机制可以让一个进程同时监听多个I/O事件,避免了使用阻塞IO时的等待时间,提高了程序的效率和性能。而select、poll和epoll是常用的IO复用机制。其中,epoll是效率最高的一种机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值