其实IO复用的历史和多进程一样长,Linux很早就提供了 select 系统调用,可以在一个进程内维持1024个连接。后来又加入了poll系统调用,poll做了一些改进,解决了 1024 限制的问题,可以维持任意数量的连接。但select/poll还有一个问题就是,它需要循环检测连接是否有事件。这样问题就来了,如果服务器有100万个连接,在某一时间只有一个连接向服务器发送了数据,select/poll需要做循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。

直到Linux 2.6内核提供了新的epoll系统调用,可以维持无限数量的连接,而且无需轮询,这才真正解决了 C10K 问题。现在各种高并发异步IO的服务器程序都是基于epoll实现的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 这样单进程单线程的程序,都可以维持超过1百万TCP连接,全部归功于epoll技术。

IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化。
Reactor模型

在这里我们不去探讨具体的Reactor模型,只尝试用IO多路复用来实现一个简单的tcp服务器,作为学习之用。

Server 代码实现

<?php
/**
 * Created by PhpStorm.
 * User: skymei
 * Date: 2018/12/20
 * Time: 16:07
 */

namespace App\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;

class httpMutilIOServer extends Command
{
    public function configure()
    {
        $this
            // the name of the command (the part after "bin/console")
            ->setName('MutilIOserver:start')
            // the short description shown while running "php bin/console list"
            ->setDescription('start a simple http server.')
            // the full command description shown when running the command with
            // the "--help" option
            ->setHelp('start a simple http server');
    }

    public function execute(InputInterface $input, OutputInterface $output)
    {
        set_time_limit(0);
        // create socket server
        $socketServer = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($socketServer === false) {
            $output->writeln('create socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        //!!!!! important  the timeout setting
        socket_set_option($socketServer, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 1, "usec" => 0));
        socket_set_option($socketServer, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 3, "usec" => 0));

        // bind ip and port for server
        if (!socket_bind($socketServer, '127.0.0.1', 8081)) {
            $output->writeln('bind socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // how many clients the server can listen
        if (!socket_listen($socketServer, 32)) {
            $output->writeln('listen socket client fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // continuously to handle the client's request
        $readSocket = [];  // the connection source which can be read
        $writeSocket = []; // the connection source which can be wrote
        $expectSocket = NULL;  // the connection source which will not be processed

        $readSocket[] = $socketServer;
        while (true) {
            $tmpReads = $readSocket;
            $tmpWrites = $writeSocket;

            if(false === socket_select($tmpReads, $tmpWrites, $expectSocket, Null)){//Asynchronous detection
                $output->writeln('select socket client fail : ' . socket_strerror(socket_last_error()));
                die();
            }

            foreach ($tmpReads as $read) {  // handle the source which can read
                if ($read == $socketServer) { // main server connection
                    $clientConnect = socket_accept($socketServer);

                    if ($clientConnect) {
                        socket_getpeername($clientConnect, $addr, $port);
                        $output->writeln("client $addr connect with port $port..");
                        $readSocket[] = $clientConnect;
                        $writeSocket[] = $clientConnect;
                    }
                } else {
                    $data = socket_read($read, 1024);
                    socket_getpeername($read, $addr, $port);

                    if(empty($data)){
                        unset($readSocket[array_search($read, $readSocket)]);
                        unset($writeSocket[array_search($read, $writeSocket)]);

                        socket_close($read);
                        $output->writeln("client $addr:$port disconnect !");
                    }else{
                        $output->writeln("receive data from client $addr:$port : " . $data);
                        $data = strtoupper($data);

                        if(in_array($read, $tmpWrites)){
                            socket_write($read , $data);
                        }
                    }
                }
            }
        }
        socket_close($socketServer);  // stop socket server
    }
}

效果实践

通过command bin/console client:start 开启多个client来接入,结果输出如下:

>php bin/console MutilIOserver:start
client 127.0.0.1 connect with port 58015..
receive data from client 127.0.0.1:58015 : Hello World!
client 127.0.0.1 connect with port 58016..
receive data from client 127.0.0.1:58016 : Hello World!
client 127.0.0.1 connect with port 58018..
receive data from client 127.0.0.1:58018 : Hello World!
client 127.0.0.1:58015 disconnect !
client 127.0.0.1:58016 disconnect !
client 127.0.0.1:58018 disconnect !
client 127.0.0.1 connect with port 58019..
receive data from client 127.0.0.1:58019 : Hello World!
client 127.0.0.1 connect with port 58020..
receive data from client 127.0.0.1:58020 : Hello World!
client 127.0.0.1 connect with port 58021..
receive data from client 127.0.0.1:58021 : Hello World!
client 127.0.0.1:58019 disconnect !
client 127.0.0.1:58020 disconnect !
client 127.0.0.1:58021 disconnect !

同样达到了并发的效果!但是只需要开启一个进程即可,大大的节约了系统资源的消耗。对比多进程的方式,优点显而易见。
对于socket编程,能做的不仅仅如此,学无止境,也有很多优秀的php 框架能解决此类问题,如workman、swoole等,在之后的时间里我会逐步的去了解学习。