HTTP协议漫谈 - HTTP协议请求方法

前言

在上一篇文章《HTTP协议漫谈 - HTTP协议历史和报文结构》中介绍了HTTP协议的历史和版本变化,以及HTTP协议报文的总体结构。

按照HTTP/1.1 RFC文档中的定义,HTTP报文包括起始行,头域和消息体三个部分。其中起始行又分为请求行和状态行,请求行是HTTP请求中的起始行,它又包含了三个部分:请求方法,请求URI和HTTP协议版本。本文就来介绍HTTP请求中的请求方法。

请求方法(Request Methods)

HTTP协议的请求方法有很多,一般可以分为标准的请求方法和扩展的请求方法。标准的请求方法指的是在HTTP协议中定义的请求方法,扩展的请求方法指的是在其他协议中定义的请求方法。

标准的请求方法

在HTTP/1.0版本(RFC 1945)中定义了GET、POST和HEAD三个方法,在HTTP/1.1的最初版本(RFC 2068)中,则定义了GET、HEAD、PUT、POST、DELETE、TRACE、OPTIONS七个方法,在HTTP/1.1的第二版(RFC 2616)中,又增加了一个CONNECT方法,之后的HTTP/1.1的第三版(RFC 7231)中没有添加新的方法。这样,在HTTP/1.1中就有八个标准的HTTP请求方法:GET、HEAD、PUT、POST、DELETE、TRACE、OPTIONS和CONNECT。在HTTP/2(RFC 7540)中添加了一个PRI方法,它只在客户端和服务端建立连接时使用,PRI请求作为一个连接序言,其报文结构和一般的HTTP报文结构并不相同。

HTTP协议要求HTTP服务端必须实现GET和HEAD方法,其他方法可以根据需要实现。此外,协议还要求,如果HTTP服务端接收到一个已经实现,但不能被用于相应的资源的请求方法,需要返回错误码405,表示Method Not Allowed。如果HTTP服务端接收到一个未知的方法,则需要返回错误码501,表示未实现 (Not Implemented)。(注意501并非只用于接收到未知的请求方法,还有其他情况也会返回501。)

HTTP请求报文中直接将方法对应的名字写入到HTTP请求行中,请求方法区分大小写,所有标准的HTTP方法都是全部大写字母的,所以使用get和GET会是两个不同的方法。参考文章HTTP method names: upper or lower case?

扩展的请求方法

HTTP协议允许在其他协议或规范中对请求方法进行扩展,除了八个标准的HTTP方法外,还有一些扩展的HTTP方法。

RFC 2068 19.6.1 Additional Request Methods一节中,描述了PATCH,LINK和UNLINK三个附加请求方法,这里的Additional Request Methods属于Additional Features一章,Additional Features并不属于HTTP/1.1标准的一部分,它仅仅提供了一些当时已有的HTTP实现中的协议元素用来参考,其中有一些是实验性的功能。在随后的RFC 2616中也提到,这三个方法并未普遍实现。此外,这里的描述是相当简略,并没有完整的定义这三个方法的语义。所以虽然这三个方法存在于HTTP协议的最初版本中,但并不作为标准的请求方法来看待,甚至很难将它们作为扩展的请求方法,因为它们并没有完整的语义。

出于对部分资源更新的需求,PATCH方法又被重新定义在RFC 5789中,RFC 5789是一个PROPOSED STANDARD。不同于RFC 2068中的PATCH方法,在RFC 5789中完整的定义了PATCH方法的语义和用法,并给出了示例,可以认为这里的PATCH方法是一个新的方法,和RFC 2068中的PATCH方法仅仅是名字和基本概念相同。

WebDAV是基于HTTP/1.1的一个扩展协议,最早定义在RFC 2518中,之后更新为RFC 4918。它在HTTP/1.1的基础上定义了一组额外的HTTP请求方法,头域和状态码等。其中新增请求方法主要有BIND,CHECKIN,CHECKOUT,COPY,DELETE,LOCK,MKCOL,MOVE,PROPFIND,PROPPATCH,REPORT,SEARCH ,UNBIND,UNCHECKOUT,UNLOCK,UPDATE等。

无论是标准的HTTP方法还是扩展的HTTP方法都需要在IANA(Internet Assigned Numbers Authority)注册。IANA除了管理Internet中使用域名和IP地址外,另一项重要职责是对各个互联网协议中使用的名字和数字进行注册和分配。在IANA官网首页上对其职责的描述为。
这里写图片描述

