使用PHP开发一个HTTP服务器

众所周知,我们一般使用PHP开发Web程序时需要使用到比如Apache或Nginx等Web服务器来支持,那么有没有办法直接使用PHP开发HTTP服务器,答案当然是可以的,最近看了一遍Workerman框架的源码,于是自己仿照写了一个简易的HTTP服务器,学习为主,感兴趣的朋友可以看看。

前言

本次应用主要是围绕Socket编程、HTTP协议、事件、进程控制和信号等知识点来实现的,大体思路是主进程创建TCP服务器,创建子进程监听事件来接受请求和处理业务并实现HTTP协议,同时主进程注册信号和监听信号并处理信号事件,监视子进程状态并控制子进程退出,重启等动作,下面我将过一遍基本的知识点。

TCP服务器

HTTP协议是建立在TCP协议之上的一种应用,因此实现HTTP服务区必须先实现TCP服务器,PHP可以使用Sockets相关函数来提供一个TCP服务器,除此之外,推荐使用更加方便的Streams相关函数实现TCP服务器,以下是两种方式的实现:

Sockets相关函数:

<?php

$address = '127.0.0.1'; // 设置地址
$port = 1234; // 设置端口
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // AF_INET=ipv4,AF_INET6=ipv6,SOCK_STREAM=tcp,SOCK_DGRAM=udp
socket_set_block($sock); // 阻塞模式
socket_bind($sock, $address, $port); // 绑定到socket端口
socket_listen($sock, 4); // 开始监听

while(1) {
    $newSocket = socket_accept($sock); //接受请求
    if ($newSocket) {
        while (($buffer = socket_read($newSocket,1024)) !== false ) {
            echo "get client data:{$buffer}\n";
            $msg = "server say hello world\n";
            socket_write($newSocket, $msg);
        }
        socket_close($newSocket);
    } else {
        echo "no newSocket\n";
    }
}

Streams相关函数:

<?php

$errno = 0;
$errmsg = '';
$socket = stream_socket_server('tcp://0.0.0.0:1234', $errno, $errmsg); //创建tcp服务器
stream_set_blocking($socket, 0); // 设置非堵塞

while(1) {
    $newSocket = stream_socket_accept($socket); // 接受请求
    if ($newSocket) {
        while ($buffer = fread($newSocket, 1024) !== false) { //获取数据
            echo "get client data:{$buffer}\n";
            $msg = "server say hello world\n";
            fwrite($newSocket, $msg);
        }
        fclose($newSocket);
    } else {
        echo "no newSocket\n";
    }
}

Sockets相关函数提供的是更加低级别的接口,在我们这次应用中,使用Streams相关函数显然更加简洁和通用。

HTTP服务器

HTTP协议是建立在TCP协议之上的一种应用,要实现HTTP服务器,只需要在TCP服务器上实现HTTP协议即可,以下为仅支持GET请求的HTTP协议解码和编码:

HTTP解码:

<?php

/**
 * http解码(GET请求)
 * @param $content
 * @return array
 */
