仅将此文献给我的身边的盆友们
其实很早之前就想写一篇关于POST,GET,PUT等操作与区别的文章了,不过因为一直比较懒,所以也就搁置了(比如我那篇“深入浅出正则表达式”的中级和高级篇>_<)。正好前几天在研究http协议,加上今天基友问我关于DELETE的事,所以就简单的把所学所感写一下,一方面对自己这部分知识进行总结巩固,另一方面也与大家分享一下,共同探讨,少走弯路。
不过毕竟我这方面不是专家,才疏学浅,难免有纰漏之处,欢迎各位深♂入♂交♂流。
首先HTTP协议又称为超文本传输协议(HyperText Transfer Protocol)我想对于这个名字大家应该都不陌生,不过我们是否真的了解它呢?因此我们的旅程就先从HTTP协议的本质开始。
了解HTTP协议
以下HTTP协议如无特殊说明均是指HTTP1.1版本。
首先它是一个基于TCP协议之上的应用层协议,因此我们可知他是面向连接且可靠的,其本质上也依旧是一个客户端与服务端的数据交换协议。之所以强调这点,是因为我们接下来要好好分析并简单的实现这个协议。
我们先看看HTTP请求时的协议
我们以假设访问必应为例:
GET / HTTP/1.1
Host: cn.bing.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=2&tn=baiduhome_pg&wd=spdy%E5%8D%8F%E8%AE%AE%20%E6%A0%BC%E5%BC%8F&rsv_spt=1&oq=spdy&rsv_pq=dfc97fa700000dd8&rsv_t=e716puVCApL3dR3dlOHENmH%2FRlQDknsRn%2Bu6oO%2B3KWaPgak8F4ZDiuJJPOT0LioH2PvW&rqlang=cn&rsv_enter=1&rsv_sug3=9&rsv_sug1=5&rsv_sug7=100&sug=spdy%25E5%258D%258F%25E8%25AE%25AE&rsv_n=1&rsv_sug2=0&inputT=6138&rsv_sug4=7363
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: **************************************************************************************
这里请允许我把cookie以*代替,不过这不影响我们的分析。这就是我们标准的HTTP请求的协议头。
标准的HTTP请求协议分为三个部分,请求行,消息报头和请求正文。如上所示,其中GET / HTTP/1.1就是请求行,从Host到Cookie结尾就是报头,因为这里没有要传递的参数,所以请求正文为空————准确的说是GET方法是不应该有请求正文的,如果你加上之后有的服务器会返回400错误————其中从前两部分我们一般称为协议头。
特别注意:请求报头与请求正文之间是有一个空行的,这是用来区分协议头与协议体的,即使没有协议体这个空行也是必须的。请务必谨记。
如果你之前做过爬虫的话应该对其中几个字段并不陌生,比如User-Agent,Referer,cookie等,这些都是常用的请求消息报头。
下面我们挑选几个常用的解释一下:
Connection:这是在HTTP1.1中才加入的,主要用来持久连接。因为在HTTP1.1之前的HTTP协议都是发送一次请求连接一次TCP,也就是说你每打开一个页面都要重新进行三次握手,这显然是不合适的。为此新添加了这个报头用来保证每次连接可以保持,直到客户端长时间失去响应或者主动发送close才断开连接。
User-Agent:就是著名的UA了,它一般标识了客户端的类型,当我们写爬虫时常需要伪造这个报头让服务器以为我们是正常浏览器,当然这也是服务器端常用来判断电脑还是移动设备的标志。
Referer:来源链接,一般可以爬虫用来伪造来源,服务器端生成回退链接,也可以用来统计网站的流量来源,或者简单的防盗链。
Accept-Encoding:声明客户端支持的编码类型,主要用于压缩数据,比如当你的客户端支持gzip压缩,那么如果服务端也支持,那么服务端就会发送压缩后的信息给你,此时就可以减少网络流量,速度也就越快。
Cookie:这就不提了,如果你做过web开发一定会碰到它。
说的再多也只是理性认识,不如感性认识来的强烈。所以我们现在就不妨来写一个代码看看效果,简单的实现一下发送http协议。
$opt=array(
'http'=>array(
'method' =>'GET',
'header' =>"Host: cn.bing.com\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nReferer: http://cn.bing.com/search?q=HTTP%E6%A0%BC%E5%BC%8F&qs=n&form=QBRE&sp=-1&pq=http%E6%A0%BC%E5%BC%8F&sc=0-6&sk=&cvid=A16E041104AA4F9EBFA671C7BCEB1EED\r\nAccept-Language: zh-CN,zh;q=0.8\r\n",
)
);
echo @file_get_contents('http://cn.bing.com/search?q=HTTP&qs=n&form=QBRE&sp=-1&pq=http&sc=8-4&sk=&cvid=D301560748E84375A67150CF9025FA61',false,stream_context_create($opt));
print_r($http_response_header);
上面这段代码就是向必应服务器发送一段HTTP协议的报文,查找内容为HTTP的有关信息页。
也许你会问这不是和我写PHP抓取网页一样吗?嗯,恭喜你答对了。不过理解上要改一下,不是你写代码去抓取这个网页,而是你发送了一个HTTP的报文然后服务器端响应之后返回一个HTTP报文。通俗的说是你给了对方一张兑换券,然后对方把兑换券的商品返回给你,而不是你凭空去那边拿一个商品。没错,这就是HTTP协议的作用和本质。
我之所以强调这点是为了突出其与客户端的无关性。因为我们常用浏览器去访问网页,其实本质上是浏览器提我们发送了这个HTTP协议,但同样的其实我们可以使用PHP的命令行解释器完成同样的事,并没有任何区别,除了命令行没有渲染最终的结果。我也碰到过不少程序员对HTTP协议仅仅只把概念停留在浏览器上,记得有一年面试,因为当时我对SOAP函数并不是很熟(当然这是我自己的问题),然后说简单的SOAP完全可以基于HTTP协议去完成,你发送一个对方需要的报文,得到之后再解析就行了。当时面试官一脸不屑的表示不可能,HTTP协议只是浏览器实现的,你服务端即使实现也会很复杂,当我说迅雷也是基于HTTP协议的时候对方表示已经没兴趣再听了╮(╯▽╰)╭
如果你还觉得有些不置可否,可以试试修改一下上面的UA,或是加上Accept-Encoding: gzip, deflate, sdch,你会发现结果是不同的。
讲完了请求,我们就要讲一下响应了。同样我们以刚才访问必应首页的请求头来看它的响应结果。
HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/8.5
P3P: CP="NON UNI COM NAV STA LOC CURa DEVa PSAa PSDa OUR IND"
X-MSEdge-Ref: Ref A: CA0326D9064C462E81D4E49BF6C2C64A Ref B: BJ1SCHEDGE0117 Ref C: Tue Feb 28 06:38:48 2017 PST
Date: Tue, 28 Feb 2017 14:38:47 GMT
Transfer-Encoding: chunked
从上面我们可以看出HTTP响应报文和请求报文几乎是一样的,它也是由三部分构成————状态行,消息报头,响应正文。
与之前一样HTTP/1.1 200 OK是状态行,Cache-Control到Transfer-Encoding结尾是消息报头,而在其之后的一个空行之后的就是响应正文了。值得注意的是,响应的状态行并不区分你的请求方法,无论你是GET还是POST或是其他的最终返回的格式都是HTTP/版本号 响应代码 代码描述。
接下来我们也挑选几个比较常用的消息报头来讲解一下
Content-Type:最常见的响应报头,用于返回响应体的格式,如果你还记得请求报文时我们填写的Accept的话这里就是返回客户端请求所能接收的格式。
Content-Encoding:响应返回编码。与请求时的Accept-Encoding相对应。
Location:用于重定向客户端到一个新的位置,常见的就是PHP中用这个做内部跳转。
Server:返回服务器的有关信息
Date:生成响应的日期和时间。需要注意的是响应时间是 Tue, 28 Feb 2017 14:29:24 GMT 这样的格林尼治时间。主要用于缓存等优化。
Vary:主要用于缓存服务器,比如当你的缓存服务器需要同时提供Gzip压缩或者非压缩版本的时候,明确告知缓存服务器要根据Vary所定义的值来区分保存的版本。
和之前的请求一样,说的多总没有实践来的直观,因此我们在这里简单的实现一下HTTP的响应。
//服务器端
header('HTTP/1.1 200 OK');
header('Cache-Control: private, max-age=0');
header('Content-Type: text/html; charset=utf-8');
header('Vary: Accept-Encoding');
header('P3P: CP="NON UNI COM NAV STA LOC CURa DEVa PSAa PSDa OUR IND"');
header('X-MSEdge-Ref: Ref A: CA0326D9064C462E81D4E49BF6C2C64A Ref B: BJ1SCHEDGE0117 Ref C: Tue Feb 28 06:38:48 2017 PST');
header('IWANTHAVEAHEADER: this is a test header');
echo "\r\n";
echo " Hello World\r\n";
echo "\r\n";
//客户端
$opt=array(
'http'=>array(
'method' =>'GET',
'header' =>"Host: localhost\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nReferer: http://cn.bing.com/search?q=HTTP%E6%A0%BC%E5%BC%8F&qs=n&form=QBRE&sp=-1&pq=http%E6%A0%BC%E5%BC%8F&sc=0-6&sk=&cvid=A16E041104AA4F9EBFA671C7BCEB1EED\r\nAccept-Language: zh-CN,zh;q=0.8\r\n",
)
);
echo @file_get_contents('http://localhost/response.php',false,stream_context_create($opt));
print_r($http_response_header);
客户端除了修改一下host为localhost其他均保持不变。而服务器端我们主要工作就是输出一个标准HTTP响应报文,其中我还加了一个自定义报头IWANTHAVEAHEADER,你可以在客户端使用$http_response_header变量来获取响应头。我想经过之前请求的分析你现在也应该可以自行分析响应报文了。你可以尝试一下将状态码改成404或者302之后会发生什么。
基本上HTTP的请求与响应部分讲的差不多了,现在让我们思考两个问题来温习一下
1、请解释为什么PHP里面header函数之前不能有任何输出,为什么单个纯PHP文件推荐不应该加入结尾标签,我们常说BOM头会带来很多问题,请问为什么?
2、还有很多我没有提到的报头,请尝试去添加并感受区别。
既然知道了HTTP的原理,那么我们是不是可以自己开发一个极简WEB服务器呢?答案是当然可以。不过因为篇幅限制,所以这边就不展开了,引用我之前的文章 50行实现简易HTTP服务器,大体思路是一样的,首先建立一个TCP连接,然后监听端口,然后将请求转发到我的服务器,最后输出响应。
PHP关于HTTP请求的实现
这部分属于选读内容,本来我是不想加的,毕竟这块我自己也不是特别熟,但某位基友强烈要求把拍黄片的实现加上去,这样显得有(neng)深(zhuang)度(bi)。所以就先简单加一下,等以后底层能更熟练的时候再完善一番。如果你直接跳过这部分其实也没有什么问题。
这里主要讨论在Apache下PHP的实现原理,也就是将PHP作为Apache模块加载时所运行时的工作原理,因为这个相对来说比较简单。
这边主要以分析5.6的源码为主,7或许以后等我会了再补充。
众所周知,我们的PHP一般时作为Apache的一个动态模块来运行的,那么它究竟是怎么工作的呢?让我们一步步来。首先Apache启动之后会会读取配置文件,假设这时候它读到了
LoadModule php5_module modules/mod_php5.so
的时候会根据模块名mod_php5去寻找相应的模块,然后再加载。而对于每一个Apache模块,其命名必须是“mod_”的格式,如果格式不对,则认为是非法模块。当在相关路径下找到该模块后便将其加载到内存之中。既然知道了模块名,那么我们就可以顺藤摸瓜在PHP的sapi下面去找mod_php5.c文件了。
你如果是PHP5的话或许还可能还要考虑一下应该在apache2,apache2handler,apache2filter,apache_hooks中的哪一个下的mod_php5.c,如果你是7的话就只有apache2handler目录了。因此这便是我们所需要的目录。于是当你兴冲冲的打开/sapi/apache2handler/mod_php5.c
时一定是像这样子Σ(っ °Д °;)っ一脸懵逼。
/*
+----------------------------------------------------------------------+
| PHP Version 5 |
+----------------------------------------------------------------------+
| Copyright (c) 1997-2016 The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| [email protected] so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Sascha Schumann |
| Parts based on Apache 1.3 SAPI module by |
| Rasmus Lerdorf and Zeev Suraski |
+----------------------------------------------------------------------+
*/
/* $Id$ */
#define ZEND_INCLUDE_FULL_WINDOWS_HEADERS
#include "php.h"
#include "php_apache.h"
AP_MODULE_DECLARE_DATA module php5_module = {
STANDARD20_MODULE_STUFF,
create_php_config, /* create per-directory config structure */
merge_php_config, /* merge per-directory config structures */
NULL, /* create per-server config structure */
NULL, /* merge per-server config structures */
php_dir_cmds, /* command apr_table_t */
php_ap2_register_hook /* register hooks */
};
/*
* Local variables:
* tab-width: 4
* c-basic-offset: 4
* End:
* vim600: sw=4 ts=4 fdm=marker
* vim<600: sw=4 ts=4
*/
对的,这就是整个文件,除了一个结构体,什么都没有。不要着急,你看一下结构体中的最后一个php_ap2_register_hook,这便是我们所需要的。它提供了将PHP SAPI模块与Apache模块连接起来的工作。这个函数在sapi_apache2.c中实现如下
void php_ap2_register_hook(apr_pool_t *p)
{
ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE);
}
其中post_config挂钩的作用就是启动PHP,它通过php_apache_server_startup函数调用sapi_startup启动sapi进而启动PHP来执行相应操作。那么接下来我们继续按着这条线索向下挖掘,会发现apache2_sapi_module结构,其中便是Apache服务器专属的方法(关于sapi_module_struct结构体的功能及作用可以参考PHP SAPI的相关内容,这里就不再展开了)。
static sapi_module_struct apache2_sapi_module = {
"apache2handler",
"Apache 2.0 Handler",
php_apache2_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
NULL, /* activate */
NULL, /* deactivate */
php_apache_sapi_ub_write, /* unbuffered write */
php_apache_sapi_flush, /* flush */
php_apache_sapi_get_stat, /* get uid */
php_apache_sapi_getenv, /* getenv */
php_error, /* error handler */
php_apache_sapi_header_handler, /* header handler */
php_apache_sapi_send_headers, /* send headers handler */
NULL, /* send header handler */
php_apache_sapi_read_post, /* read POST data */
php_apache_sapi_read_cookies, /* read Cookies */
php_apache_sapi_register_variables,
php_apache_sapi_log_message, /* Log message */
php_apache_sapi_get_request_time, /* Request Time */
NULL, /* Child Terminate */
STANDARD_SAPI_MODULE_PROPERTIES
};
至此整个Apache加载PHP及调用的过程就结束了,接下来就是PHP内部处理以及返回给前台Apache后输出。可以看到PHP只是在服务器响应的时候才进行相关处理,而在之前及之后都还是在Apache中处理的。
对于有兴趣的可以继续参考鸟哥的PHP的GET/POST等大变量生成过程里面介绍了GET,POST等变量的处理过程。
关于GET、POST等请求方法
终于把基础的HTTP协议理清楚之后就应该轮到我这次写这篇文章的主要目的,就是理清楚GET,POST,PUT,DELETE,HEAD和OPTION等这几个的联系与区别,以免总是被人弄错。
这次主要涉及的就是在RFC2616中定义必须实现的8个方法:
1. GET 获取资源
2. HEAD 与GET类似,但仅返回报头
3. POST 添加及更新资源
4. PUT 添加及更新资源
5. DELETE 删除资源
6. CONNECT 动态切换代理
7. OPTIONS 获取服务器相关信息
8. TRACE 跟踪调试
其中前5项是我们与服务器端进行交互的常用操作,其中1,3,4,5就是对服务器端资源的增删改查。
其中RFC2616规定了,对于仅仅获取资源而不会执行其他操作产生副作用的行为,应该视为安全的,比如GET,HEAD,而POST,PUT以及DELETE这些方法可能会执行不安全的行为。
但因为要保证服务器执行而完全不产生副作用是不可能的,为此我们需要引入一个概念————幂等性。它用于描述当某一次操作执行一次或多次时所产生的副作用是完全相同的,即你执行一次与你执行N(N>0)所产生的影响没有任何区别。
因此RFC2616规定了方法GET,HEAD,PUT,DELETE在实现时必须是幂等的,而OPTIONS和TRACE因为本身就不应该产生副作用,所以也具有内在的幂等性。
看了上述文档,我想你也应该有一定的了解了,大概可以区分他们的不同,那么现在让我们一步步剖析,那么就从最常见的GET与POST的区别开始。
这几乎是web面试时面试官必问的一个问题,几乎80%的几率都会问道,但有时候往往他们的答案是错的,这其实从侧面反映了很多面试题其实都是网上抄的( ﹁ ﹁ ) ~→
从上面的文档中我们可以知道其有三点不同:
1.语义不同,即GET一般用于获取资源,POST一般用于添加及更新资源。
2.幂等性不同,即两者产生的副作用的后果影响不同。
3.HTTP协议中GET没有请求体,而POST有请求体
我之所以说一般用于是因为很多情况下人们并不遵守这个规定,比如说我猜想不少人应该写过
http://example.com/id/1/change_status/1
http://example.com/id/1/change_status/2
这样的操作。显然这其实是不符合GET语义的,因为每次GET请求都不应该产生任何副作用,但这个函数就没做到,每次运行之后都会修改状态,毕竟可以减少建立表单的工作。解决方法的话可以使用ajax,但需要注意的是当使用jQery等插件之后应该选用其POST方法而不是GET。
其次幂等性不同主要表现在于当你多次执行POST请求之后副作用是累计的,比如example.com/article
这样的请求,当你执行3次之后就理应有3篇一模一样的文章新增。而这正是POST非幂等所表现出来的。
看到这儿我想网上经常说POST与GET的安全性问题你也有答案了吧,其实这两个安全性上没有任何区别,就好像一个是把一万块钱直接放在桌子上直接等人拿,一个是拿张报纸把一万块钱包起来放桌子上,你觉得安全性有差吗?
说完了GET和POST,那么我们接下来谈谈PUT与POST,其实这两个的功能几乎是完全一样的,唯一的区别就是幂等性。所以理论上你不断调用PUT只会上传一个资源或更新同一个资源。之所以不同是因为POST所对应的URI并非创建的资源本身,而是资源的接收者;而PUT所对应的URI是要创建或更新的资源本身。前者类似于
http://example.com/article
而后者类似于
http://example.com/article/id/1
前者会不断的创建新的资源,而后者则创建或更新id为1的资源
所以你其实会发现你平常POST所做的工作其实是在执行PUT,比如常见的防止订单反复提交,对同一个操作自动判断是更改还是新增等等,这也是为什么我们平常开发增删改查一般只用到POST的原因。
接下来要说说DELETE,因为这个有必要单独说一下。第一它返回的状态比较与众不同,其次就是关于它本身HTTP报文的格式。
之所以说其返回与众不同是因为当我们一般服务器响应200信息的时候就代表此次响应已经结束,其操作以及操作所带来的副作用也已经产生了,但DELETE却不是这样,如果其只返回200并不强制其要完成删除操作,因为RFC2616的定义是
This method MAY be overridden by human intervention (or other means) on the origin server. The client cannot be guaranteed that the operation has been carried out, even if the status code returned from the origin server indicates that the action has been completed successfully.
也就是说客户端不能保证此操作能被执行,即使源服务器返回成功状态码。当然后面建议是如果未能将其移除,则不应该
返回成功。也就是说其并非一个强制要求。
因此随后定义如果响应里包含描述成功的实体,响应状态应该是200(Ok);如果DELETE动作已提交未通过,则应该以202(已接受)响应;如果DELETE方法请求已经通过了,但响应不包含实体,那么应该以204(无内容)响应。
记得之前实验试过,你如果使用GET请求的时候是不应该有请求体的,否则有可能服务器会抛出一个400错误,对于DELETE也是一样的,其是否可以有请求体是不确定的,视你的服务器支持而定。因为DELETE其具有两面性,一方面其应该和PUT一样,那么应该是可以带有请求体参数,但另一方面DELETE又有其特殊性,因为当其顺利执行之后其实资源已经不存在了,具有破坏性,那么也就是说其每次执行只应该针对同一个资源,那么其请求体参数完全是无必要的。所以现在对于服务器来说建议是会忽略其请求体,但也不排除其会将其完整的请求体发送给其他处理程序的可能,这完全看服务器的实现而定。
常用的几个已经讲完了,下面呢也简单的介绍几个不太常用的。
其中最简单的就是CONNECT和HEAD了,前者的作用仅仅只是为了动态切换代理,而后者也只是为了获取响应头。
然后OPTIONS就相对来说有些复杂,它的主要作用是
The OPTIONS method represents a request for information about the communication options available on the request/response chain identified by the Request-URI. This method allows the client to determine the options and/or requirements associated with a resource,or the capabilities of a server, without implying a resource action or initiating a resource retrieval.
简单说就是通过该请求去获取服务器信息以及请求资源应该使用的选项。翻译成人话就是指通过该请求知道访问该URI资源应该使用POST还是GET或者其他请求方法,同时是否还需要其他自定义报头等信息。它提供了一种不需要直接访问资源就能获取信息的方式。
TRACE则是用于让客户端发送一个请求,看其是否能正确返回一个响应,用于测试消息回路。其最后的接收者可以是接收请求中Max-Forwards为0的源服务器或第一台代理服务器或网关,因此它可以作为请求链的跟踪信息。另外需要注意的是TRACE是不能有请求体。
当然实际开发中你会发现并不是所有的方法都需要用到,有时你甚至会觉得几乎99%的web操作其实都可以交给POST和GET完成。你完全可以用自己的业务逻辑用POST来实现PUT,用GET来实现DELETE。对的,这不会产生任何问题,只是不一定合乎规范而已。那么是不是有一种方法可以合乎规范又无需修改太多呢?答案就是下一节要提到的RESTful。
我想到这里如果你已经把这个关系理顺了,那么我想我写这篇文章的目的也达到了,如果依旧没有,那么还是看看文档吧/(ㄒoㄒ)/~~
关于RESTful
RESTful是一个近几年比较常见的一个词语,它随着API的成熟而开始大规模应用并进入我们的视野中。所以我们今天最后的部分就来谈谈RESTful。
首先需要明确的是RESTful不是一种开发模式或开发标准,而是一种软件设计风格。它的核心是将所有的软件功能及服务都看作是一种资源,即每个URI都代表一种资源。而通过POST,GET,PUT,DELETE等操作完成对该资源的增删改查操作。你可以把你的功能想象成一个具体的页面,那么接下来无论你是对其做任何操作就和操作实体文件一样,这样的好处就是为服务器提供了一个统一的管理方式,而不会产生其他歧义。那么究竟什么是RESTful风格呢?下面让我们一步步来具体解释。
REST的全称是Representational State Transfer,而如果一个软件的设计风格符合REST,那么我们就将其称之为RESTful。
REST这个词现在似乎并没有什么特别合适的中文名称,阮一峰对其的翻译为“表现层状态转化”,那么这个看上去有些茫然的中文究竟该怎么解释呢?首先它有两个关键点,一是表现层,二是状态转化。
那么什么是表现层呢?记得上面说过REST的设计核心就是将所有的URI都看作是资源,那么对于我来说无论你是txt,html还是pdf,其实只要是同一个URI,那么资源都应该是一样的,仅仅只是“呈现”出来的形式不一样。比较通俗的讲,比如我将一张图片的二进制数据存储在服务器上,那么无论我输出到客户端上的是一张jpg,还是一段二进制数据,又或是一段base64的字符串,其实都是同一个数据,唯一的不同只是客户端的显示方式不同。那么是什么规定了客户端的显示方式呢?还记得之前说过的Accept和Content-Type吗?就是这两个报头。因此我们把"资源"具体呈现出来的形式,叫做它的"表现层"。
那么什么又是状态转化呢?标准HTTP协议是无状态的,这也就意味着服务器和客户端并不知道各自的实际情况,也就不可能有上下文关系。比如你请求多次同一个URI都应该视为是互相独立的新请求,HTTP协议本身并不会知道你这是第几次请求同一个URI,又或者是当你在服务器上用FTP上传一个文件之后并不会直接反映到你的客户端上。所以必须得有方法能使客户端与服务器发生状态交互,这就是状态转化。比如你使用GET才能知道服务器上新增加了文件,你用DELETE直接删除文件等。因为这种转化是建立在表现层之上的,所以就是"表现层状态转化"。
因此我们可以认为REST其实就是指只通过HTTP协议中的GET,POST,PUT和DELETE方法对某一个URI资源进行交互的一种软件设计模式。
所以我们以上面更改状态为例,按RESTful方式设计的API报文可以是如下样子
PUT /status HTTP/1.1
Host: example.com
...id=1&change=1
它代表了使用PUT方法请求到example.com/status ,然后将id为1的资源的状态修改为1。需要注意的是RESTful方式因为每个URI都代表一个资源,所以设计的时候不应该使用动词,而使用名词。所以比如开启活动不应该取名openActivity,而应该使用PUT example.com/activity 然后将open作为一个参数传递进去。
可能这时有人会问既然之前说过我们99%的操作都能使用GET和POST来完成,那么我们是否有必要使用PUT和DELETE来完成呢?在我看来如果有可能的话,应该尽量使用这些方法来实现RESTful的设计。除了保证之前所说的操作的一致性,其中还有一个最大的优点就是在于假设服务器完全支持这些方法的话,对于你来说可以无需在意你的实现是否具有幂等性,因为这件事服务器已经为你做好了。假设以常见的提现操作为例,假设你想要实现幂等性操作需要进行两步,第一步创建ticket_id并放入session中,然后POST到后台之后使用
withdraw($ticket_id, $account_id, $amount);
当每次提交到后台时首先判断$ticket_id是否在session中存在,如果存在那么执行提现操作,然后删除该session,如果不出现那么就直接返回提现失败。这样就是一个简单的幂等设计。
但假设如果这个创建ticket_id和验证的工作都已经由服务器来替你完成了是不是很爽?重复的提交可能完全都不会传递到你的后台程序,你后台只要接收就认为是新的提交,这样无疑会减轻很多开发负担不是吗?因为对于web服务器来说你提交由后台的程序所产生的对资源操作应该与其本身对资源的操作是相兼容的。比如当你客户端连接服务器的时候服务器自动给你发送一个ticket_id然后你请求时客户端将这个ticke_id通过报头发送到服务器,是不是就很舒服了?就像客户端发送普通报头一样简单。
当然前提是服务器支持,但现实有时候并没那么舒服,就像我当时看《图解TCP/IP》那书时全篇都在讲IPv4是个渣,IPv6大家都会笑哈哈,但是就因为某些原因所以一直没有全面推行,你有什么办法呢?┑( ̄Д  ̄)┍
所以现在的情况往往是服务器不做特殊处理,而是交由解释程序交给开发者自行判断你是不是要完成幂等,前者的方法对于开发简单,而后者的话交给开发者的自由控制权更大,孰优孰劣可能现在也不好一时下判断。或许时间最终会给出答案的吧。
结语
基本上我这次要讲的也说的差不多了,我们从基本的HTTP协议一直讲到了RESTful设计,应该算是比较基础的一些概念性的东西,但有时候还是要稍微梳理一下才能温故而知新的,这也是我写本文的重要目的了。其中有不少也是我自己的理解加感悟,当然难免会有不少错漏之处,欢迎大家能交流指正。
最后我想说,作为一名专业的程序员,怎么可能会写不完博客而女装,告诉你飞龙骑脸怎么输!
参考文献
HTTP 协议入门,by 阮一峰
PHP的GET/POST等大变量生成过程,by Laruence
TIPI:深入理解PHP内核——Apache模块
[PHP核心技术与最佳实践],by 列旭松,陈文
rfc2616
HTTP幂等性概念和应用,by Todd
HTTP GET with request body
Is an entity body allowed for an HTTP DELETE request?
理解RESTful架构,by 阮一峰