从Protocol Registries进去就可以看到所有已注册的项目列表,其中比较常用的有:记录所有已注册的基于TCP/UDP的服务名及其端口号”Service Name and Transport Protocol Port Number Registry”;记录所有已注册的Mime类型的”Media Types”;以及和HTTP协议相关的”HTTP Method Registry”,”HTTP Status Codes”,”HTTP Upgrade Tokens”等。在HTTP Method Registry中可以找到已注册的HTTP方法列表。可以看到,这里包含了八个标准的HTTP请求方法,还包含了HTTP/2中定义的PRI方法,RFC 5789中定义的PATCH方法,RFC 2068中定义的LINK和UNLINK方法和WebDAV中定义的一系列方法。

请求方法的属性

在RFC 2068中,讨论了请求方法的两类属性,安全性和幂等性,在RFC 7231中,又增加了方法的可缓存性。

安全的方法(Safe Methods)

说到安全,一般都会想到保护数据不被窃取,保证系统不被攻击,但这里安全的含义并不是这样。一个安全的请求方法(safe method),指的是这个方法在语义上是只读的,它不会对服务器产生任何预期修改,一个安全的请求(safe request)指的是这条请求的请求方法是安全的。

当客户端对某个资源(URI)执行了一个安全的请求方法,在语义上来说,表示它期望获得一些信息,而不是去主动改变服务端上的数据。对安全方法的合理使用,不应当对服务端产生有害的结果。这里对安全方法的定义特别强调了它是一个语义上的安全,区别于实现安全。站在客户端的角度来看,一个安全的请求应当是只读的,无害的。然而这并不能保证它在服务端的实现就一定是只读的,一个安全的请求方法在实现上可能会对服务器数据进行一些修改,这类修改通常是为了满足一定的业务需求,但如果没有恰当的实现,就可能会产生一些有害的结果。例如,大部分的服务端程序都会将请求信息写入到日志文件中,即使是一个安全的请求方法,仍然会在日志文件中写入这条请求。再比如,用户点击了网页上的一个广告,在产生一个安全的请求的同时,还需要对后台的广告账户进行计费操作。

根据上述定义,GET,HEAD,OPTIONS和TRACE方法被认为是安全的方法,其他方法则被认为是不安全的。

区分安全不安全的请求方法的主要目的在于,让网络爬虫和网页预加载程序能够安心工作,不用担心会造成什么危害。此外,它能够让浏览器在访问一些不被信任的页面时,对网页中执行的不安全的请求方法采取一定的策略来限制它们的活动。浏览器需要能够区分出不安全的请求,让用户能够在这些请求被提交之前,意识到可能存在的风险。

幂等的方法(Idempotent Methods)

在数学和计算机科学中,一项操作被称为幂等的表示这项操作执行任意多次和执行一次的结果是完全一样的。例如求绝对值的运算就是幂等的abs(abs(x))≡ abs(x)。

对一个HTTP请求来说,如果一个请求方法多次独立执行和只执行一次对服务器产生的预期效果完全相同,则称这个请求方法是幂等的,同样的,如果一个请求的请求方法是幂等的,则称这个请求是幂等的请求。根据此定义,PUT,DELETE是幂等的方法,此外所有安全的请求方法也都是幂等的。

这里有两点需要注意。
1. 这里的幂等操作涉及的执行结果指的是对服务端产生的预期结果,并不是客户端得到的报文内容。例如,GET请求是幂等的,执行任意多次都不会对服务器产生任何预期修改。但这并不是说客户端每次执行GET得到的报文内容是一样的。
2. 和安全性一样,幂等性同样是一种从客户端角度来看请求方法的性质,它同样是一种语义上的性质。它并不代表服务端实现上一定是幂等的。例如,服务端可以自由的为每条GET请求记录日志,也可以为每条PUT请求记录修改时间,显然,这时每次幂等的操作在服务端都会产生不同的结果,但这些不同的结果并非是客户端请求时所预期的。

区分幂等操作的意义在于,当客户端发送一条请求后,在获取服务器响应前出现连接错误导致无法确定本次请求是否成功时,可以安全的再次重复这次请求,而不用担心会产生什么副作用。

可缓存的方法(Cacheable Methods)

在RCF 2616中并没有Cacheable Methods这一节内容,它是在RFC 7231中新增的。

可缓存的请求方法指的是该方法对应的响应消息能够在客户端被存储,并在之后的请求中被直接使用,而不再需要从服务端重新获取。可缓存的方法有GET,HEAD和POST,但大部分的时候都只实现了GET和HEAD的缓存。