public function httpDecode($content)
{
    // 全局变量
    $_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = array();

    $_SERVER = array(
        'QUERY_STRING'         => '',
        'REQUEST_METHOD'       => '',
        'REQUEST_URI'          => '',
        'SERVER_PROTOCOL'      => '',
        'SERVER_NAME'          => '',
        'HTTP_HOST'            => '',
        'HTTP_USER_AGENT'      => '',
        'HTTP_ACCEPT'          => '',
        'HTTP_ACCEPT_LANGUAGE' => '',
        'HTTP_ACCEPT_ENCODING' => '',
        'HTTP_COOKIE'          => '',
        'HTTP_CONNECTION'      => '',
        'REMOTE_ADDR'          => '',
        'REMOTE_PORT'          => '0',
        'REQUEST_TIME'         => time()
    );

    // 解析头部
    list($http_header, $http_body) = explode("\r\n\r\n", $content, 2);
    $header_data = explode("\r\n", $http_header);

    list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = explode(' ',
        $header_data[0]);
    unset($header_data[0]);
    foreach ($header_data as $content) {
        // \r\n\r\n
        if (empty($content)) {
            continue;
        }
        list($key, $value)       = explode(':', $content, 2);
        $key                     = str_replace('-', '_', strtoupper($key));
        $value                   = trim($value);
        $_SERVER['HTTP_' . $key] = $value;
    }

    // 查询字符串
    $_SERVER['QUERY_STRING'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
    if ($_SERVER['QUERY_STRING']) {
        // $GET
        parse_str($_SERVER['QUERY_STRING'], $_GET);
    } else {
        $_SERVER['QUERY_STRING'] = '';
    }

    // REQUEST
    $_REQUEST = array_merge($_GET, $_POST);

    return array('get' => $_GET, 'post' => $_POST, 'cookie' => $_COOKIE, 'server' => $_SERVER, 'files' => $_FILES);
}

HTTP编码:

<?php

/**
 * http编码(仅GET请求)
 * @param $content
 * @return string
 */
public function httpEncode($content)
{
    $header = "HTTP/1.1 200 OK\r\n";
    $header .= "Content-Type: text/html;charset=utf-8\r\n";
    $header .= "Connection: keep-alive\r\n";
    $header .= "Server: workerman/3.5.4\r\n";
    $header .= "Content-Length: " . strlen($content) . "\r\n\r\n";
    return $header . $content;
}

将TCP服务器接收的数据按照HTTP协议进行解码,将解码后的数据注册进PHP全局变量,PHP从全局变量获取请求数据,业务处理后将输出内容按照HTTP协议编码返回,即实现了一个HTTP服务器,完整HTTP协议编解码可以参考Workman源码。

事件

以上HTTP服务器的缺点是单进程堵塞,同一时间只能处理一个请求,而通过事件处理,我们可以同一时间接受多个请求,在请求可读的时候来处理业务,做到IO复用,PHP使用Libevent可以使用系统Epoll、Kqueue等高级事件处理机制,相关函数和应用介绍如下:

相关函数:

  • 创建和初始化新的事件库:
resource event_base_new ( void )
  • 创建一个新事件:
resource event_new ( void )
  • 准备一个事件:
bool event_set ( resource $event , mixed $fd , int $events , mixed $callback [, mixed $arg ] )

fd:信号或流资源。

events:事件。以下标志:ev_timeout,ev_signal,ev_read,ev_write和ev_persist。

callback:处理函数。

arg:处理函数参数

  • 将事件库与事件关联:
bool event_base_set ( resource $event , resource $event_base )
  • 将事件添加到监视事件集:
bool event_add ( resource $event [, int $timeout = -1 ] )
  • 启动事件循环:
int event_base_loop ( resource $event_base [, int $flags = 0 ] )

官方监听标准输入事件例子:

<?php

function print_line($fd, $events, $arg)
{
    static $max_requests = 0;

    $max_requests++;

    if ($max_requests == 10) {
        // exit loop after 10 writes
        event_base_loopexit($arg[1]);
    }

    // print the line
    echo  fgets($fd);
}

// create base and event
$base = event_base_new();
$event = event_new();

$fd = STDIN;

// set event flags
event_set($event, $fd, EV_READ | EV_PERSIST, "print_line", array($event, $base));
// set event base
event_base_set($event, $base);

// enable event
event_add($event);
// start event loop
event_base_loop($base);

基于事件的TCP服务器:

<?php

// 创建tcp服务器
$errno = 0;
$errmsg = '';
$socket = stream_socket_server('tcp://0.0.0.0:1234', $errno, $errmsg);
stream_set_blocking($socket, 0);

// 创建事件并监听socket文件描述符,监听事件EV_READ:可读,EV_PERSIST:持续化(断开连接事件被关闭可被监听)
$base = event_base_new();
$event = event_new();
event_set($event, $socket, EV_READ | EV_PERSIST, "acceptConnect", array($event, $base));
event_base_set($event, $base);
event_add($event);
event_base_loop($base); // 开始event轮询,当socket文件描述符可读时会触发acceptConnect函数处理

function acceptConnect($socket, $events, $arg)
{
    $newSocket = @stream_socket_accept($socket, 0, $remote_address); // 第二个参数设置0,不堵塞,未获取到会警告
    if (!$newSocket) {
        return;
    }
    stream_set_blocking($newSocket, 0);
    echo "接受一个新连接\n";

    // 监听newSocket文件描述符,监听事件EV_READ:可读,EV_PERSIST:持续化(断开连接事件被关闭可被监听)
    $event = event_new();
    event_set($event, $newSocket, EV_READ | EV_PERSIST, "acceptData", array($event, $arg[1]));
    event_base_set($event, $arg[1]);
    event_add($event);
}

function acceptData($newSocket, $events, $arg)
{
    $buffer = @fread($newSocket,1024); //获取数据
    if ($buffer === '' || $buffer === false) {
        if (feof($newSocket) || !is_resource($newSocket) || $buffer === false) {
            echo "连接退出\n";
            event_del($arg[0]); //关闭连接事件
            @fclose($newSocket); // 关闭连接
            return;
        }
    }
    echo "get client data:{$buffer}\n";
    $msg = "server say hello world\n";
    fwrite($newSocket, $msg);
}

基于事件的TCP服务器加上HTTP协议的解编码就实现了一个基于事件的HTTP服务器。

进程控制和信号

基于事件的HTTP服务器能同时处理多个请求,但仍然是单进程模式,无法发挥较大的性能,而通过进程控制和信号机制,我们可以自定义化我们的HTTP服务器,PHP可以通过PCNTL函数和POSIX 函数来实现进程控制和使用信号进行通信,如下分别介绍一下本应用创建新进程和实现信号机制涉及到的函数和使用:

创建子进程和监听子进程退出状态相关函数:

  • 返回当前进程 id:
int posix_getpid ( void )

返回进程 id 号,是整型(integer)。

  • 创建一个子进程:
int pcntl_fork ( void )

父进程和子进程都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0。

  • 等待或返回fork的子进程状态:
int pcntl_wait ( int &$status [, int $options = 0 ] )

wait函数刮起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。

使用以上三个函数实现创建子进程并监听子进程退出状态:

<?php

$pid = pcntl_fork();
if ($pid == -1) {
    die('子进程创建失败');
} else if ($pid == 0) {
    // 以下代码在子进程中运行
    echo "这是子进程,pid:" . posix_getpid() . "\n";
    sleep(5);
    exit(0);
} else {
    // 以下代码在父进程中运行
    echo "这是父进程,pid:" . posix_getpid() . "\n";
    $status = 0;
    $pid = pcntl_wait($status, WUNTRACED); // 堵塞直至获取子进程退出或中断信号或调用一个信号处理器,或者没有子进程时返回错误
    echo "子进程退出,pid:" . $pid . "\n";
}

接下来介绍信号处理相关函数:

  • 安装一个信号处理器:
bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )

signo:信号编号。例如按下CTRL+C的时候会发送SIGINT信号,使用命令行kill -l可以查看全部信号。

handler信号处理器,用户函数或方法,也可以是系统常量 SIG_IGN(译注:忽略信号处理程序)或SIG_DFL(默认信号处理程序)。

restart_syscalls:指定当信号到达时系统调用重启是否可用。

  • 调用等待信号的处理器:
bool pcntl_signal_dispatch ( void )

函数pcntl_signal_dispatch()调用每个等待信号通过pcntl_signal() 安装的处理器。

  • 发送一个信号给指定进程:
bool posix_kill ( int $pid , int $sig )

pid:进程号

sig:信号,命令行kill -l查看全部信号

使用已上函数实现注册,分发和接收信号:

<?php

function signalHandler($sigNo)
{
    switch ($sigNo) {
        // Stop.
        case SIGINT:
            echo "进程退出\n";
            exit(0);
        // user1
        case SIGUSR1:
            echo "处理用户自定义信号\n";
            break;
        default:
            // 处理所有其他信号
    }
}
$pid = posix_getgid();
echo "pid:{$pid}\n";

// 注册SIGINT信号,ctrl+c,或命令行kill SIGINT $pid,或使用函数posix_kill触发
pcntl_signal(SIGINT, 'SignalHandler', false);
// 注册SIGUSR1信号,
pcntl_signal(SIGUSR1, 'SignalHandler', false);

