用PHP实现Web Server

以前做的一个例子,用PHP模拟Web服务器和客户端。年初的时候网站数据丢失,重新补传一次。

服务端代码server.php:

<?php

$host = '127.0.0.1';
$port = 9083;

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === FALSE)
{
    display_socket_error();
}

if (socket_bind($socket, $host, $port) === FALSE)
{
    display_socket_error();
}

if (socket_listen($socket, 3) === FALSE)
{
    display_socket_error();
}

while (true)
{
    $csocket = socket_accept($socket);
    if ($csocket === FALSE)
    {
        display_socket_error();
    }
    
    $request = parse_socket_request($csocket);
    print_r($request);
    
    if (strcasecmp($request['uri'], '/quit') === 0)
    {
        socket_close($csocket);
        break;
    }
    
    handle_request($csocket, $request);
    socket_close($csocket);
}

socket_close($socket);

function display_socket_error()
{
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("Socket error: [$errorcode] $errormsg");
}

function parse_socket_request($socket)
{
    $result = socket_read($socket, 4096, PHP_BINARY_READ);
    if ($result === FALSE)
    {
        display_socket_error();
    }
    
    $lines = explode("\r\n", $result);
    
    list($method, $uri, $protocol) = explode(" ", $lines[0], 3);
    
    $headers = array();
    for ($i = 1; $i < count($lines); $i++)
    {
        if ($lines[$i] === "")
            break;
        $parts = explode(":", $lines[$i], 2);
        $headers[trim($parts[0])] = trim($parts[1], " \"");
    }
    
    $body = '';
    for ($i = $i + 1; $i < count($lines); $i++)
    {
        $body .= $lines[$i] . "\r\n";
    }
    
    return array('protocol' => $protocol,
            'uri' => $uri,
            'method' => $method,
            'headers' => $headers, 
            'body' => $body
    );
}

function send_response_line($socket, $line)
{
    if (socket_write($socket, $line, strlen($line)) === FALSE)
    {
        display_socket_error();
    }
}

function handle_request($socket, $request)
{
    $abs_path = realpath(dirname(__FILE__) . '/../' . $request['uri']);
    
    if (is_file($abs_path))
    {
        send_response_content($socket, $abs_path, 200, "OK");
        return;
    }
    
    if (is_dir($abs_path))
    {
        $index_file = $abs_path . DIRECTORY_SEPARATOR . 'index.html';
        if (is_file($index_file))
        {
            send_response_content($socket, $index_file, 200, "OK");
            return;
        }
    }
    
    send_response_404($socket);
}

function send_response_content($socket, $path, $statusCode, $statusText)
{
    $mimetype = get_content_type($path);
    $content = file_get_contents($path);
    
    send_response_line($socket, "HTTP/1.1 " . $statusCode . " " . $statusText . "\r\n");
    send_response_line($socket, "Server: PHPSocketServer\r\n");
    send_response_line($socket, "Accept-Ranges: bytes\r\n");
    send_response_line($socket, "Content-Type: " . $mimetype . "\r\n");
    send_response_line($socket, "Content-Length: " . strlen($content) . "\r\n");
    send_response_line($socket, "\r\n");
    send_response_line($socket, $content);    
}

function send_response_404($socket)
{
    send_response_content($socket, realpath(dirname(__FILE__) . '/error404.html'), 404, 'Not Found');
}

function get_content_type($path)
{
    $mimetypelist["gif"] = "image/gif";
    $mimetypelist["jpeg"] = "image/jpeg";
    $mimetypelist["jpg"] = "image/jpeg";
    $mimetypelist["jpe"] = "image/jpeg";
    $mimetypelist["png"] = "image/png";
    $mimetypelist["tiff"] = "image/tiff";
    $mimetypelist["tif"] = "image/tiff";
    $mimetypelist["bmp"] = "image/x-ms-bmp";
    $mimetypelist["html"] = "text/html";
    $mimetypelist["htm"] = "text/html";
    $mimetypelist["txt"] = "text/plain";

    $ext = pathinfo($path, PATHINFO_EXTENSION);
    if (isset($mimetypelist[$ext]))
        return $mimetypelist[$ext];
    else
        return '';
}

客服端代码client.php:

<?php

$host = '127.0.0.1';
$port = 9083;

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === FALSE)
{
    display_socket_error();
}

if (socket_connect($socket, $host, $port) === FALSE)
{
    display_socket_error();
}

echo "Connection successful on IP $host, port $port\r\n\r\n\r\n";

send_request_line("GET /abc.txt HTTP/1.1\r\n");
send_request_line("Host: localhost\r\n");
send_request_line("\r\n");

$response = parse_socket_response();
print_r($response);

socket_close($socket);

function send_request_line($line)
{
    global $socket;
    if (socket_write($socket, $line, strlen($line)) === FALSE)
    {
        display_socket_error();
    }
}
       
function display_socket_error()
{
    $errorcode = socket_last_error();
    $errormsg = socket_strerror($errorcode);
    die("Socket error: [$errorcode] $errormsg");
}

function parse_socket_response()
{
    global $socket;
    
    $result = socket_read($socket, 4096, PHP_BINARY_READ);
    if ($result === FALSE)
    {
        display_socket_error();
    }
    
    /*
    返回的响应信息如下:
    HTTP/1.1 200 OK
    Date: Wed, 27 Jul 2011 01:49:49 GMT
    Server: Apache/2.2.15 (Win32) mod_ssl/2.2.15 OpenSSL/0.9.8n
    Last-Modified: Tue, 26 Jul 2011 05:57:42 GMT
    ETag: "200000006a2ae-16-4a8f29c2a18a6"
    Accept-Ranges: bytes
    Content-Length: 22
    Content-Type: text/plain
    
    This is a server file.    
    
    每一行都以\r\n作为结尾。
    
    响应信息可以分为三个部分:
    1. 第一行是响应行,以空格分割,依次是协议,状态码和状态说明。
    2. 响应头,在响应信息中以一个空行分割。名称和值以冒号分开。
    3. 内容,在响应信息的最后面。
    */
    
    $lines = explode("\r\n", $result);
    
    list($protocol, $statusCode, $statusText) = explode(" ", $lines[0], 3);
    
    $headers = array();
    for ($i = 1; $i < count($lines); $i++)
    {
        if ($lines[$i] === "")
            break;
        $parts = explode(":", $lines[$i], 2);
        $headers[trim($parts[0])] = trim($parts[1], " \"");
    }
    
    $body = '';
    for ($i = $i + 1; $i < count($lines); $i++)
    {
        $body .= $lines[$i] . "\r\n";
    }
    
    return array('protocol' => $protocol,
            'statusCode' => $statusCode,
            'statusText' => $statusText,
            'headers' => $headers, 
            'body' => $body
    );
}