TP6实现原理分析系列(二):请求与响应

讲在前面的话:

请求与响应是框架生命周期中两个重要的环节,是框架的首尾两端。请求负责接管客户端请求信息,并对外提供大量接口,获取更精细化数据。响应负责将业务逻辑执行后的结果以各种形式输出。本章节将从九个方面详细介绍请求与响应的实现细节,九小节分别是:请求信息、请求变量、请求类型、头信息、请求缓存、响应类型、响应输出、响应参数、重定向。接下来是详细内容:

2.1 请求信息:

什么是请求信息?请求信息是指客户端请求服务端,发送过来的所有信息,这些信息包括请求协议、域名、端口、请求方法、请求参数等等,对于这些信息PHP语言将它们存储在一些超全局数组中,所谓的请求对象接管请求信息就是接管这些超全局数组,请看具体代码:

// 这是 Request 类的初始化方法,接管了除$_SESSION之外的超全局数据
// vendor\topthink\framework\src\think\Request.php
public static function __make(App $app)
{
        $request = new static();
 
        if (function_exists('apache_request_headers') && $result = apache_request_headers()) {
            $header = $result;
        } else {
            $header = [];
            $server = $_SERVER;
            foreach ($server as $key => $val) {
                if (0 === strpos($key, 'HTTP_')) {
                    $key          = str_replace('_', '-', strtolower(substr($key, 5)));
                    $header[$key] = $val;
                }
            }
            if (isset($server['CONTENT_TYPE'])) {
                $header['content-type'] = $server['CONTENT_TYPE'];
            }
            if (isset($server['CONTENT_LENGTH'])) {
                $header['content-length'] = $server['CONTENT_LENGTH'];
            }
        }
 
        $request->header = array_change_key_case($header);
        $request->server = $_SERVER;
        $request->env    = $app->env;
 
        $inputData = $request->getInputData($request->input);
 
        $request->get     = $_GET;
        $request->post    = $_POST ?: $inputData;
        $request->put     = $inputData;
        $request->request = $_REQUEST;
        $request->cookie  = $_COOKIE;
        $request->file    = $_FILES ?? [];
 
        return $request;
}

接管了这一系列信息之后,Request 类通过各种精细化接口对这些信息的部分获取全部进行输出,具体对外提供了那些接口,大家可以参考手册,这里对几个重要的接口进行分析。

当前访问域名或者IP:host() 方法

// 获取访问域名 vendor\topthink\framework\src\think\Request.php  1754 行
public function host(bool $strict = false): string
{
        if ($this->host) {
            $host = $this->host;
        } else {
            
            // 通过 $_SERVER 中的HTTP_X_FORWARDED_HOST属性或者HTTP_HOST来获取
            $host = strval($this->server('HTTP_X_FORWARDED_HOST') ?: $this->server('HTTP_HOST'));
        }
        // 这里是判断要不要包含端口
        return true === $strict && strpos($host, ':') ? strstr($host, ':', true) : $host;
}

当前完整URL:url() 方法

// 获取当前完整URL 包括QUERY_STRING 
// vendor\topthink\framework\src\think\Request.php  460 行
public function url(bool $complete = false): string
{
        if ($this->url) {
            $url = $this->url;
        } elseif ($this->server('HTTP_X_REWRITE_URL')) {
            $url = $this->server('HTTP_X_REWRITE_URL');
        } elseif ($this->server('REQUEST_URI')) {
            $url = $this->server('REQUEST_URI');
        } elseif ($this->server('ORIG_PATH_INFO')) {  // web服务器重定向会出现这个属性
            $url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : '');
        } elseif (isset($_SERVER['argv'][1])) {
            $url = $_SERVER['argv'][1];
        } else {
            $url = '';
        }
 
        return $complete ? $this->domain() . $url : $url;
}

2.2 输入变量:

