使用純php構建一個簡單的PHP服務器


使用原生PHP構建一個簡單的PHPWeb服務器

1.目錄機構

webserver
    --src
        -- Response.php
        -- Server.php
        -- Request.php
    -- vendor
    -- Server.php
    -- composer.json

2. 使用comoposer構建自動加載

編輯`comooser.json`文件

{
    "autoload": {
        "psr-4": {
            "Icarus\\Station\\PHPServer\\": "src/"
        }
    }
}

使用PSR-4自動加載方式構建自動加載

3. 編寫 Server文件

該文件作為啟動文件,使用以下命令 php Server 8080 啟動服務

<?php

use Icarus\Station\PHPServer\Server;
use Icarus\Station\PHPServer\Request;
use Icarus\Station\PHPServer\Response;

array_shift($argv);

//獲取端口號
if (empty($argv)) {
    $port = 80;
}else {
    $port = (int)array_shift($argv);
}

require_once "./vendor/autoload.php";

$server = new Server("127.0.0.1",$port);

$server->listen(function(Request $request){
    return new Response("Hello World!");
});

4. 編寫Response.php

該類實現對請求的響應

<?php

namespace Icarus\Station\PHPServer;

class Response
{
    protected static $statusCodes = [
        100 => 'Continue',
        101 => 'Switching Protocols',

        // Success 2xx
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',

        // Redirection 3xx
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found', // 1.1
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        // 306 is deprecated but reserved
        307 => 'Temporary Redirect',

        // Client Error 4xx
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',

        // Server Error 5xx
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported',
        509 => 'Bandwidth Limit Exceeded'
    ];

    protected $status = 200;
    protected $body = '';
    protected $headers = [];

    public function __construct($body, $status = null)
    {
        if (!is_null($status)) {
            $this->status = $status;
        }
        $this->body = $body;
        $this->header('Date', gmdate('D, d M Y H:i:s T'));
        $this->header('Content-Type', 'text/html; charset=utf-8');
        $this->header('Server', 'PHPServer/1.0.0');
    }

    public function header($key, $val)
    {
        $this->headers[ucwords($key)] = $val;
    }

    public function buildHeaderString()
    {
        $lines = [];
        $lines[] = "HTTP/1.1 " . $this->status . " " . static::$statusCodes[$this->status];

    
        foreach ($this->headers as $key => $value) {
            $lines[] = $key . ": " . $value;
        }

        return implode(" \r\n", $lines) . "\r\n\r\n";
    }

    public static function error($statusCode)
    {
        header(self::$statusCodes[$statusCode], '', $statusCode);
    }

    public function __toString()
    {
        return $this->buildHeaderString() . $this->body;
    }
}

5. 編寫Request.php

該類主要實現請求的解析(暫時為GET請求)

<?php

namespace Icarus\Station\PHPServer;

class Request
{
    protected $uri = '';
    protected $method = '';
    protected $params = [];
    protected $headers = [];

    public function __construct($method, $uri, $headers)
    {
        $this->method = strtolower($method);
        $this->headers = $headers;
        list($this->uri, $param) = explode('?', $uri);
        parse_str($param, $this->params);
    }

    public function method()
    {
        return $this->method;
    }

    public function headers($key, $default = null)
    {
        if (isset($this->headers[$key])) {
            $default = $this->headers[$key];
        }
        return $default;
    }

    public function uri()
    {
        return $this->uri;
    }

    public function params($key, $default = null)
    {
        if (isset($this->params[$key])) {
            $default = $this->params($key);
        }
        return $default;
    }


    public static function withHeaderString($data)
    {
        $headers = explode("\n", $data);
        list($method, $uri) = explode(" ", array_shift($headers));
        $header = [];
        foreach ($headers as $value) {
            $value = trim($value);
            if (strpos($value, ":") !== false) {
                list($key, $value) = explode(":", $value);
                $header[$key] = $value;
            }
        }
        return new static($method, $uri, $header);
    }

    public function __toString()
    {
        return json_encode($this->headers);
    }
}

6.編寫Server.php

該模塊主要實現對socket的封裝。

<?php

namespace Icarus\Station\PHPServer;

class Server
{

    protected $host = null;
    protected $port = null;
    protected $socket = null;

    public function __construct($host, $port)
    {
        $this->host = $host;
        $this->port = $port;
        $this->createSocket();
        $this->bind();
    }

    protected function createSocket()
    {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, 0);
    }

    protected function bind()
    {
        if (!socket_bind($this->socket,$this->host, $this->port)) {
            throw  new \Exception("未能綁定socket: " . $this->host . $this->port . socket_strerror(socket_last_error()));
        }

    }

    public function listen(callable $callback)
    {
        while (true) {
            socket_listen($this->socket);
            if (!$client = socket_accept($this->socket)) {
                socket_close($client);
                continue;
            }
            $data = socket_read($client, 1024);
            $request = Request::withHeaderString($data);
            $response = call_user_func($callback, $request);
            if (!$response | !$response instanceof Response) {
                $response = Response::error(404);
            }
            $response = (string)$response;
            socket_write($client,$response,strlen($response));
            socket_close($client);
        }
    }
}

7. 使用

1. 進入到項目目錄
2. 執行` php Server 8080`
3. 另起一個終端,執行 `curl "http://127.0.0.1:8080`
4. 顯示 `Hello World!`

8. 總結

該demo使用socket來實現一個簡單的webserver,但是由於php不支持多線程的的特性(其實也能實現,不過需要安裝pthreads擴展),還是不適合開發webserver,此demo主要用來學習webserver的概念。

此demo參考了http://station.clancats.com/writing-a-webserver-in-pure-php/的講解及實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM