博客 / 詳情

返回

php實現web服務器

使用php(非swoole)實現tcp/http服務器。

php內置的stream系列函數 和 socket擴展提供了對網絡編程的支持。socket擴展需要在編譯時通過配置--enable-sockets開啓,而strem系列函數則完全是php核心內置的函數。php社區中的workman框架底層就是基於stream函數來實現的。以下代碼通過stream系列函數演示php如何實現簡單的tcp/http等服務器

1:單進程阻塞模式

<?php

class Server
{
    /**
     * @var false|resource
     */
    private $socket;

    /**
     * 是否http服務器 默認tcp服務器
     * @var bool
     */
    private $isHttp = false;

    public function __construct(string $address)
    {
        $this->socket = @stream_socket_server($address, $errNo, $errMessage);
        if (!is_resource($this->socket)) {
            throw new InvalidArgumentException("參數異常:" . $errMessage);
        }
    }

    public function isHttp(bool $bool): self
    {
        $this->isHttp = $bool;
        return $this;
    }

    /**
     * 啓動服務器
     */
    public function run()
    {
        while (true) {
            $conn = @stream_socket_accept($this->socket);
            if ($conn) {
                if ($this->isHttp) {
                    //http server
                    $data     = "Hello World";
                    $response = "HTTP/1.1 200 OK\r\n";
                    $response .= "Content-Type: text/html;charset=UTF-8\r\n";
                    $response .= "Server: MyServer1\r\n";
                    $response .= "Content-length: " . strlen($data) . "\r\n\r\n";
                    $response .= $data;
                    fwrite($conn, $response);
                    fclose($conn);
                } else {
                    // tcp server
                    while ($message = fread($conn, 1024)) {
                        // 主動退出
                        if (trim($message) == 'quit') {
                            echo "close\n";
                            fclose($conn);
                            break;
                        }
                        echo 'I have received that : ' . $message;
                        fwrite($conn, "OK\n");
                    }
                }
            }
        }
    }
}

$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
$server->run();

// 啓動 http服務器
//$server->isHttp(true)->run();

http服務瀏覽器訪問http://127.0.0.1:2345/看可以看到輸出hello world

tcp1.png

tcp服器通過telnet進行測試

由於連接和read都是阻塞的,此時一次只能處理一個請求,只能噹噹前請求處理結束後才能處理下一個請求,不具備併發能力。

2:多進程改造

<?php

class Server
{
    /**
     * @var false|resource
     */
    private $socket;

    /**
     * 是否http服務器 默認tcp服務器
     * @var bool
     */
    private $isHttp = false;

    public function __construct(string $address)
    {
        $this->socket = @stream_socket_server($address, $errNo, $errMessage);
        if (!is_resource($this->socket)) {
            throw new InvalidArgumentException("參數異常:" . $errMessage);
        }
    }

    public function isHttp(bool $bool): self
    {
        $this->isHttp = $bool;
        return $this;
    }

    /**
     * 啓動服務器
     */
    public function run()
    {
        while (true) {
            $conn = @stream_socket_accept($this->socket);
            if ($conn) {
                if ($this->isHttp) {
                    if (pcntl_fork() == 0) {
                        //http server
                        $data     = "Hello World";
                        $response = "HTTP/1.1 200 OK\r\n";
                        $response .= "Content-Type: text/html;charset=UTF-8\r\n";
                        $response .= "Server: MyServer1\r\n";
                        $response .= "Content-length: " . strlen($data) . "\r\n\r\n";
                        $response .= $data;
                        fwrite($conn, $response);
                        fclose($conn);
                        exit;
                    }
                } else {
                    // tcp server
                    if (pcntl_fork() == 0) {
                        while ($message = fread($conn, 1024)) {
                            echo 'I have received that : ' . $message;
                            fwrite($conn, "OK\n");
                        }
                        exit;
                    }
                }
            }
        }
    }
}

$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
$server->run();

// 啓動 http服務器
//$server->isHttp(true)->run();