posix_kill($pid, SIGUSR1);

while(1) {
    sleep(1);
    pcntl_signal_dispatch(); // 信号分发
}

通过进程控制和信号机制,我们可以创建子进程来处理HTTP请求,主进程注册和处理信号并监视和控制子进程。

PHP开发的HTTP服务器

将上述知识点整合在一起,实现最终的HTTP服务器:

PHP开发的HTTP服务器:

<?php

// 解析命令
$command  = isset($argv[1]) ? trim($argv[1]) : '';
$available_commands = array(
    'start',
    'stop',
    'reload',
    'status',
);
$usage = "Usage: php index.php {" . implode('|', $available_commands) . "}\n";
if (empty($command) || !in_array($command, $available_commands)) {
    exit($usage);
}

$worker = new Worker();

switch ($command) {
    case 'start':
        $worker->start();
        break;
    case 'stop':
    case 'reload':
    case 'status':
        $worker->sendSignalToMaster($command);
        break;
}


class Worker
{

    public $count = 4; //子进程数
    public $localSocket = 'tcp://0.0.0.0:2345'; // 监听地址
    public $onMessage = null; // 处理函数
    
    private $masterPidFile = 'masterPidFile.pid'; // 主进程pid
    private $masterStatusFile = 'masterStatusFile.status'; // 主进程状态文件
    private $forkArr = array(); // 子进程pid数组
    private $socket = null; // 监听socket
    private $newSocket = null; // 连接socket
    private $masterStop = 0; // 主进程是否停止
    private $connectionCount = 0; //每个子进程到连接数
    private $requestNum = 0; //每个子进程总请求数

    public function __construct()
    {
        if (!$this->onMessage) { // 默认处理
            $this->onMessage = function($connection)
            {
                //var_dump($_GET, $_POST, $_COOKIE, $_SESSION, $_SERVER, $_FILES);
                // 发送数据给客户端
                $connection->sendData("hello world \n");
            };
        }
    }

    /**
     * 主进程启动
     */
    public function start()
    {
        // 判断当前程序是否已经启动
        $masterPidFileExist = is_file($this->masterPidFile);
        if ($masterPidFileExist) {
            exit("当前程序已经在运行,请不要重启启动\n");
        }

        // 保存主进程pid到文件用于stop,reload,status等命令操作
        $masterPid = posix_getpid();
        file_put_contents($this->masterPidFile, $masterPid);

        // 注册主进程信号,pcntl_signal第三个参数设置成false,才会有信号时被pcntl_wait调用
        pcntl_signal(SIGINT, array($this, 'masterSignalHandler'), false); // 退出,用于stop命令或主进程窗口按下ctrl+c
        pcntl_signal(SIGUSR1, array($this, 'masterSignalHandler'), false); // 自定义信号1,用于reload命令
        pcntl_signal(SIGUSR2, array($this, 'masterSignalHandler'), false); // 自定义信号2,用户status命令

        // 主进程创建tcp服务器
        $errno = 0;
        $errmsg = '';
        $socket = stream_socket_server($this->localSocket, $errno, $errmsg);

        // 尝试打开KeepAlive TCP和禁用Nagle算法。
        if (function_exists('socket_import_stream')) {
            $socketImport = socket_import_stream($socket);
            @socket_set_option($socketImport, SOL_SOCKET, SO_KEEPALIVE, 1);
            @socket_set_option($socketImport, SOL_TCP, TCP_NODELAY, 1);
        }

        // Non blocking.
        stream_set_blocking($socket, 0);
        $this->socket = $socket;

        // 创建count个子进程,用于接受请求和处理数据
        while(count($this->forkArr) < $this->count) {
            $this->fork();
        }

        // 主进程接受信号和监听子进程信号
        while(1) {

            //sleep(1);
            pcntl_signal_dispatch(); // 信号分发
            $status = 0;
            $pid = pcntl_wait($status, WUNTRACED); // 堵塞直至获取子进程退出或中断信号或调用一个信号处理器,或者没有子进程时返回错误
            pcntl_signal_dispatch();
            if ($pid > 0) {
                // 子进程退出
                echo "子进程退出pid:{$pid}\n";
                unset($this->forkArr[$pid]);
                // 关闭还是重启
                if (!$this->masterStop) {
                    // 重启
                    $this->fork();
                }
            } else {
                // 主进程退出状态并且没有子进程时退出
                if ($this->masterStop && empty($this->forkArr)) {
                    unlink($this->masterPidFile);
                    fclose($this->socket);
                    echo "主进程退出\n";
                    exit(0);
                }
            }
        }
    }