输入变量是指可以变化的输入信息,那什么是可以变化的输入信息呢?比如,查询参数、上传文件、Post请求体这些都是可以变化的,用户都是通过这些可以变化的东西去向服务器获取或者保存信息,比如通过查询参数的变化去获取不同的商品信息,通过提交图片信息保存自己的照片,这些对于框架而言都是输入变量,Request(请求) 类提供了丰富的API帮我们去获取这些信息,请看详细代码:

获取 $_POST 变量: post() 方法

// 获取POST参数,传递属性名称就可以获取它对应的值
// vendor\topthink\framework\src\think\Request.php  961 行
public function post($name = '', $default = null, $filter = '')
{
        if (is_array($name)) {
            return $this->only($name, $this->post, $filter);
        }
 
        return $this->input($this->post, $name, $default, $filter);
}
 
 
// 获取变量,并且支持过滤 不管是get() post() 还是 param() 都依赖这个方法
// vendor\topthink\framework\src\think\Request.php 1241行
public function input(array $data = [], $name = '', $default = null, $filter = '')
{
        if (false === $name) {
            // 获取原始数据
            return $data;
        }
 
        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                [$name, $type] = explode('/', $name);
            }
 
            $data = $this->getData($data, $name);
 
            if (is_null($data)) {
                return $default;
            }
 
            if (is_object($data)) {
                return $data;
            }
        }
 
        $data = $this->filterData($data, $filter, $name, $default);
 
        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }
 
        return $data;
 }

获取 $_FILES 变量(上传文件信息):file() 方法

// 获取上传文件的信息,这里返回的是一个对象数组,每一个上传文件都会生成一个上传对象
// vendor\topthink\framework\src\think\Request.php 1128 行
public function file(string $name = '')
{
        $files = $this->file; //
        if (!empty($files)) {
 
            if (strpos($name, '.')) {
                [$name, $sub] = explode('.', $name);
            }
 
            // 处理上传文件
            $array = $this->dealUploadFile($files, $name);
 
            if ('' === $name) {
                // 获取全部文件
                return $array;
            } elseif (isset($sub) && isset($array[$name][$sub])) {
                return $array[$name][$sub];
            } elseif (isset($array[$name])) {
                return $array[$name];
            }
        }
}
 
// 上传文件对象创建 vendor\topthink\framework\src\think\Request.php 1175 行
$item[] = new UploadedFile($temp['tmp_name'], $temp['name'], $temp['type'], $temp['error']);
 
// 文件上传依赖 flysystem 组件这个我们后面再介绍

2.3 请求类型:

请求类型是指 HTTP 请求类型,HTTP 总共五种请求类型,分别是:GET、POST、PUT、 DELETE、 HEAD。Request 类为我们提供的接口分为两类,一类是获取当前请求类型,一类是判断是否是某种类型,接下为大家分析两个代表性API:

获取当前请求类型: method() 方法

// 获取请求类型 请求类型的获取支持类型伪造,如果伪造请求类型优先获取伪造类型
// vendor\topthink\framework\src\think\Request.php  717 行
public function method(bool $origin = false): string
{
        if ($origin) {
            // 获取原始请求类型
            return $this->server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this->method) {
            if (isset($this->post[$this->varMethod])) {
                $method = strtolower($this->post[$this->varMethod]);
                if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
                    $this->method    = strtoupper($method);
                    $this->{$method} = $this->post;
                } else {
                    $this->method = 'POST';
                }
                unset($this->post[$this->varMethod]);
            } elseif ($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
                $this->method = strtoupper($this->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
            } else {
                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
            }
        }
 
        return $this->method;
}

判断是否AJAX请求: isAjax() 方法

// 判断是否为 ajax 请求 主要是通过 HTTP_X_REQUESTED_WITH 属性来判断,只有ajax请求才会有这个属性
// ajax 请求同样支持类型伪造,并且有限获取伪造类型。
// vendor\topthink\framework\src\think\Request.php  1545 行
public function isAjax(bool $ajax = false): bool
{
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = $value && 'xmlhttprequest' == strtolower($value) ? true : false;
 
        if (true === $ajax) {
            return $result;
        }
 
        return $this->param($this->varAjax) ? true : $result;
}

