php实现HTTP 服务器

  • baagee 发布于 2018-12-31 14:07:41
  • 分类:PHP
  • 141 人围观
  • 0 人喜欢

2018年最后一天里,还是和往常周末一样,宅起来。。没有什么乐趣,写写代码,看看电影,玩玩手机就这样过去了。今天打算写一篇文章吧,讲一讲php socket之类的,这个平时基本用不到,平时都是curd之类的对数据库的操作而已。具体的php socket系列函数直接看官方文档就行http://www.php.net/sockets/

我们知道http是基于tcp/ip协议实现的,那么就可以通过C或者java/python等等语言通过socket编程实现。

简单说下http协议,就是通过tcp协议发送特定格式的信息,响应特定格式的信息而已,具体说起来又是一大推,我就不说了,具体的查一下就行了。

php版本的性能很渣渣,我就是自己实现一下而已,对http协议有了更深刻的理解,希望你看了也有所收获吧。

废话少说,show me code!

下面简单示范一下面相过程简单版的示例,打开浏览器页面出现hello:

<?php
#创建一个tcp的socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

#绑定需要监听的端口号
if (socket_bind($socket, '127.0.0.1', 8888) == false) {
    echo 'server bind fail:' . socket_strerror(socket_last_error());
    die;
}
#开始监听
if (socket_listen($socket, 4) == false) {
    echo 'server listen fail:' . socket_strerror(socket_last_error());
    die;
}

while (true) {
#获取一个连接对象
    $accept_resource = socket_accept($socket);

    if ($accept_resource !== false) {
    	#读取请求信息
        $string = socket_read($accept_resource, 1024);

        echo 'Server receive is:' . PHP_EOL . $string . PHP_EOL;
        if ($string != false) {
            $response = <<<EOF
HTTP/1.1 200 OK
Server: MyServer
Content-Type: text/html; charset=utf-8

hello
EOF;
			#向对方回复信息
            socket_write($accept_resource, $response, strlen($response));
        } else {
            echo 'socket_read is fail';
        }
        #关闭连接
        socket_close($accept_resource);
    }
}
#关闭socket
socket_close($socket);

浏览器访问http://127.0.0.1:8888

页面就会出现hello。


下面复杂一点的面向过程的写法,思路和面向过程的基本一样,就是稍微封装了一下,增加了一点点小的功能,可以像nginx/apache一样配置网站根目录,返回静态资源文件,解析执行php代码,将结果返回浏览器等基本的功能,使用fork简单实现了多进程,虽然并没什么卵用。

主要代码 HttpServer.php:

<?php
/**
 * Desc: Http服务器
 * User: baagee
 * Date: 2018/12/24
 * Time: 下午3:52
 */

namespace SimServer;
/**
 * Class HttpServer
 * @package SimServer
 */
class HttpServer
{
    /**
     * 默认访问的文件
     */
    protected $index = 'index.html';
    /**
     * @var string ip地址
     */
    protected $ip = '127.0.0.1';
    /**
     * @var string web root 根目录
     */
    protected $web_root = '';
    /**
     * @var int 端口号
     */
    protected $port = 8888;

    /**
     * @var null|resource
     */
    protected $socket = null;

    /**
     * @var int
     */
    protected $worker_number = 5;

    /**
     * @var string
     */
    protected $main_app = '';

    /**
     * HttpServer constructor.
     * @param string $webroot       根目录
     * @param string $ip            IP
     * @param int    $port          端口
     * @param string $http_log_dir  log目录
     * @param string $main_app      app入口类
     * @param string $index         默认页面
     * @param int    $worker_number 子进程数量
     */
    public function __construct($webroot, $ip = '127.0.0.1', $port = 8888, $http_log_dir = __DIR__, $main_app = '', $index = 'index.html', $worker_number = 5)
    {
        $this->web_root      = $webroot;
        $this->ip            = $ip;
        $this->port          = $port;
        $this->main_app      = $main_app;
        $this->index         = $index;
        $this->worker_number = $worker_number;
        $this->socket        = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        ServerLog::init($http_log_dir);
        $this->bind();
        $this->listen();
    }

    /**
     * 开始监听
     */
    protected function listen()
    {
        if (!socket_listen($this->socket, 4)) {
            ServerLog::record(sprintf('socket_listen failed %s', socket_strerror(socket_last_error())));
            die;
        }
        ServerLog::record(sprintf('socket listening on %s:%d', $this->ip, $this->port));
        ServerLog::record(sprintf('http://%s:%d', $this->ip, $this->port));
    }

    /**
     * 绑定端口
     */
    protected function bind()
    {
        if (!socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
            ServerLog::record('Unable to set option on socket: ' . socket_strerror(socket_last_error()));
            die;
        }
        if (!socket_bind($this->socket, $this->ip, $this->port)) {
            ServerLog::record(sprintf('socket_bind failed on %s:%d %s', $this->ip, $this->port, socket_strerror(socket_last_error())));
            die;
        }
    }

    /**
     * 获取客户端
     * @return resource
     */
    protected function getClient()
    {
        return socket_accept($this->socket);
    }

