简单整理自知乎大佬的回答,推荐读读。
链接:https://www.zhihu.com/question/28586791/answer/767316172
HTTP最早被用来做浏览器与服务器之间<u>交互HTML</u>和<u>表单</u>的通讯协议;后来又被被广泛的扩充到<u>接口格式</u>的定义上。所以在讨论GET和POST区别的时候,需要现确定下到底是浏览器使用的GET/POST还是用HTTP作为接口传输协议的场景。
浏览器的GET和POST
这里特指浏览器中非Ajax的HTTP请求,即从HTML和浏览器诞生就一直使用的HTTP协议中的GET/POST。浏览器用GET请求来获取一个html页面/图片/css/js等资源;用POST来提交一个<form>表单,并得到一个结果的网页。
GET
“读取“一个资源。反复读取不应该对访问的数据有副作用。没有副作用被称为“幂等“(Idempotent)。
因为GET因为是读取,就可以对GET请求的数据做缓存。这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以减少带宽消耗)。
POST
发出一个POST请求让服务器做一件事,如提交订单。这件事往往是有副作用的,不幂等的。不幂等也就意味着不能随意多次执行。因此也就不能缓存。
当然,服务器的开发者完全可以把GET实现为有副作用;把POST实现为没有副作用。
GET和POST携带数据的格式区别
当浏览器发出一个GET请求时,要么是用户自己在浏览器的地址栏输入,要不就是点击了html里a标签的href中的url。其实并不是GET只能用url,而是浏览器直接发出的GET只能由一个url触发。所以没办法,GET上要在url之外带一些参数就只能依靠url上附带querystring。但是HTTP协议本身并没有这个限制。
浏览器的POST请求都来自表单提交。每次提交,表单的数据被浏览器用编码到HTTP请求的body里。浏览器发出的POST请求的body主要有有两种格式,一种是application/x-www-form-urlencoded用来传输简单的数据,大概就是"key1=value1&key2=value2"这样的格式。另外一种是传文件,会采用multipart/form-data格式。采用后者是因为application/x-www-form-urlencoded的编码方式对于文件这种二进制的数据非常低效。
浏览器在POST一个表单时,url上也可以带参数,只要 <form action="url" >
里的url带querystring就行。只不过表单里面的那些用<input>
等标签经过用户操作产生的数据都在会在body里。
因此我们一般会泛泛的说“GET请求没有body,只有url,请求数据放在url的querystring中;POST请求的数据在body中“。但这种情况仅限于浏览器发请求的场景。
接口中的GET和POST
这里是指通过浏览器的Ajax api,或者iOS/Android的App的http client,java的commons-httpclient/okhttp或者是curl,postman之类的工具发出来的GET和POST请求。
此时GET/POST不光能用在前端和后端的交互中,还能用在后端各个子服务的调用中(即当一种RPC协议使用)。HTTP协议在微服务中的使用是相当普遍的。
当用HTTP实现接口发送请求时,就没有浏览器中那么多限制了,只要是符合HTTP格式的就可以发。
HTTP请求由三部分组成,分别是:请求行、消息报头、请求正文。格式大概是这样的一个字符串(为了美观,我在\r\n后都换行一下):
<METHOD> <URL> HTTP/1.1\r\n
<Header1>: <HeaderValue1>\r\n
<Header2>: <HeaderValue2>\r\n
...
<HeaderN>: <HeaderValueN>\r\n
\r\n
<Body Data....>
可参考文章:HTTP协议。
从协议本身看,并没有什么限制说GET一定不能没有body,POST就一定不能把参放到<URL>的querystring上。因此其实可以更加自由的去利用格式。
比如Elastic Search的_search api就用了带body的GET;也可以自己开发接口让POST一半的参数放在url的querystring里,另外一半放body里;你甚至还可以让所有的参数都放Header里——可以做各种各样的定制,只要请求的客户端和服务器端能够约定好。
针对如何使用这些方法,出现了一些列接口规范/风格。其中名气最大的当属REST。REST充分运用GET、POST、PUT和DELETE,约定了这4个接口分别获取、创建、替换和删除“资源”,REST最佳实践还推荐在请求体使用json格式。这样仅仅通过看HTTP的method就可以明白接口是什么意思,并且解析格式也得到了统一。
json相对于x-www-form-urlencoded的优势在于1)可以有嵌套结构;以及 2)可以支持更丰富的数据类型。通过一些框架,json可以直接被服务器代码映射为业务实体。用起来十分方便。但是如果是写一个接口支持上传文件,那么还是multipart/form-data格式更合适。
在REST中, 【GET】 + 【资源定位符】被专用于获取资源或者资源列表,比如:
GET http://foo.com/books 获取书籍列表
GET http://foo.com/books/:bookId 根据bookId获取一本具体的书
【POST】+ 【资源定位符】则用于“创建一个资源”,比如:
POST http://foo.com/books
{
"title": "大宽宽的碎碎念",
"author": "大宽宽",
...
}
REST POST和REST PUT的区别。有些api是使用PUT作为创建资源的Method。PUT与POST的区别在于,PUT的实际语义是“replace” replace。REST规范里提到PUT的请求体应该是完整的资源,包括id在内。
到底用PUT还是POST创建资源,完全要看是不是提前可以知道资源所有的数据(尤其是id,指代主键),以及是不是完整替换。
(详细解释请见作者原文)
关于安全性
因为HTTP本身是明文协议。每个HTTP请求和返回的每个byte都会在网络上明文传播,不管是url,header还是body。所以从攻击的角度,无论是GET还是POST都不够安全。
常听到GET不如POST安全,因为POST用body传输数据,而GET用url传输,更加容易看到。这完全不是一个“是否容易在浏览器地址栏上看到“的问题。
为了避免传输中数据被窃取,必须做从客户端到服务器的端端加密。业界的通行做法就是https——即用SSL协议协商出的密钥加密明文的http数据。这个加密的协议和HTTP协议本身相互独立。如果是利用HTTP开发公网的站点/App,要保证安全,https是最最基本的要求。
关于数据不安全的点:从客户端到服务器端,有大量的中间节点,包括网关,代理等。他们的access log通常会输出完整的url,比如nginx的默认access log就是如此。敏感数据无论在url上携带,在body里,都可以被记录下来的,因此如果请求要经过不信任的公网,避免泄密的唯一手段就是https。
(本弱鸡接触的安全知识不多,不敢卖弄揣度作者深意,详细可见原文和查找相关资料)
关于编码
通常讨论的GET和POST编码的区别,为GET的参数只能支持ASCII,而POST能支持任意binary,包括中文。但是GET和POST实际上都能用url和body。因此所谓编码确切地说应该是http中url用什么编码,body用什么编码。
url只能支持ASCII的解释:
这里规定的仅仅是一个ASCII的子集[a-zA-Z0-9$-_.+!*'(),]。它们是可以“不经编码”在url中使用。比如尽管空格也是ASCII字符,但是不能直接用在url里。
特殊符号和中文怎么办呢?使用一种叫做percent encoding的编码方法,即使是binary data,也是可以通过编码后放在URL上的。
详尽解释见作者原文。
再讨论下Body。HTTP Body相对好些,因为有个Content-Type来比较明确的定义。
POST xxxxxx HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded ; charset=UTF-8
总的来说,body和url都可以提交中文数据给后端,但是POST的规范好一些,相对不容易出错,容易让开发者安心。对于GET+url的情况,只要不涉及到在老旧浏览器的地址栏输入url,也不会有什么太大的问题。
浏览器的POST需要发两个请求吗?
关于这个点要搞清楚几个问题:
- 服务器端是怎样处理POST请求?
- 哪些场景适合发一个请求?
- 哪些场景适合发两次请求?
- 这样选择是基于哪方面的考虑?
上文中的"HTTP 格式“清楚的显示了HTTP请求可以被大致分为“请求头”和“请求体”两个部分。使用HTTP时大家会有一个约定,即所有的“控制类”信息应该放在请求头中,具体的数据放在请求体里“。于是服务器端在解析时,总是会先完全解析全部的请求头部。这样,服务器端总是希望能够了解请求的控制信息后,就能决定这个请求怎么进一步处理,是拒绝,还是根据content-type去调用相应的解析器处理数据,或者直接用zero copy转发。
比如在用Java写服务时,请求处理代码总是能从HttpSerlvetRequest里getParameter/Header/url。这些信息都是请求头里的,框架直接就解析了。而对于请求体,只提供了一个inputstream,如果开发人员觉得应该进一步处理,就自己去读取和解析请求体。这就能体现出服务器端对请求头和请求体的不同处理方式。
举个实际的例子,比如写一个上传文件的服务,请求url中包含了文件名称,请求体中是个尺寸为几百兆的压缩二进制流。服务器端接收到请求后,就可以先拿到请求头部,查看用户是不是有权限上传,文件名是不是符合规范等。如果不符合,就不再处理请求体的数据了,直接丢弃。而不用等到整个请求都处理完了再拒绝。
为了进一步优化,客户端可以利用HTTP的Continued协议来这样做:客户端总是先发送所有请求头给服务器,让服务器校验。如果通过了,服务器回复“100 - Continue”,客户端再把剩下的数据发给服务器。如果请求被拒了,服务器就回复个400之类的错误,这个交互就终止了。这样,就可以避免浪费带宽传请求体。但是代价就是会多一次Round Trip。如果刚好请求体的数据也不多,那么一次性全部发给服务器可能反而更好。
基于此,客户端就能做一些优化,比如内部设定一次POST的数据超过1KB就先只发“请求头”,否则就一次性全发。客户端甚至还可以做一些Adaptive的策略,统计发送成功率,如果成功率很高,就总是全部发等等。不同浏览器,不同的客户端(curl,postman)可以有各自的不同的方案。不管怎样做,优化目的总是在提高数据吞吐和降低带宽浪费上做一个折衷。
关于URL的长度
因为上面提到了不论是GET和POST都可以使用URL传递数据,所以我们常说的“GET数据有长度限制“其实是指”URL的长度限制“。
HTTP协议本身对URL长度并没有做任何规定。实际的限制是由客户端/浏览器以及服务器端决定的。不同浏览器不太一样。比如我们常说的2048个字符的限制,其实是IE8的限制。Chrome的URL限制是2MB。
为啥要限制呢?
如果写过解析一段字符串的代码就能明白,解析的时候要分配内存。对于一个字节流的解析,必须分配buffer来保存所有要存储的数据。而URL这种东西必须当作一个整体看待,无法一块一块处理,于是就处理一个请求时必须分配一整块足够大的内存。如果URL太长,而并发又很高,就容易挤爆服务器的内存;同时,超长URL的好处并不多,我也只有处理老系统的URL时因为不敢碰原来的逻辑,又得追加更多数据,才会使用超长URL。
作者建议,只要某个要开发的资源/api的URL长度有可能达到2000个bytes以上,就必须使用body来传输数据,除非有特殊情况。至于到底是GET + body还是POST + body可以看情况决定。
总结
感谢大宽宽大佬,收益匪浅!
文中还有很多点本弱鸡没理解,感兴趣的可以读一读原文!