2.4 请求头信息:

请求头信息就是指 HTTP 请求报文头,以 “属性名:属性值”的形式组织信息,服务端据此获取绝大部分客户端信息。Request 以两种方式获取此信息,第一种是从 $_SERVER 超全局数组中提取,第二种是 从 apache_request_headers这个方法中获取(前提是要采用Apache作为web容器),看代码:

// 在初始化方法中获取 vendor\topthink\framework\src\think\Request.php 307 行
public static function __make(App $app)
{
        $request = new static();
 
        // 只有在Apache 容器下才会有此方法
        if (function_exists('apache_request_headers') && $result = apache_request_headers()) {
            $header = $result;
        } else {
            // 没有的话从 $_SERVER 中提取
            $header = [];
            $server = $_SERVER;
            foreach ($server as $key => $val) {
                if (0 === strpos($key, 'HTTP_')) {
                    $key          = str_replace('_', '-', strtolower(substr($key, 5)));
                    $header[$key] = $val;
                }
            }
            if (isset($server['CONTENT_TYPE'])) {
                $header['content-type'] = $server['CONTENT_TYPE'];
            }
            if (isset($server['CONTENT_LENGTH'])) {
                $header['content-length'] = $server['CONTENT_LENGTH'];
            }
        }
}

请求信息可以通过 header 方法获取:

// 设置或者获取请求头 vendor\topthink\framework\src\think\Request.php 1221 行
public function header(string $name = '', string $default = null)
{
        if ('' === $name) {
            return $this->header;
        }
 
        $name = str_replace('_', '-', strtolower($name));
 
        return $this->header[$name] ?? $default;
}

2.5 请求缓存:

什么是请求缓存?就是将请求的内容缓存在客户端,下次请求的服务端,服务端只需响应状态码,无需响应内容,浏览器自动从缓存中读取,这样能大大提升用户体验。这么好的功能是如何实现的呢?其实是通过全局中间件实现,那如何开启呢?看代码:

<?php
// 第一步:将全局缓存中间件的注释去掉
// 全局中间件定义文件 app\middleware.php
return [
    // 全局请求缓存
     \think\middleware\CheckRequestCache::class,
    // 多语言加载
     \think\middleware\LoadLangPack::class,
    // Session初始化
     \think\middleware\SessionInit::class
];
 
// 第二步:将 路由配置中此项设置为 true
// config\route.php  30 行
 
    // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
    'request_cache_key'     => true,

接下来揭晓实现原理,代码如下:

    // 中间件入口函数 且只支持 get 请求
   public function handle($request, Closure $next, $cache = null)
   {
        if ($request->isGet() && false !== $cache) {
            $cache = $cache ?: $this->getRequestCache($request);
 
            if ($cache) {
                if (is_array($cache)) {
                    [$key, $expire, $tag] = $cache;
                } else {
                    $key    = str_replace('|', '/', $request->url());
                    $expire = $cache;
                    $tag    = null;
                }
 
                if (strtotime($request->server('HTTP_IF_MODIFIED_SINCE', '')) + $expire > $request->server('REQUEST_TIME')) {
                    // 读取缓存
                    return Response::create()->code(304);
                } elseif (($hit = $this->cache->get($key)) !== null) {
                    [$content, $header, $when] = $hit;
                    if (null === $expire || $when + $expire > $request->server('REQUEST_TIME')) {
                        return Response::create($content)->header($header);
                    }
                }
            }
        }
 
        $response = $next($request);
 
        if (isset($key) && 200 == $response->getCode() && $response->isAllowCache()) {
            $header                  = $response->getHeader();
            $header['Cache-Control'] = 'max-age=' . $expire . ',must-revalidate';
            $header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
            $header['Expires']       = gmdate('D, d M Y H:i:s', time() + $expire) . ' GMT';
 
            $this->cache->tag($tag)->set($key, [$response->getContent(), $header, time()], $expire);
        }
 
        return $response;
    }