    /**
     * 主进程处理信号
     * @param $sigNo
     */
    public function masterSignalHandler($sigNo)
    {
        switch ($sigNo) {
            // Stop.
            case SIGINT:
                // 退出,先发送子进程信号关闭子进程,再等待主进程退出
                foreach ($this->forkArr as $pid) {
                    echo "关闭子进程pid:{$pid}\n" ;
                    posix_kill($pid, SIGKILL);
                }
                $this->masterStop = 1; // 将主进程状态置成退出
                break;
            // user1
            case SIGUSR1:
                // 重启,关闭当前存在但子进程,主进程会监视退出的子进程并重启一个新子进程
                foreach ($this->forkArr as $pid) {
                    echo "关闭子进程pid:{$pid}\n" ;
                    posix_kill($pid, SIGKILL);
                }
                break;
            // user2
            case SIGUSR2:
                echo "将状态信息保存至文件:{$this->masterStatusFile}\n";
                // 将状态信息保存至文件
                $str = "---------------------STATUS---------------------\n";
                $str .= 'PHP version:' . PHP_VERSION . "\n";
                $str .= 'processes num:' . $this->count . "\n";
                $str .= "---------------------PROCESS STATUS---------------------\n";
                $str .= "pid\n";

                foreach ($this->forkArr as $childPid) {
                    $str .= $childPid."\n";
                }
                file_put_contents($this->masterStatusFile, $str);
                break;
            default:
                // 处理所有其他信号
        }
    }

