PHP 200行代码实现一个简单的master(守护进程)-worker(工作进程)的高性能异步非阻塞Web服务器

1.实现前的知识储备

2.什么是master-worker结构?

就是主进程用于管理维护worker进程,而worker进程则用于处理业务请求,下面我就盗用一张workerman的原理图展示下这种结构。

图片来源: http://doc.workerman.net/principle.html

3.代码实现

如果您阅读完了知识前的储备章节,那么下面我们就需要把这这些章节的内容都组合起来,实现一个类似workerman结构的web服务器雏形。

我们把实现的步骤分为4步:1.初始化守护进程,2.初始化工作进程,3.初始化master进程信号监听处理,4.master进程等待回收子进程。

<?php

class Server
{
    private $master_id;

    protected $so_server;

    protected $so_client;

    protected $eventBase;

    protected $address;

    protected $workers_pid = [];

    protected $worker_num;

    protected $default_options = ['address' => 'tcp://0.0.0.0:7777', 'work_nums' => 2];

    public function __construct(array $options=[])
    {
        $options = array_merge($this->default_options, $options);

        $this->address = $options['address'];

        $this->worker_num = $options['work_nums'];

        $this->eventBase = new eventBase();

    }

    public function run()
    {

        $this->initDaemon();

        $this->initWorkers();

        $this->initMonitor();

        $this->waitForWorkers();
    }


    public function initWorkers()
    {

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

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

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

        $this->so_server = stream_socket_server($this->address, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);

        while (count($this->workers_pid) < $this->worker_num) {
            $pid = pcntl_fork();

            if ($pid == -1) {
                exit;
            } else if ($pid) {
                $this->master_id = posix_getpid();

                $this->workers_pid[] = $pid;
            } else {
                cli_set_process_title('Worker:' . posix_getpid());

                stream_set_blocking($this->so_server, false);

                $event_usr1 = Event::signal($this->eventBase, SIGUSR1 , [$this, 'worker_handler']);

                $event_usr1->addSignal();

                $event_term = Event::signal($this->eventBase, SIGTERM , [$this, 'worker_handler']);

                $event_term->addSignal();

                $event = new Event($this->eventBase, $this->so_server, Event::READ | Event::PERSIST, function($server) {
                    $this->so_client = stream_socket_accept($server);

                    stream_set_blocking($this->so_client, false);

                    $event = new Event($this->eventBase, $this->so_client, Event::READ | Event::PERSIST, function($client) use(&$event) {
                        $msg = fread($client, 1024);

                        $content = "Hello Client";

                        $responseHeader = "HTTP/1.1 200 OK \r\n";

                        $responseHeader .= "Connection: Keep-Alive\r\n";

                        $responseHeader .= "Content-Type: text/html;charset=utf-8\r\n";

                        $responseHeader .= "Content-Length: " . strlen($content) . "\r\n\r\n";

                        fwrite($client, $responseHeader . $content);

                        fclose($client);

                        $event->del();
                    }, $this->so_client);

                    $event->add();
                });

                $event->add();

                $this->eventBase->loop();
            }
        }
    }

    public function initDaemon()
    {
        $pid = pcntl_fork();

        if ($pid == -1) {
            exit;
        } else if ($pid) {
            exit;
        } else {
            if (posix_setsid() < 0) {
                exit;
            }

            umask(0);

            chdir('/');

            cli_set_process_title('Master:Daemon');
        }
    }

    public function installSignal()
    {
        pcntl_signal(SIGUSR1, [$this, 'signal_handler']);

        pcntl_signal(SIGTERM, [$this, 'signal_handler']);
    }


    public function signal_handler($signal)
    {
        switch($signal) {
            case SIGUSR1:
                echo '主进程收到SINGUSR1信号 '. posix_getpid() . PHP_EOL;
                $this->notifyWorkers(SIGUSR1);
                break;
            case SIGTERM:
                echo '主进程收到SIGTERM信号'. posix_getpid() . PHP_EOL;
                $this->notifyWorkers(SIGTERM);
            default:
                break;
        }
    }

    public function worker_handler($signal)
    {
        echo '子进程收到信号:' . $signal . PHP_EOL;
    }

    public function notifyWorkers($signal=SIGUSR1)
    {
        foreach($this->workers_pid as $pid) {
            posix_kill($pid, $signal);
        }
    }

    public function initMonitor()
    {
        $this->installSignal();

        while(1) {
            pcntl_signal_dispatch();
        }
    }

    public function waitForWorkers()
    {
        while(count($this->workers_pid) > 0) {
            foreach($this->workers_pid as $k => $pid) {
                $pid = pcntl_waitpid($pid, $status, WNOHANG);
                if ($pid == -1 || 0 < $pid) {
                    unset($this->workers_pid[$k]);
                }
            }
        }
    }
}

$server = new Server();

$server->run();
执行效果,我们以看到守护进程的父进程id为1,当我们发送USR1信号的时候,守护进程会通知所有的子进程USR1信号,子进程此时可以按需求进行相关处理。

4.与workerman和swoole的性能对比

这是我们自己写的,默认2个工作进程,采用Event事件机制
这是workerman内置的webserver,默认2个工作进程,采用select机制,Work的版本号为3.5.20
这是swoole内置的httpserver,swoole版本为4.4.4
如无特殊说明,文章均为本站原创,转载请注明出处。如发现有什么不对的地方,希望得到您的指点。

发表评论

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