此時,由於通過子進程處理原本阻塞的read函數,所以主進程可以accept新的請求,提高併發能力。

tcp2.png

由於創建進程/線程會帶來很多新的問題,比如系統開銷增大等,所以,這種方法基本不會在生產環境中使用。

3:IO多路複用(select方式)

<?php

class Server
{
    /**
     * @var false|resource
     */
    private $socket;

    /**
     * 是否http服務器 默認tcp服務器
     * @var bool
     */
    private $isHttp = false;

    /**
     * @var []resource
     */
    private $socketList = [];

    public function __construct(string $address)
    {
        $this->socket = @stream_socket_server($address, $errNo, $errMessage);
        if (!is_resource($this->socket)) {
            throw new InvalidArgumentException("參數異常:" . $errMessage);
        }
        // stream_set_blocking 設置非阻塞io
        stream_set_blocking($this->socket, false);
        // 添加當前監聽的socket
        $this->socketList[(int)$this->socket] = $this->socket;
    }

    public function isHttp(bool $bool): self
    {
        $this->isHttp = $bool;
        return $this;
    }

    /**
     * 啓動服務器
     */
    public function run()
    {
        while (true) {
            $read  = $this->socketList;
            $write = $except = [];
            //阻塞監聽 設置null阻塞監聽
            $count = @stream_select($read, $write, $except, null);
            if ($count) {
                foreach ($read as $k => $v) {
                    if ($v == $this->socket) {
                        // connect success
                        // accept Connection
                        @$conn = stream_socket_accept($this->socket, null, $remote_address);
                        echo "connect fd:" . $k . PHP_EOL;
                        $this->socketList[(int)$conn] = $conn;
                    } else {
                        //receive
                        if ($this->isHttp) {
                            $this->receiveHttp($v);
                        } else {
                            $this->receiveTcp($v);
                        }
                    }
                }
            }
        }
    }

    protected function receiveHttp($stream)
    {
        $buffer = fread($stream, 1024);
        if (empty($buffer) && (feof($stream) || !is_resource($stream))) {
            fclose($stream);
            unset($this->socketList[(int)$stream]);
            echo "退出成功" . PHP_EOL;
            return;
        }
        //http server
        $data     = "Hello World";
        $response = "HTTP/1.1 200 OK\r\n";
        $response .= "Content-Type: text/html;charset=UTF-8\r\n";
        $response .= "Server: MyServer1\r\n";
        $response .= "Content-length: " . strlen($data) . "\r\n\r\n";
        $response .= $data;
        fwrite($stream, $response);
    }

    /**
     * 處理tcp請求
     */
    private function receiveTcp($stream)
    {
        $buffer = fread($stream, 1024);
        if (feof($stream) || !is_resource($stream)) {
            unset($this->socketList[(int)$stream]);
            fclose($stream);
            echo "退出成功" . PHP_EOL;
            return;
        }
        echo 'onReceive ' . $buffer . " ->" . $stream . PHP_EOL;
        $message = 'I have received that : ' . $buffer;
        fwrite($stream, "{$message}");
    }
}

$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
//$server->run();

// 啓動 http服務器
$server->isHttp(true)->run();

通過IO多路複用是目前解決高併發(C10k)問題的主要思路。

通過stream_select函數調用操作系統提供的select方法,將接收到的文件描述符號傳遞給操作系統底層,由操作系統遍歷並且返回需要處理的文件描述符號,從而不需要在應用層面一直阻塞,提高併發能力。

4:IO多路費用(poll epoll)

由於select方法存在種種問題,比如性能差,數量限制等。應對小規模併發沒有問題,但是大規模併發就顯得捉襟見肘,這個時候就需要poll,epoll(推薦)方法實現多路複用。

具體到php裏面需要安裝event或者libevent擴展,詳細代碼可以參考workman源碼,這裏不再展示。

實例代碼

  • https://github.com/tim1116/ph...

參考:

  • https://www.php.net/manual/zh... (php支持的Socket Transports)
  • https://www.php.net/manual/zh...
  • https://www.php.net/manual/zh...
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.