    /**
     * 创建子进程
     */
    public function fork()
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('子进程创建失败');
        } else if ($pid == 0) {
            // 以下代码在子进程中运行

            // 创建event事件
            $base = event_base_new();
            $event = event_new();

            // 设置event监听事件,监听socket文件描述符,监听事件EV_READ:可读,EV_PERSIST:持续化(断开连接可被监听到)
            event_set($event, $this->socket, EV_READ | EV_PERSIST, array($this, "acceptConnect"), array($event, $base));
            // 设置event
            event_base_set($event, $base);
            event_add($event);
            // 开始event轮询,当socket文件描述符有可读或断开会触发acceptConnect函数处理
            event_base_loop($base);
        } else {
            // 主进程将子进程pid保存到数组
            echo "创建子进程pid:{$pid}\n";
            $this->forkArr[$pid] = $pid;
        }
    }

    /**
     * 子进程接受请求
     * @param $socket
     * @param $events
     * @param $arg
     */
    public function acceptConnect($socket, $events, $arg)
    {
        $newSocket = @stream_socket_accept($socket, 0, $remote_address); // 第二个参数设置0,不堵塞,未获取到会警告
        //有一个连接过来时,子进程都会触发本函数,但只有一个子进程获取到连接并处理
        if (!$newSocket) {
            return;
        }

        echo "acceptConnect\n";
        $this->connectionCount++;

        stream_set_blocking($newSocket, 0);
        // 兼容hhvm
        if (function_exists('stream_set_read_buffer')) {
            stream_set_read_buffer($newSocket, 0);
        }

        // 子进程添加一个事件在newSocket文件描述符上
        $event = event_new();
        // 设置event监听事件,监听newSocket文件描述符,事件为EV_READ:可读,EV_PERSIST:持续化(断开连接可被监听到)
        event_set($event, $newSocket, EV_READ | EV_PERSIST, array($this, "acceptData"), array($event, $arg[1]));
        event_base_set($event, $arg[1]);
        event_add($event);
    }

    /**
     * 子进程处理数据
     * @param $newSocket
     * @param $events
     * @param $arg
     */
    public function acceptData($newSocket, $events, $arg)
    {
        $this->newSocket = $newSocket;
        // http服务器(HTTP1.1默认使用keep-alive保持连接)
        $buffer = @fread($newSocket,65535); //获取数据
        //echo "获取客户端数据:{$buffer}\n";
        if ($buffer === '' || $buffer === false) {
            if (feof($newSocket) || !is_resource($newSocket) || $buffer === false) {
                echo "客户端关闭\n";
                event_del($arg[0]); //关闭连接事件
                @fclose($this->newSocket); // 关闭连接
                $this->connectionCount--;
                return;
            }
        }
        $this->requestNum++;
        $this->httpDecode($buffer); // http解码
        call_user_func($this->onMessage, $this); // 调用处理函数

        /*// tcp服务器
        $buffer = fread($newSocket,1024);
        if ($buffer === '' || $buffer === false) {
            if (feof($newSocket) || $buffer === false) {
                echo "客户端关闭连接\n";
                event_del($arg[0]);
                fclose($newSocket);
            }
        } else {
            echo "获取客户端数据:" . $buffer;
            $msg = "hello client\n";
            fwrite($newSocket, $msg);
        }*/
    }

    /**
     * http服务器返回数据
     * @param $sendBuffer
     * @return bool
     */
    public function sendData($sendBuffer) {
        $msg = $this->httpEncode($sendBuffer); // http编码
        fwrite($this->newSocket, $msg, 8192);
        return true;
    }

    /**
     * http解码(仅GET请求)
     * @param $content
     * @return array
     */
    public function httpDecode($content)
    {
        // 全局变量
        $_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = array();

        $_SERVER = array(
            'QUERY_STRING'         => '',
            'REQUEST_METHOD'       => '',
            'REQUEST_URI'          => '',
            'SERVER_PROTOCOL'      => '',
            'SERVER_NAME'          => '',
            'HTTP_HOST'            => '',
            'HTTP_USER_AGENT'      => '',
            'HTTP_ACCEPT'          => '',
            'HTTP_ACCEPT_LANGUAGE' => '',
            'HTTP_ACCEPT_ENCODING' => '',
            'HTTP_COOKIE'          => '',
            'HTTP_CONNECTION'      => '',
            'REMOTE_ADDR'          => '',
            'REMOTE_PORT'          => '0',
            'REQUEST_TIME'         => time()
        );

        // 解析头部
        list($http_header, $http_body) = explode("\r\n\r\n", $content, 2);
        $header_data = explode("\r\n", $http_header);

        list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = explode(' ',
            $header_data[0]);
        unset($header_data[0]);
        foreach ($header_data as $content) {
            // \r\n\r\n
            if (empty($content)) {
                continue;
            }
            list($key, $value)       = explode(':', $content, 2);
            $key                     = str_replace('-', '_', strtoupper($key));
            $value                   = trim($value);
            $_SERVER['HTTP_' . $key] = $value;
        }

        // 查询字符串
        $_SERVER['QUERY_STRING'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
        if ($_SERVER['QUERY_STRING']) {
            // $GET
            parse_str($_SERVER['QUERY_STRING'], $_GET);
        } else {
            $_SERVER['QUERY_STRING'] = '';
        }

        // REQUEST
        $_REQUEST = array_merge($_GET, $_POST);

        return array('get' => $_GET, 'post' => $_POST, 'cookie' => $_COOKIE, 'server' => $_SERVER, 'files' => $_FILES);
    }

    /**
     * http编码(仅GET请求)
     * @param $content
     * @return string
     */
    public function httpEncode($content)
    {
        $header = "HTTP/1.1 200 OK\r\n";
        $header .= "Content-Type: text/html;charset=utf-8\r\n";
        $header .= "Connection: keep-alive\r\n";
        $header .= "Server: workerman/3.5.4\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n\r\n";
        return $header . $content;
    }

    /**
     * 发送命令给主进程
     * @param $command
     */
    public function sendSignalToMaster($command)
    {
        $masterPid = file_get_contents($this->masterPidFile);
        if ($masterPid) {
            switch ($command) {
                case 'stop':
                    posix_kill($masterPid, SIGINT);
                    break;
                case 'reload':
                    posix_kill($masterPid, SIGUSR1);
                    break;
                case 'status':
                    posix_kill($masterPid, SIGUSR2);
                    sleep(1); // 等待主进程将状态信息放入文件
                    $masterStatus = file_get_contents($this->masterStatusFile);
                    echo $masterStatus;
                    unlink($this->masterStatusFile);
                    break;
            }
            exit;
        } else {
            echo "主进程不存在\n";
            exit;
        }
    }
}