方法的可缓存性只是标记了哪些方法的结果是可以被缓存的,哪些方法的结果则不需要考虑缓存,但是具体的缓存策略和缓存实现则非常复杂。在RFC 2616中用了专门的一章来讲述HTTP协议的缓存问题,而在最新的版本中,则将其写入到一个独立的RFC文档RFC 7234中。

标准请求方法的属性

根据上述定义,可以得到如下HTTP协议标准请求方法的分类表格。
这里写图片描述

常用请求方法的语义

如前所述,标准的HTTP请求方法一共有八个:GET、HEAD、PUT、POST、DELETE、TRACE、OPTIONS和CONNECT。TRACE、OPTIONS和CONNECT由于在实际开发中几乎不会遇到,这里不打算介绍了,需要了解的可以查看RFC文档和相关文章。扩展请求方法中只有PATCH方法应用比较广泛。因此,下文只介绍GET、HEAD、PUT、POST、DELETE和PATCH方法的语义。

在介绍具体的方法语义之前,先阐述一点,HTTP协议的请求方法都是从UA发起,服务端接收并作出响应,这些方法语义表达的是一个预期的结果,也就是UA发起这个请求后希望服务端如何处理这次请求。后文也会对这一点做进一步的阐述。

  1. GET方法
    GET方法用来向服务器请求指定的资源,它是万维网中信息检索的主要方式。当服务器收到一个GET请求后,它会将所请求的资源内容放到响应体中,客户端收到GET响应后,根据头域中的一些信息,对响应体进行解析,从而得到所需要的资源。

  2. HEAD方法
    HEAD方法用来请求资源的相关属性,而非资源本身,HEAD方法的响应消息中没有响应体。对HEAD请求,服务器需要保证对同一个URI,HEAD请求的响应内容和GET请求的响应内容去掉响应体后完全相同。

    在协议文档中没有对GET和HEAD请求中请求体的语义作出定义,为GET和HEAD请求添加请求体是允许的,但由于语义上未定义,所以服务器实现时有可能会直接将其丢弃,或拒绝连接,这取决于服务端的实现和配置。参见文章HTTP GET with request body

    GET和HEAD请求都是可缓存的,对GET请求的缓存可以在随后的GET和HEAD请求中使用,而对HEAD请求的缓存则只能在随后的HEAD请求中使用。此外,如果已有GET请求的缓存,但缓存已过期,或强制指定HEAD请求不使用缓存,则收到的HEAD请求的响应消息可能会被用来验证或更新之前的GET请求的缓存。

  3. POST方法
    POST方法用于将请求报文中的消息体message body提交给服务器,请求目标资源对消息体内容进行相应处理,这通常会导致服务器上的状态发生变化。

    常见的POST方法的使用场景有:

    • 提供一块数据,例如一组HTML表单数据,供服务器处理。
    • 在BBS,blog,新闻组等网络系统中发布文章或信息。
    • 在服务器上创建新的资源对象。
    • 向已有的资源中添加新的数据

    一般来说POST方法的响应消息是不被UA缓存的,除非服务器在响应消息中明确指定有效期(Freshness Lifetime)信息(表示服务器明确希望这条响应消息被UA缓存),但即使在响应消息中附带了有效期,也不能保证一定会被客户端缓存,因为POST缓存并未普遍实现。

  4. PUT方法
    PUT方法用于将请求报文中的消息体message body提交给服务器,请求服务器创建一个新的目标资源,或者替换原先的目标资源。当一个PUT请求被成功执行后,意味着使用同样的请求URI执行GET请求,在响应消息体中会得到和原先PUT请求中消息体的等价表示。但是,不能保证这种状态变化是可观察的,因为在接收到任何后续GET之前,目标资源可能已经会被其他UA执行的请求所修改,或者之前PUT请求所需要的修改正在被服务器处理但还没有完成,因为UA收到PUT方法的成功响应仅仅表示本次请求的意图已经被服务器接受,但并不代表服务器已经完成了资源创建或替换的工作。

  5. DELETE方法
    对DELETE方法,在RFC 2616中是这样描述的。

    The DELETE method requests that the origin server delete the resource identified by the Request-URI.

    DELETE方法用来请求源服务器删除请求URI标识的目标资源。

    然而在RFC 7231中,对这段描述做了修改,在RFC 7231中是这样描述的。

    The DELETE method requests that the origin server remove the association between the target resource and its current functionality.

    DELETE方法用来请求源服务器删除目标资源与其当前功能之间的联系。也就是说,在新的RFC文档中定义的DELETE方法删除的不再是目标资源,而是删除一种联系。文档中随后做了补充说明,如果目标资源具有一个或多个表示,则它们可能会也可能不会被原始服务器销毁,其关联的存储可能会也可能不会被回收,这完全取决于资源的性质以及源服务器的实现。这段话进一步的说明了,当客户端发送一条DELETE请求,它所预期删除的是这个请求URI相关的功能,而不是删除这个请求URI对应的资源本身,资源本身是否删除取决于资源的属性和服务器实现。

    举例来说,/article/details/123456表示一篇文章,客户端对这个URI发送一条DELETE请求,它所预期的是删除这个URI和对应文章之间的关联,之后无法通过这个URI来查看这篇文章内容,至于文章本身是否需要删除,由服务器自行决定。也许这篇文章有多个URI,/article/details/654321也表示这篇文章,那么显然对/article/details/123456发送DELETE请求,并不能影响到通过/article/details/654321来查看文章内容。

    和GET方法一样,在协议中没有对DELETE请求中请求体的语义作出定义。

    DELETE方法的响应本身不可缓存,但对一个URI成功执行DELETE方法,会导致该请求URI的其他缓存失效。

  6. PATCH方法
    PATCH方法用于对资源进行部分修改。由于PATCH不是标准的HTTP方法,所以不能保证客户端和服务端都已经实现。例如,在JDK中HttpURLConnection类就不支持将请求方法设置为PATCH。

