PHP回顾系列目录

PHP作为为web开发而生的语言,在web开发中是一把便捷顺手的利器。将web流程中的细节梳理清楚,在实际工作中能起到提升效率和快速定位问题的作用。

本篇将简要介绍组成web请求的url(网址)、header(头部)和body(正文)三个部分,以及对应概念的PHP操作。

注: 本文所述从用户和开发者角度出发,与HTTP协议规范定义的格式描述层面不同。

web请求

请求url

请求的第一步是资源的url,例如 https://itlanyan.com/about。url包含以下信息:

  1. 协议, http或https。
  2. 端口,http默认是80, https默认443。https://itlanyan.com/about的效果同https://tlanyan.pp.ua:443/about。
  3. 主机名,例如tlanyan.pp.ua。
  4. 请求字符串。请求网址如:https://www.baidu.com/s?ie=utf-8&wd=flower,则请求字符串为 id=utf-8&wd=flower。请求字符串是网址的重要组成部分,动态页面会根据请求参数返回不同的结果。

浏览器 不会发送锚链接 到服务器。用curl等工具发送锚链接信息到服务端,默认被过web服务器滤掉。开发中如果需要锚链接的值,建议转换成namr=value的请求字符串,服务端捕捉后再进行处理。

PHP的几个超全局变量包含了url的所有信息,相关的超全局变量有:$_SERVER, $_GET,$_REQUEST。以下是获取网址信息的方式:

1.  $_SERVER['HTTPS']为空,表示用的https协议,否则是http协议;
2.  $_SERVER['SERVER_PORT']可以获取到服务器监听端口;
3.  $_SERVER['SERVER_NAME']可以获取请求主机名;
4.  $_GET数组包含了请求字符串的键值对。

url是了解用户请求的第一步。例如: 为了安全,将http请求重定向到https;对请求主机名进行判定,如果请求主机名非限定的域名,则拒绝:

$serverName = $_SERVER['SERVER_NAME'];

// 将http流量转到https
if (isset($_SERVER['HTTPS']) {
    header("location: https://{$serverName}:{$_SERVER['SERVER_PORT']}{$_SERVER['REQUEST_URI']}");
    exit;
}

// 限定请求主机,可以防止恶意ip扫描带来的旁站攻击
if ($serverName !== "tlanyan.pp.ua" || $serverName !== "tlanyan.pp.ua") {
    exit("please visit with server name");
}

站点中动态页面的大部分业务逻辑都需要$_GET中的数据作为参数输入,所以$_GET数组的重要性不言而喻。静态页面一般不需要参数,有无$_GET参数影响不大。

请求header

url指示资源的位置,header提供额外的参数和选项。常见的header选项有:

  • User Agent(UA),用户代理字符串,指示当前用户使用何种浏览器/工具访问页面。常见的用户代理包括浏览器、curl工具以及各种爬虫;
  • Content-Type,请求的文档类型。常见的文档类型包括form表单、json和xml;
  • referrer,指示用户由哪个页面引导而来;
  • cookie,请求携带的cookie。由于http是无状态的协议,对于认证的会话,一般需要携带PHPSESSID, JSESSIONID等认证信息;
  • Authorization,认证信息,内容包含用户名和base64后的密码。

其他header信息还包括accept/accept-encoding,以及各类自定义头部等。

头部信息为请求提供了额外的数据,让服务端程序得以了解更全面的请求信息。例如可以根据UA返回适用于IE/chrome/firefox浏览器的页面,或者根据UA跳转桌面版/移动版/wap版,以改善体验;referrer头部可用来鉴定页面来源是否合法,资源是否被盗用;对于POST/PUT等更新类请求,服务端程序需要根据Content-type来确定请求正文的类型,从而正确解码和反序列化。

站在开发角度上,PHP如何获取头部信息?

配合apache使用的PHP程序,可用 getallheaders 函数获取所有的头部信息;如果安装了PECL的http拓展,可使用 http_get_request_headers 函数获取所有头部。如果以nginx+php-fpm的方式运行PHP程序,则需要从 $_SERVER 超全局变量中获取。$_SERVER超全局变量中以 HTTP_ 为前缀的键值信息即是解析到的HTTP头部,下面是一个从 $\_SERVER 超全局变量中提取http头部的函数示例:

function getAllHeaders()
{
    $headers = [];
    foreach ($_SERVER as $name => $value) {
        if (strncmp($name, 'HTTP_', 5) === 0) {
            $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
            $headers[$name] = $value;
        }
    }
    return $headers;
}

头部中的cookie信息是web开发中必须掌握的概念,计划今后单独开篇详谈, 此文中略过。

头部中的Content-type是另外一个重要的概念,一般出现在POST/PUT/PATCH等更新类的资源请求中。常规的form表单请求,内容类型的值为: “application/x-www-form-urlencoded”,即以url方式对参数键值进行解码。文件上传时,form表单需要设置 enctype=”multipart/form-data” 属性,对应的内容类型为 “multipart/form-data”。另外SOAP请求中常见 “application/xml”,Restful API中则常见 “application/json” 的内容类型。非常规form表单和文件上传请求,web服务器一般不能正确处理请求正文,需要手动在程序中对程序进行解码。

请求body

浏览器和服务器对请求的url的长度一般是有限制的,类似于文件上传的请求,把文件内容放到url上再发送到服务端的做法难以让人接受;另一方面,url的信息会直接显示在地址栏上,认证请求中的用户名和密码如果附加在url上会让人直接一览无余,直观上的安全性不强。对于类似的请求,将请求正文主题放在专用的正文字段中,显得合理且必要。

常见的form表单请求(包含文件上传),PHP内置了直接的支持。表单字段可以使用 $_POST, $_REQUEST 超全局变量获取值,上传的文件通过 $_FILES 获取。开发中常会接触到这几个超全局变量,以获取用户提交过来的数据。

其他非表单形式的请求,需要从 $HTTP_RAW_POST_DATA(仅限PHP7之前)或者 “php://input”(推荐方式) 中读取。 例如SOAP业务的交互,基本上以xml为交互内容载体,则需要先提取出请求的xml内容,然后解析。以下是将xml请求解析成数组的函数示例:

function getRequestData()
{
    $data = file_get_contents("php://input");

    $xml = simplexml_load_string($data);
    $json = json_encode($xml, JSON_UNESCAPED_UNICODE);
    return json_decode($json, true);
}

Restful API中常用json作为数据交换载体,也需要做类似的解析。

其他

一个常见的问题,用户端的IP如何获取?

HTTP是TCP上层的应用层协议,而TCP是可靠的端对端通信协议。客户端和服务器在通信链路建立时,需要知道对方的IP和通信端口。web服务器收到请求后,转发请求到PHP端时会传递IP相关信息,PHP可以从 $_SERVER 超全局变量中读取。以下是函数示例:

public function getUserIP()
{
    return isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] :  isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
}

更多环境变量可以参考 $_SERVER 超全局变量。

总结

了解以上web请求的三部分,可以帮助web开发人员掌握用户请求的详细信息,为后续的业务逻辑处理扫清第一道障碍。