zoukankan      html  css  js  c++  java
  • php做一个webserver

    php做一个webserver

    1. 目标

    利用php实现一个不依靠nginx/apache的简易webserver,同时支持Router路由功能,实现如在命令行键入php server 8080启动的功能

    2. 流程

    做一个webserver需要做的模块:

    • 监听连接进来
    • 客户端连接服务端
    • 服务端接连接
    • 服务端做出响应

    3. 接收命令行参数

    项目根目录创建一个server.php文件,用于接收命令行参数,并进行项目初始化工作

    先是获取命令行参数,有一个提前定义好的变量名argv,我们直接打印测试一下

    <?php
    var_dump($argv);
    

    运行php server a b c d

    数据结果为:

    array(5) {
      [0]=>
      string(10) "server.php"
      [1]=>
      string(1) "a"
      [2]=>
      string(1) "b"
      [3]=>
      string(1) "c"
      [4]=>
      string(1) "d"
    }
    

    我们发现第一个选项是文件名,这个并不是我们所需要的,所以使用array_shift去除他,这样我们就能获取他的参数了,下面我们实现通过php server port这条命令

    <?php
    array_shift($argv);
    if (empty($argv)){
        $port = 80;
        var_dump("port is 80");
    }else{
        $port = $argv[0];
        var_dump("port is ".$port);
    }
    

    5 实现自动加载

    利用composer生成自动加载的文件:

    生成composer.json文件

    composer init
    

    下面是我的composer.json:

    {
        "name": "root/webserver",
        "type": "lib",
        "license": "mit",
        "minimum-stability": "dev",
        "require": {},
        "autoload":{
            "psr-4":{
                "webserver\":"src/"
            }
        }
    }
    
    

    生成自动引入程序:

    copmoser install
    

    创建webserver目录和src目录:

    结构如下:

    ├── composer.json
    ├── composer.lock
    ├── server
    ├── src
    └── vendor
        ├── autoload.php
        └── composer
            ├── autoload_classmap.php
            ├── autoload_namespaces.php
            ├── autoload_psr4.php
            ├── autoload_real.php
            ├── autoload_static.php
            ├── ClassLoader.php
            ├── installed.json
            ├── installed.php
            ├── InstalledVersions.php
            └── LICENSE
    
        
    

    接下来进行验证是否可以 使用:

    在src下新建Hello.php,内容如下:

    <?php
    namespace webserver;
    
    class Hello{
        public function say(){
            return "hello.world";
        }
    }
    

    根目录的server.php:

    <?php
    
    require __DIR__."/vendor/autoload.php";
    array_shift($argv);
    if (empty($argv)){
        $port = 80;
    }else{
        $port = $argv[0];
    }
    
    $hello = new webserverHello();
    $a = $hello->say();
    var_dump($a); //hello.world
    

    如果正确打印,且不报错,说明到这里所有的步骤都是争取的.

    下面开始我们的核心部分

    6 .server服务

    启用一个webserver需要一个服务去不停的去监听该端口是不是又请求进来,在src目录下,新建server.php文件

    创建监听,又可以查分成三部:

    • 创建一个socket
    • 绑定port到socket
    • 通过一个while 循环去监听请求

    第一步: 创建socket:

    这里我们使用函数socket_create()函数

    该函数用法: socket_create ( int $domain, int$type, int$protocol)`

    需要注意的,这几个参数,的值都是int,所以需要查找手册,看一下他预定义的常量表示的含义

    下面是创建socket的代码:

    <?php
    namespace webserver;
    
    class Server{
        protected $host;
        protected $port;
        protected $socket;
    
        protected function createSocket(){
            $this->socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
        }
    }
    

    第二步:绑定端口到socket

    这里使用的函数socket_bind

    用法:socket_bind ( Socket $socket, string $address , int $port):bool

    手册地址https://www.php.net/manual/en/function.socket-bind.php

    第一个参数是,上面我们定义好的socket的实例,

    <?php
    namespace webserver;
    
    class Server{
        protected $host;
        protected $port;
        protected $socket;
        public function __construct($host,$port){
            $this->createSocket();
            $this->bind($host,$port);
        }
    
        protected function createSocket(){
            $this->socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
        }
    
        protected function bind($host,$port){
            $res = socket_bind($this->socket,$host,$port);
            if (!$res){
                var_dump("socket 绑定失败");
            }
        }
    }
    

    绑定socket到端口,然后将这两部都放进初始化函数中

    第三步: 监听端口

    前面我们第一步创建socket的一端,然后绑定到指定端口,接下来我们要告诉socket,开始监听了,使用socket_listen函数

    用法:socket_listen ( Socket $socket , int $backlog = 0 ) : bool

    第二个参数有默认值0, 返回值是布尔

    开始监听以后使用socket_accept用来接收请求参数,socket_accept 的返回值是一个含有请求信息的socket,然后在利用socket_read去读取这个socket里面的内容.

    下面是具体的代码:

    public function listen(){
    
            $listen_res = socket_listen($this->socket);
            if (!$listen_res){
                var_dump(socket_strerror(socket_last_error()));
                contine();
            }
            //socket_set_nonblock($this->socket);
    
            while(true){
                //判断接收请求是否存在异常,如果有异常则跳过该条请求
                if(!$client=socket_accept($this->socket)){
                    socket_close($this->socket);
                    continue;
                }
                //进入到这里说明请求没有问题,接下进行解析请求参数
                
                $data = socket_read($this->socket,1024);
                var_dump($data);
            }
        }
    

    修改一下根目录下的server.php:

    <?php
    
    require __DIR__."/vendor/autoload.php";
    
    
    array_shift($argv);
    if (empty($argv)){
        $port = 80;
    }else{
        $port = $argv[0];
    }
    
    use phpserverServer;
    $server = new Server("0.0.0.0",$port);
    $server->listen();
    

    启动webserver:

    php server.php 9001
    

    如果感觉server.php不好看的话,可以将文件名改成 server

    那么命令就变成php server 9001

    测试结果

    "
    string(730) "GET / HTTP/1.1
    Host: 192.168.2.10:9001
    Connection: keep-alive
    Cache-Control: max-age=0
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
    Cookie: experimentation_subject_id=eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqSTNaak5tT0dJMUxUSmhNbUl0TkdGaVlpMWlZelUyTFRVeU5HRmpaVGMyT1dJeE15ST0iLCJleHAiOm51bGwsInB1ciI6ImNvb2tpZS5leHBlcmltZW50YXRpb25fc3ViamVjdF9pZCJ9fQ%3D%3D--98ff316f61bc94dfd47dc8cfdd41e6d568723001
    
    "
    

    7.解析请求

    这一步我们将得到head头数据进行解析,获取到uri地址,以及数据,然后将请求与响应的路由做适配,最后将适配的结果返回给浏览器,大体是这样一个流程.

    所以我们接下来做的就是解析header头信息:

    首先我们获取请求类型,get/post 然后是他的路由地址

    $data = explode("
    ",$data);
    //首先获取请求方法
    list($method,$uri) = explode(" ",array_shift($data));
    @list($uri,$param_str) = explode("?",$uri);// 因为可能没有参数,所以用错误抑制符
    parse_str( $param_str, $params); //将路由参数,解析成数组
    

    然后我们将其他header头参数进行解析:

     $headers = [];
    foreach($data as $headOpt){
        $arr   = explode(":",$headOpt);
        if(count($arr)==2){
            $headers[$arr[0]] = $arr[1];
        }    
    }
    

    现在的话,我们得到的数据有,请求参数,请求uri,各个header信息,接下来我们将得到的数据,放到全局中,便于调用,这些参数不能别修改,所以使用protected修饰,同时提供访问的方法

    下面是一个汇总以后的方法:

    protected $uri;
    protected $method;
    protected $params;
    protected $headers;
    //....
    protected function getRequest($data){
        $data = explode("
    ",$data);
        //首先获取请求方法
        list($method,$uri) = explode(" ",array_shift($data));
        @list($uri,$param_str) = explode("?",$uri);
        parse_str( $param_str, $params);
    
        $headers = [];
        foreach($data as $headOpt){
            $arr   = explode(":",$headOpt);
            if(count($arr)==2){
                $headers[$arr[0]] = $arr[1];
            }    
        }
        $this->uri = $uri;
        $this->method = strtoupper($method);
        $this->params = $params;
        $this->headers = $headers;
        return $this;
    
    }
    public function method(){
        return $this->method;
    }
    public function uri(){
        return $this->uri;
    }
    public function params(){
        return $this->params;
    }
    public function headers(){
        return $this->headers;
    }
    

    我们将request请求抽离成一个单独的文件Request.php:

    <?php
    /**
     * Created by PhpStorm.
     * User: lx
     * Date: 2021/4/18
     * Time: 23:03
     */
    
    namespace webserver;
    
    
    class Request
    {
        protected $uri;
        protected $method;
        protected $params;
        protected $headers;
        public function getRequest($data){
            $data = explode("
    ",$data);
            //首先获取请求方法
            list($method,$uri) = explode(" ",array_shift($data));
            @list($uri,$param_str) = explode("?",$uri);
            parse_str( $param_str, $params);
    
            $headers = [];
            foreach($data as $headOpt){
                $arr   = explode(":",$headOpt);
                switch(count($arr)){
                    case 2:
                        $headers[$arr[0]] = $arr[1];
                        break;
                    case 3:
                        $headers[$arr[0]] = $arr[1].$arr[2];
                        break;
    
                }
            }
            $this->uri = $uri;
            $this->method = strtoupper($method);
            $this->params = $params;
            $this->headers = $headers;
            return $this;
    
        }
        public function method(){
            return $this->method;
        }
        public function uri(){
            return $this->uri;
        }
        public function params(){
            return $this->params;
        }
        public function headers(){
            return $this->headers;
        }
    
    }
    

    8.响应请求

    根据不同的请求地址,访问到不同的控制器,所以这里我们需要首先实现一个路由功能

    做一个路由:

    首先我们在src/server.php 同级目录创建一个Router.php文件,这个文件作为我们的处理逻辑与请求地址的映射关系.一般我们平时使用的框架,写一条路由会包含三部分,请求方法,请求uri,逻辑处理文件方法,这里我们同样需要这样做:

    实现的效果: Router::get();这种形式

    下面是Router中的方法:

    <?php
    /**
     * Created by PhpStorm.
     * User: lx
     * Date: 2021/4/18
     * Time: 22:45
     */
    
    namespace webserver;
    
    
    class Router
    {
        public static $GetRouter=[];
        public static $PostRouter=[];
    
        public static function get($uri,$reflect){
            //首先将$method解析一下
            @list($class,$method) = explode("@",$reflect);
            self::$GetRouter[$uri] = [
                "class" => $class,
                "method"=>$method
            ];
        }
        public static function post($uri, $reflect){
            //首先将$method解析一下
            @list($class,$method) = explode("@",$reflect);
            self::$PostRouter[$uri] = [
                "class" => $class,
                "method"=>$method
            ];
        }
    
    }
    

    接下来,我们创建定义路由的文件,同样在同级目录,创建config.php,用于注册路由:

    <?php
    namespace webserver;
    
    Router::get("/","webservercontrollerindex@index");
    Router::get("/welcome","webservercontrollerindex@welcome");
    

    加载路由

    路由我们做好了,接下来就是在程序初始的时候,将路由的映射加载进来:

    下面我们在src/server.php中增加init方法

    //初始化一些准备工作
    protected function init(){
        require_once __DIR__."/config.php";
    
    }
    

    并在构造函数中调用

    public function __construct($host,$port){
    
        $this->init();
       //....
    
    }
    

    路由和控制器做绑定

    我们得到了路由,通过Requst.php我们得到了请求信息,接下来我们来做映射关系:

    创建Response.php作为相应处理, 在这之前我们需要将src/server.php做一些调整,我们让request获取的数据传递给response进行处理

    //src/server.php
    
    public function listen(){
    
            $listen_res = socket_listen($this->socket);
            if (!$listen_res){
                var_dump(socket_strerror(socket_last_error()));
                contine();
            }
            //socket_set_nonblock($this->socket);
    
            $request = new Request();
            $response = new Response();
            while(true){
                //判断接收请求是否存在异常,如果有异常则跳过该条请求
                if(!$client=socket_accept($this->socket)){
                    socket_close($this->socket);
                    continue;
                }
                //进入到这里说明请求没有问题,接下进行解析请求参数
                
                $data = socket_read($client,1024);
    
                $requestObj =$request->getRequest($data);
                $resCtx = $response->handle($requestObj);//交给response进行处理
        
            }
        }
    

    下面是处理逻辑:

    Response.php:

    <?php
    
    namespace webserver;
    
    
    class Response
    {
    
        public function setHeader($code,$msg,$len){
    
            /**
             *  HTTP/1.1 200 OK
                Content-Length: 152
                Content-Type: text/plain; charset=UTF-8
                Date: Sun, 18 Apr 2021 15:22:23 GMT
             */
            $lines = [];
            $lines[] = "HTTP/1.1 ".$code." ".$msg;
            $lines[] = "Content-Length: ".$len;
            $lines[] = "Date: ".date( 'D, d M Y H:i:s T' );
    
            return implode( " 
    ", $lines )."
    
    ";
        }
    
        public function handle(Request $request){
            $method = $request->method();
    
            switch($method){
                case "GET":
                    $map = Router::$GetRouter;
                    break;
                case "POST":
                    $map = Router::$PostRouter;
                    break;
            }
    
            if(isset($map[$request->uri()])){
                $className  = $map[$request->uri()]["class"];
                $methodName = $map[$request->uri()]["method"];
                $obj = new $className;
                $content = (string)$obj->$methodName();
                $header = $this->setHeader(200,"OK",strlen($content));
                return $header. $content;
            }else{
                $header = $this->setHeader(404,"Not Found",0);
                return $header;
            }
    
        }
    
    }
    

    上面的程序也很简单,需要注意的是,我们在返回给浏览器的时候要有响应头

    将数据写入浏览器

    src/server.php

     //进入到这里说明请求没有问题,接下进行解析请求参数
                
    $data = socket_read($client,1024);
    
    $requestObj =$request->getRequest($data);
    $resCtx = $response->handle($requestObj);//交给response进行处理
    
    //上面是之前的代码,只为了标识一下位置,
    socket_write( $client, $resCtx, strlen($resCtx));
    socket_close( $client );
    

    9.完成

    基本完成了,下面我们新建一个控制器做一个测试,创建一个controller/index.php

    <?php
    
    namespace webservercontroller;
    
    class index
    {
        public function index(){
            return "<h1>hello,world</h1>";
        }
        public function welcome(){
            return json_encode([
                "msg"=>"welcome"
            ]);
        }
    
    
    }
    

    最终的目录结构为:

    .
    ├── composer.json
    ├── composer.lock
    ├── server
    ├── src
    │   ├── config.php # 路由文件
    │   ├── controller
    │   │   └── index.php #测试的控制器
    │   ├── Request.php # 处理请求
    │   ├── Response.php #处理响应
    │   ├── Router.php #路由逻辑处理
    │   └── Server.php #socket服务
    └── vendor
        ├── autoload.php
        └── composer
            ├── autoload_classmap.php
            ├── autoload_namespaces.php
            ├── autoload_psr4.php
            ├── autoload_real.php
            ├── autoload_static.php
            ├── ClassLoader.php
            ├── installed.json
            ├── installed.php
            ├── InstalledVersions.php
            └── LICENSE
    
    

    最后我们修改一下入口文件:

    <?php
    
    require __DIR__ . "/vendor/autoload.php";
    
    
    array_shift($argv);
    if (empty($argv)){
        $port = 80;
    }else{
        $port = $argv[0];
    }
    
    $server = new webserverServer("0.0.0.0",$port);
    
    $server->listen();
    

    运行

    php server 9001
    

    打开浏览器访问: http://192.168.2.10:9001/

    最后放一张效果图:

  • 相关阅读:
    理清可导、可微、连续...的概念
    f(sinx)到底是啥
    高数基础知识整理7.渐近线与曲率
    高数基础知识整理6.函数单调性与凹凸性
    高数基础知识整理4.导数与微分
    数组、稀疏矩阵、广义表综合应用
    数据库(外键及其约束理解)
    js中按钮去触发定时器,那么多次点击这个定时器会越来越快,解决方法
    JavaScript splice() 方法和JavaScript split() 方法
    javascript中加号(+)操作符的作用
  • 原文地址:https://www.cnblogs.com/callmelx/p/14675226.html
Copyright © 2011-2022 走看看