请求方法的选取

在实际应用中经常能听到各种讨论,例如,如果将数据放在请求URI中,是否能用GET代替POST来提交数据?PUT和POST都是用来提交数据,它们的区别是什么,能否用PUT替代POST使用?PUT和PATCH又该如何选取,既然PATCH用作局部更新,那它同样也可以用作全部更新,是否能用PATCH替代PUT使用?

当遇到这类问题时,我们可以从语义上给出一系列的解释,指明它们该如何使用,以及不能混用的原因。

GET和POST的选取

在知乎上的这篇《get和post区别?》中对GET和POST的语义和用法做了很多分析。GET和POST从语义上来说很容易区分。GET在语义上用于请求指定的资源,它的安全的幂等的,不能被用来添加或更改服务器上的资源。而POST则是不安全,不幂等的,它的用途和GET相反,用来对创建新的资源,或对已有资源进行修改。所以如果要提交的数据用来查询,则使用GET方法,如果要提交的数据用来创建或修改,则用POST方法。例如,获取某个用户的信息,获取某个帖子的评论应该用GET方法,而注册用户,发表评论,点赞等则使用POST方法。

POST和PUT的选取

在RFC 2616中是这样描述POST和PUT的区别的。

The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URI. The URI in a POST request identifies the resource that will handle the enclosed entity. That resource might be a data-accepting process, a gateway to some other protocol, or a separate entity that accepts annotations. In contrast, the URI in a PUT request identifies the entity enclosed with the request – the user agent knows what URI is intended and the server MUST NOT attempt to apply the request to some other resource. If the server desires that the request be applied to a different URI, it MUST send a 301 (Moved Permanently) response; the user agent MAY then make its own decision regarding whether or not to redirect the request.

POST和PUT的区别在于请求URI代表的含义不同,对POST请求来说,URI表示对实体(也就是请求体message body)进行处理的资源,它可能是一个数据接受的过程,也可能是一个跳转到其他协议的网关,或者是一个接受注释的实体。而PUT请求中请求URI表示实体本身,一般就是实体对应的资源在服务器的地址。这段话相当抽象和难以理解,以下是个人对这段话的一些理解。

对POST请求来说,URI表示对实体进行处理的资源,它实质上代表的是一种处理实体的方式。当客户端发送一条POST请求后,它希望服务器用请求URI代表的处理方式对请求体中的实体进行处理,至于请求URI代表何种处理方式,如何处理实体内容,取决于实际的业务逻辑。例如,可以用/api/comments表示一个发表评论的接口,客户端向这个地址发送一条POST请求,表示希望使用实体作为评论内容来发表一条评论,再比如/api/pay/buy_something表示付费购买接口,客户端向这个地址发送一条POST请求,表示希望对实体中所携带的商品信息进行付费购买操作。和发表评论不同的是,这里并没有将实体内容保存到某个资源中。一个请求URI也可以代表多种不同的处理,虽然这并不常见,例如同样是/api/comments接口,可以约定当添加消息头clear=true后,服务器会清空评论列表,当添加close=true后,服务器会关闭评论功能,这样/api/comments接口就有了发表评论,清空评论和关闭评论三个功能(当然清空评论和关闭评论的功能应当只对管理后台开放)。此外,POST请求也可以不需要实体,例如/api/comments/{id}/like表示一个对某条评论点赞的接口,客户端向这个地址发送一条POST请求,表示希望对这条评论进行点赞或取消点赞,服务端可以自行根据当前是否已点赞来决定是点赞还是取消点赞。