以上实现了一个简易的HTTP服务器,主进程处理信号和监视子进程,子进程负责接受请求和处理业务。

测试

使用上述PHP开发的HTTP服务器与Nginx+Php-fpm作为HTTP服务器进行对比,两者都输出hello world,使用apache ab工具进行测试结果如下:

1.Nginx+Php-fpm(4个Nginx worker进程和4个Php-fpm worker进程):

命令(并发100,总共发起1000次请求):

ab -n1000 -c100 -k http://127.0.0.1:2345/

结果:

Server Software:        nginx/1.8.1
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /index.php
Document Length:        21 bytes

Concurrency Level:      100
Time taken for tests:   0.363 seconds
Complete requests:      1000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      183000 bytes
HTML transferred:       21000 bytes
Requests per second:    2758.03 [#/sec] (mean)
Time per request:       36.258 [ms] (mean)
Time per request:       0.363 [ms] (mean, across all concurrent requests)
Transfer rate:          492.89 [Kbytes/sec] received

2.PHP开发的HTTP服务器(4个worker进程):

命令(并发100,总共发起1000次请求):

ab -n1000 -c100 -k http://127.0.0.1:2345/

结果:

Server Software:        workerman/3.5.4
Server Hostname:        127.0.0.1
Server Port:            2345

Document Path:          /
Document Length:        13 bytes

Concurrency Level:      100
Time taken for tests:   0.087 seconds
Complete requests:      1000
Failed requests:        0
Keep-Alive requests:    1000
Total transferred:      140000 bytes
HTML transferred:       13000 bytes
Requests per second:    11506.82 [#/sec] (mean)
Time per request:       8.691 [ms] (mean)
Time per request:       0.087 [ms] (mean, across all concurrent requests)
Transfer rate:          1573.20 [Kbytes/sec] received

可用看出,因为PHP开发的HTTP服务器少了中间转发等操作,速度更快。

上述demo可以戳这里:demo

总结

PHP在上述例子中使用到的系统函数底层基本上都是Linux下相关的一些技术,对C实现TCP服务器感兴趣的可以看我另一篇文章:Linux下Socket开发简易Tcp服务器,以上代码仅作为学习使用,想深入了解相关知识建议去看一遍Workman的源码。

感想

PHP一直被视为草根语言,无不在乎大家都认为PHP能做的事情太少,但像Workerman和Swoole的出现,使得PHP可以实现更多的功能,也让大家对PHP有了新的认识,希望大家对PHP少一点偏见,多一点支持,也希望我们PHPer能越来越强,开开心心地写代码。

    原文作者:HTTP
    原文地址: https://juejin.im/entry/5b596d09e51d4519226f7ae3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