PHP网络编程各种IO模型的演示与分析

1.单进程同步阻塞模型

该模型是最简单的一个模型,服务端采用单进程创建socket套接字,阻塞监听处理客户端的请求,短连接的处理流程如下图所示:

单进程同步阻塞短连接模型
$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);

if (!$socket) {
    echo "$errstr ($errno)".PHP_EOL;
} else {
    while ($conn = stream_socket_accept($socket)) {
        echo '客户端连接成功'.PHP_EOL;
        $content = "Hello Client,".date('Y-m-d: H:i:s', time());
        $httpResponse = "HTTP/1.1 200 OK\r\n";
        $httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
        $httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
        fwrite($conn, $httpResponse.$content);
        Sleep(10);
        fclose($conn);
    }
    fclose($socket);
}

优点

不需要额外的依赖,比如Web服务器,pcntl扩展等,可以搭建基于本地的http服务器用于调试web程序。

缺点

当单个进程处理耗时任务时,新的客户端再次发起请求,只能等待服务端进程处理完请求之后才能处理,所以新的客户端可能会等待超时,如果单个进程崩溃了,那么整个服务端都会面临崩溃。

2.多进程同步阻塞模型

这种模型,服务端在处理请求的时候,会创建子进程去处理客户端请求,短连接的演示流程如下图所示:

多进程同步阻塞短连接
<?php


$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);

if (!$socket) {
    echo "$errstr ($errno)".PHP_EOL;
} else {
    while ($conn = stream_socket_accept($socket)) {
        echo '客户端连接成功'.date('Y-m-d H:i:s', time()).PHP_EOL;
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('fork failed');
        } else if ($pid) {
            pcntl_wait($status);
        } else {
            $client = fread($conn, 1024);
            $content = "Hello Client,".date('Y-m-d: H:i:s', time());
            $httpResponse = "HTTP/1.1 200 OK\r\n";
            $httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
            $httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
            fwrite($conn, $httpResponse.$content);
            sleep(10);
            fclose($conn);
            exit();
        }
    }
    fclose($socket);
}

优势

每个子进程独立,互不干扰,子进程崩溃不影响其他进程。

缺点

创建进程需要申请内存空间,当请求过多的时候,会申请很多内存,内存开销比较大。 另外进程的创建与销毁开销很大,并不是立即就能完成的。

3.多进程监听同步阻塞模型

我们上面的多进程模型是接受客户端连接后创建的多进程,进程创建和销毁的开销大,不能及时的响应客户端请求,所以我们可以在监听处理上启用多进程模式

多进程监听同步阻塞短连接模型
<?php


$socket = stream_socket_server('tcp://0.0.0.0:7777', $errno, $errstr);

if (!$socket) {
    echo "$errstr ($errno)".PHP_EOL;
} else {
    for($i = 0; $i < 4; $i ++) {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('fork failed');
        } else if ($pid[$i]) {
            echo '父进程:'.$i.'id:'.$pid[$i].PHP_EOL;
        } else {
            while ($conn = stream_socket_accept($socket)) {
                echo '客户端连接成功'.date('Y-m-d H:i:s', time()).PHP_EOL;
                $pid = pcntl_fork();
                if ($pid == -1) {
                    die('fork failed');
                } else if ($pid) {
                   $childs[] = $pid;
                } else {
                    $client = fread($conn, 1024);
                    $content = "Hello Client,".date('Y-m-d: H:i:s', time());
                    $httpResponse = "HTTP/1.1 200 OK\r\n";
                    $httpResponse .= "Content-Type:text/html;charset=utf-8\r\n";
                    $httpResponse .= "Content-Length: ".strlen($content). "\r\n\r\n";
                    fwrite($conn, $httpResponse.$content);
                    sleep(10);
                    fclose($conn);
                    exit();
                }
            }
        }
    }
    while(count($childs) > 0) {
        foreach($childs as $k=>$pid) {
            $res = pcntl_waitpid($pid, $status, WNOHANG);
            if ($res == -1 || $res >0) {
                unset($childs[$k]);
            }
        }
    }

    fclose($socket);
}