对PUT请求来说,请求URI表示某个资源在服务器的地址,它不表示对实体进行处理的方式,因为PUT请求对实体处理方式是固定的,也就是用实体内容替换请求URI对应的资源的内容。当客户端发送一条PUT请求后,客户端的目的是将这条请求中的请求URI对应的资源内容被请求体中的数据所取代。例如,/api/comments/1992表示一条评论的地址,客户端向这个地址发送一条PUT请求,就表示希望使用实体内容替换此地址原先的评论内容。

从这里可以看出,PUT请求实际上可以看成是一类特殊的POST请求,它表示的是一种固定处理方式的POST方法。换句话说,可以将PUT方法看成是从POST方法中分化出来的,专门用来处理用实体内容覆盖原先资源内容这样一种处理方式的POST方法。

在前面介绍请求方法历史的时候有提到,最初的HTTP协议只有GET、POST和HEAD三个方法,之后才添加了其他的方法。从上述结论和方法的语义上可以很容易的推论,PUT,DELETE和PATCH方法都是从POST方法中分化出来的,方法本身代表了对实体的处理方式,请求URI则退化为表示资源的地址。

回到之前的问题,由于POST方法的语义涵盖了PUT,DELETE和PATCH,所以无论何时,都可以用POST方法替代PUT,DELETE和PATCH来使用。但反过来,PUT,DELETE和PATCH方法只有在各自特定的场景下,才可以替代POST方法。

PUT和PATCH的选取

在语义上,PUT方法用于全量更新,而PATCH方法则适用于部分更新。对PUT来说,如果要更新一个资源,需要将该资源的全部内容都附加在报文的消息体中,即使是那些没有更改的部分,而PATCH方法则只需要提交那些有变化的部分。

语义和实现之争

虽然我们已经语义上给出这些方法的一系列的差异,但这并不能打消使用上的所有疑虑。这一方面是因为标准方法定义的语义并不能涵盖所有的业务场景,另一方面很多时候实现会和语义产生偏差。

在之前在讨论请求方法的安全性和幂等性的时候就明确了一点,语义上的安全性并不等于实现上就是安全的,语义上的幂等性也不等于实现上的幂等性。同样的,对请求方法来说,虽然协议对各个请求方法的语义做了明确的指定,但语义不同于语法,语义对实现并没有实质性的约束,具体到每个接口的实现上,并不能保证它们一定会按照协议上的语义要求来实现。因此,在实现上完全可以用GET方法提交数据,也可以用PATCH替代PUT,甚至你还可以用GET来删除数据,用DELETE来做数据更新,这完全取决于服务端的实现。当然除非是个人的实验性项目,实际开发是不会这样来做的。这就像写代码的时候用setData()表示获取数据,getData()表示更新数据一样,虽然编译、运行都不会有任何问题,但这除了迷惑自己外显然毫无意义。

因此,服务器在实现一个接口时,一般需要遵循协议中规定的语义,不能随意发挥,尤其是不能改变方法的安全性,幂等性和可缓存性,这除了能够保持统一的上下文环境,便于自己和他人的理解外,另一个重要的原因是,可以保持对UA和现行的互联网基础设施的兼容性。UA,CDN和网络中间节点需要根据标准方法语义的可缓存性来实现响应消息的缓存,网络爬虫也需要根据标准方法语义的安全性来决定是否执行请求。

后记

尽管已经用上了所有的空余时间,这篇《HTTP协议请求方法》仍然花了两周的时间才勉强完成,进度比预期要慢。文章在发表之前就已经修改了很多次,从19日发表之后到现在(22日)又做了十多次的修改,总体上是已经比较完善了,然而有些地方依然有欠缺,等后面有时间再来修补吧。接下来事情更多,所以后面的几篇HTTP协议文章的时间也许会更长。

下一篇预计会介绍HTTP协议的请求URI,包括URI的语法,编解码和用法等,会同时涵盖URL和URN的内容。

相关推荐
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页