    /**
     * 获取客户端请求
     * @param $client
     * @return Request
     */
    protected function getRequest($client): Request
    {
        return new Request($client);
    }

    /**
     * 开始运行
     */
    public function run()
    {
        if (!function_exists('pcntl_fork')) {
            // 单进程版
            while ($client = $this->getClient()) {
                if ($client !== false) {
                    $request = $this->getRequest($client);
                    ServerLog::record('Request:' . PHP_EOL . $request->raw_request);
                    $this->handler($request, new Response(), $client);
                    $this->closeClient($client);
                }
            }
        } else {
            // 多进程版
            for ($i = 1; $i <= $this->worker_number; $i++) {
                $pid = pcntl_fork();
                if ($pid == -1) {
                    ServerLog::record('Fork fail');
                } elseif ($pid == 0) {
                    // 子进程
                    $id = getmypid();
                    ServerLog::record("Child process pid=" . $id);
                    while ($client = $this->getClient()) {
                        if ($client !== false) {
                            $request = $this->getRequest($client);
                            ServerLog::record('Child pid=' . $id . ' get request:' . PHP_EOL . $request->raw_request);
                            $this->handler($request, new Response(), $client);
                            ServerLog::record('Child over pid=' . $id);
                            $this->closeClient($client);
                        }
                    }
                }
            }
        }
    }

    /**
     * 处理请求
     * @param Request  $request
     * @param Response $response
     * @param          $client
     * @throws \Exception
     */
    protected function handler(Request $request, Response $response, $client)
    {
        $file = $this->web_root . $request->path;
        if (is_file($file)) {
            $ext = pathinfo($file, PATHINFO_EXTENSION);
            if (array_key_exists($ext, MIMETypes::MIME_TYPE_MAP)) {
                $mime_type = MIMETypes::MIME_TYPE_MAP[$ext];
            } else {
                $mime_type = MIMETypes::TEXT_PLAIN;
            }
            Response::setHeader('Content-Type', $mime_type);
            $response->setBody(file_get_contents($file));
        } else {
            if ($request->path == '/') {
                // 默认index.html
                $index_file = $this->web_root . DIRECTORY_SEPARATOR . $this->index;
                if (is_file($index_file)) {
                    Response::setHeader('Content-Type', MIMETypes::TEXT_HTML);
                    $response->setBody(file_get_contents($index_file));
                } else {
                    $response->setStatusCode(404);
                }
            } else {
                if (!empty($request->path) && $request->path !== '/favicon.ico' && $this->main_app !== '') {
                    // /a/b/c  /aa/bb/cc /aa/bb
                    try {
                        $app_class = $this->main_app;
                        $app       = new $app_class($request);
                        if ($app instanceof AppBase) {
                            $res = $app->run();
                            $response->setBody($res);
                        } else {
                            throw new \Exception(sprintf("%s not instanceof AppBase", $app_class));
                        }
                    } catch (\Throwable $e) {
                        Response::setStatusCode(500);
                        ServerLog::record('Server 500 Error:' . $e->getMessage());
                    }
                } else {
                    Response::setStatusCode(404);
                }
            }
        }
        $response->send($client);
    }

    /**
     * 关闭客户端链接
     * @param $client
     */
    public function closeClient($client)
    {
        socket_close($client);
        ServerLog::record('Client closed' . PHP_EOL . str_repeat('-', 60));
    }

    /**
     * 关闭链接 socket
     */
    public function __destruct()
    {
        socket_close($this->socket);
    }
}

具体的全部代码请前往github查看,使用composer的类自动加载。https://github.com/baagee/php_code_snippet/tree/master/SimpleHttpServer

大致的目录结构

├── App # App目录
│   ├── index.html 默认index静态页面
│   ├── src # php代码
│   │   ├── App.php #做路由解析之类的
│   │   └── Controller # 控制器类
│   │       └── User.php
│   └── static #存放静态资源文件
│       ├── 1.gif
│       ├── 2.gif
│       ├── 3.png
│       ├── debug.jpg
│       ├── favicon.ico
│       ├── index.css
│       └── index.js
├── Server # php版的服务器
│   ├── AppBase.php
│   ├── HttpServer.php
│   ├── MIMETypes.php
│   ├── Request.php
│   ├── Response.php
│   ├── ServerLog.php
│   └── StatusCode.php
├── composer.json
├── composer.phar
├── conf.ini # 配置文件 
├── log # http服务log目录
│   └── 2018-12-31-05-http.log
├── run.php # 启动http服务
└── vendor

在mac上单进程运行测试了一下 php run.php ,浏览器打开127.0.0.1:8080


可以看到这些静态资源文件全都正常解析出来了。

用ab压力测试工具试了一下,貌似还可以


要想访问user控制器的json方法就浏览器访问127.0.0.1:8080/user/json就行啦:


那要是想分模块呢?那就在src/App.php里面自由分配路由吧


转载请说明出处:baagee博客 » php实现HTTP 服务器
标签: php

评论

点击图片切换
还没有评论,快来抢沙发吧!