2.6 响应类型:

响应类型是服务端响应客户端内容的形式,框架实现七种响应类型分别是 File、Html、Json、Jsonp、Redirect、View、Xml。其实就是七个响应类,都继承 Response 类,并且重写了部分父类的方法,下图为七个类定义的位置:

[图片上传失败...(image-38c9e8-1599632059740)]

  接下来分别介绍一下这个七个响应类所代表的含义:

  File : 通过修改响应头信息,实现文件下载。

 Html: 就是响应 html 页面,这是绝大数响应形式。

Json: 响应json数据,用于 API 响应。

Jsonp: 这是跨域请求响应。

Redirect : 这是重定向。

View: 这是响应视图本质还是html页面,这样做的好处就是无需调用视图里面 fetch 方法也可以渲染模板。

 Xml:这是响应 xml 数据。

2.7 响应输出:

响应输出其实在生命周期里面已经介绍过了,这里再补充几个点。响应输出就是调用 Response 的 send 方法,在这个方法里面有一个重要的操作就是获取输出数据,看代码:

public function send(): void
{
        // 处理输出数据
        $data = $this->getContent();
 
        // 省略若干代码
}
 
public function getContent(): string
{
        if (null == $this->content) {
            $content = $this->output($this->data);
 
            if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
                $content,
                '__toString',
            ])
            ) {
                throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
            }
 
            $this->content = (string) $content;
        }
 
        return $this->content;
}

output 方法,在除 Html 之外的六个类中都有被重写,这个方法决定输出内容的差异,这个给大家看一下 File 类的 output 方法。代码如下:

// 此代码实现了一个文件下载功能 vendor\topthink\framework\src\think\response\File.php
protected function output($data)
{
        if (!$this->isContent && !is_file($data)) {
            throw new Exception('file not exists:' . $data);
        }
 
        ob_end_clean();
 
        if (!empty($this->name)) {
            $name = $this->name;
        } else {
            $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : '';
        }
 
        if ($this->isContent) {
            $mimeType = $this->mimeType;
            $size     = strlen($data);
        } else {
            $mimeType = $this->getMimeType($data);
            $size     = filesize($data);
        }
 
        $this->header['Pragma']                    = 'public';
        $this->header['Content-Type']              = $mimeType ?: 'application/octet-stream';
        $this->header['Cache-control']             = 'max-age=' . $this->expire;
        $this->header['Content-Disposition']       = ($this->force ? 'attachment; ' : '') . 'filename="' . $name . '"';
        $this->header['Content-Length']            = $size;
        $this->header['Content-Transfer-Encoding'] = 'binary';
        $this->header['Expires']                   = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT';
 
        $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT');
 
        return $this->isContent ? $data : file_get_contents($data);
}

2.8 响应参数:

所谓的响应参数就是指响应内容,状态码,响应头。虽然这些信息可以统一设置,同时 Response 也提供了单独的方法就设置这些内容,看代码演示:

响应内容

// 输出数据设置
public function data($data)
{
        $this->data = $data;
 
        return $this;
}

状态码

// 设置 HTTP 状态码
public function code(int $code)
{
        $this->code = $code;
 
        return $this;
}

响应头

// 设置响应头
public function header(array $header = [])
{
        $this->header = array_merge($this->header, $header);
 
        return $this;
}

2.9 重定向:

重定向是属于响应类型的一种,这里就不多做介绍了,直接看框架的实现代码:

// 实现就是通过 location实现的 这里的 $data 为 url
protected function output($data): string
{
        $this->header['Location'] = $data;
 
        return '';
}

各位读者,关于请求与响应的介绍到这里就结束了,感谢大家的阅读,如果对于文字教程意犹未尽,可以移步下行地址观看视频教程,并且可以本人一对一交流。

教程地址:https://edu.csdn.net/course/detail/28045

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343