使用原生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/的講解及實現。