优势

可以解决单进程监听,多进程处理客户端连接,由于进程创建与销毁开销大,而客户端无法及时响应的问题。

缺点

多进程的通病,内存开销大。

4.多线程同步阻塞模型

多线程的模型和多进程模型类似,就是把开启子进程换成开启线程,流程图参考多进程模型。

<?php

class Client extends Thread {
    public function __construct($socket){
        $this->socket = $socket;
        $this->start();
    }
    public function run(){
        $client = $this->socket;
        if ($client) {
            $header = 0;
            while(($chars = socket_read($client, 1024, PHP_NORMAL_READ))) {
                $head[$header]=trim($chars);
                if ($header>0) {
                    if (!$head[$header] && !$head[$header-1])
                        break;
                }
                $header++;
            }
            foreach($head as $header) {
                if ($header) {
                    $headers[]=$header;
                }
            }
            $response = array(
                "head" => array(
                    "HTTP/1.0 200 OK",
                    "Content-Type: text/html"
                ),
                "body" => array()
            );

            socket_getpeername($client, $address, $port);

            $response["body"][]="<html>";
            $response["body"][]="<head>";
            $response["body"][]="<title>Multithread Sockets PHP ({$address}:{$port})</title>";
            $response["body"][]="</head>";
            $response["body"][]="<body>";
            $response["body"][]="<pre>";
            foreach($headers as $header)
                $response["body"][]="{$header}";
            $response["body"][]="</pre>";
            $response["body"][]="</body>";
            $response["body"][]="</html>";
            $response["body"] = implode("\r\n", $response["body"]);
            $response["head"][] = sprintf("Content-Length: %d", strlen($response["body"]));
            $response["head"] = implode("\r\n", $response["head"]);

            socket_write($client, $response["head"]);
            socket_write($client, "\r\n\r\n");
            socket_write($client, $response["body"]);

            socket_close($client);
        }
    }
}

$server = socket_create_listen(8787);
while(($client = socket_accept($server))){
    $clients[]=new Client($client);
}

?>

优势

由于线程是共享主进程的虚拟地址空间,共享内存,所以子线程之间可以直接通信,节约内存开销,而多进程需要通过多进程的通信方式:如管道、消息队列、共享内存等手段。(PHP的多线程是基于TSRM,线程安全。并没有做到共享内存,而是把全局的资源拷贝到每一个线程中去操作,具体的介绍可以参考:http://www.laruence.com/2008/08/03/201.html

缺点

稳定性低,某个进程发生崩溃,则所有线程都会崩溃。

5.IO多路复用异步非阻塞模型

IO多路复用,指内核一旦发生进程指定的一个或多个IO条件准备读取,它就通知该进程,目前支持I/O多路复用的系统有select、poll、epoll等,多路复用的原理就是一个进程监听多个socket,一旦socket准备就绪,就通知程序进行相应的读写操作。

select

select是最早实现IO复用的,它监视并等待多个文件描述符的属性变化(可读、可写、或异常)。select函数监视文件描述符分3类,分别为writefds、readfds、exceptfds、调用后会select会阻塞,直到有描述符就绪(可读、可写或异常),或超时,函数才会返回。返回后,可以通过遍历fdset,来找到就绪的描述符,并且描述符最大不能超过1024。

poll

poll改进了select对于描述符不超过1024的问题

epoll

select和poll在返回后需要循环遍历fdset,这样对于有效数据命中率低,效率也低。epoll在linux2.6内核中提出的,是为了改进循环遍历的问题。epoll使用了事件驱动的机制,当一个描述符有变化的时候,就会告诉进程哪个连接有I/O事件流的产生,然后进程就去处理这个连接。

下面我们来基于事件驱动,实现一个简单的I/O多路复用异步非阻塞模型,首先我们看下多路复用的流程图:

<?php

$context = stream_context_create([
    'socket'=> [
        'backlog' => 10000,
    ]
]);

stream_context_set_option($context, 'socket', 'so_reuseaddr', 1);
$socket = stream_socket_server(
    'tcp://0.0.0.0:7777',
    $errno,
    $errstr,
  STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
        $context
);
stream_set_blocking($socket, false);
$base = new EventBase();
$event = new Event($base,
    $socket,
    Event::PERSIST | Event::READ | Event::WRITE,
    function($socket) use (&$base) {
        $client = stream_socket_accept($socket);
        echo '客户端连接:'.date('Y-m-d H:i:s', time()).PHP_EOL;
        $base = new EventBase();
        $event = new Event($base, $client, Event::PERSIST | Event::READ, function(){});
        $event->set($base,
            $client,
            Event::PERSIST | Event::READ,
            function($client, $what, $event) {
                $msg = fread($client, 65535);
                echo '客户端连接处理'.PHP_EOL;
                $content='Hello Client'.date('Y-m-d H:i:s', time());
                $string="HTTP/1.1 200 OK\r\n";
                $string.="Content-Type: text/html;charset=utf-8\r\n";
                $string.="Content-Length: ".strlen($content)."\r\n\r\n";
                fwrite($client, $string.$content);
                echo 'fwrite'.PHP_EOL;
                fclose($client);
                $event->del();

            }, $event);
        $event->add();
        $base->loop();
    });
$event->add();
$base->loop();

6.IO多路复用模型的性能对比

主机环境,虚拟及环境4G内存,单核CPU
使用phpstudy跑一个php的echo脚本文件
IO多路复用模型,执行php脚本的结果

7.(补充)多进程监听IO多路复用异步非阻塞模型

在IO复用的基础上结合多进程监听可以做一个更加高性能的模型

<?php

$context = stream_context_create([
    'socket' => [
        'backlog' => 10000,
    ]
]);

stream_context_set_option($context, 'socket', 'so_reuseport', 1);
stream_context_set_option($context, 'socket', 'so_reuseaddr', 1);

$socket = stream_socket_server(
    'tcp://0.0.0.0:7777',
    $errno,
    $errstr,
    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
    $context
);

for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        exit;
    } else if ($pid) {
        $childs[] = $pid;
    } else {
        stream_set_blocking($socket, false);
        $base = new EventBase();
        $event = new Event($base,
            $socket,
            Event::PERSIST | Event::READ | Event::WRITE,
            function ($socket) use (&$base) {
                $client = stream_socket_accept($socket);
                stream_set_blocking($client, false);
                echo '客户端连接:' . date('Y-m-d H:i:s', time()) . PHP_EOL;
                $base = new EventBase();
                $event = new Event($base, $client, Event::PERSIST | Event::READ, function ($client, $what, $argv) use (&$event) {
                    $msg = fread($client, 65535);
                    echo '客户端连接处理' . PHP_EOL;
                    $content = 'Hello Client' . date('Y-m-d H:i:s', time());
                    $string = "HTTP/1.1 200 OK\r\n";
                    $string .= "Content-Type: text/html;charset=utf-8\r\n";
                    $string .= "Content-Length: " . strlen($content) . "\r\n\r\n";
                    fwrite($client, $string . $content);
                    fclose($client);
                    $event->del();
                });
                $event->add();
                $base->loop();
            });
        $event->add();
        $base->loop();
    }
}

while(count($childs) > 0) {
    foreach($childs as $k=>$pid) {
        $res = pcntl_waitpid($pid, $status, WNOHANG);
        if ($res === -1 || $res > 0) {
            unset($childs[$k]);
        }
    }
}

性能有一定的提升
如无特殊说明,文章均为本站原创,转载请注明出处。如发现有什么不对的地方,希望得到您的指点。

发表评论

电子邮件地址不会被公开。 必填项已用*标注