浏览器的GET和POST

这里特指浏览器中非Ajax的HTTP请求,即从HTML和浏览器诞生就一直使用的HTTP协议中的GET/POST。浏览器用GET请求来获取一个html页面/图片/css/js等资源;用POST来提交一个

表单,并得到一个结果的网页。

1.幂等性

GET

“读取“一个资源。比如Get到一个html文件。反复读取不应该对访问的数据有副作用。比如”GET一下,用户就下单了,返回订单已受理“,这是不可接受的。没有副作用被称为“幂等“(Idempotent)。

因为GET因为是读取,就可以对GET请求的数据做缓存。这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以减少带宽消耗)。

POST

在页面里 标签会定义一个表单。点击其中的submit元素会发出一个POST请求让服务器做一件事。这件事往往是有副作用的,不幂等的。

不幂等也就意味着不能随意多次执行。因此也就不能缓存。比如通过POST下一个单,服务器创建了新的订单,然后返回订单成功的界面。这个页面不能被缓存。试想一下,如果POST请求被浏览器缓存了,那么下单请求就可以不向服务器发请求,而直接返回本地缓存的“下单成功界面”,却又没有真的在服务器下单。那是一件多么滑稽的事情。

因为POST可能有副作用,所以浏览器实现为不能把POST请求保存为书签。想想,如果点一下书签就下一个单,是不是很恐怖?。

此外如果尝试重新执行POST请求,浏览器也会弹一个框提示下这个刷新可能会有副作用,询问要不要继续。

当然,服务器的开发者完全可以把GET实现为有副作用;把POST实现为没有副作用。只不过这和浏览器的预期不符。

2.请求url body

“GET请求没有body,只有url,请求数据放在url的querystring中;POST请求的数据在body中“。但这种情况仅限于浏览器发请求的场景。但是HTTP协议本身并没有这个限制。

GET

GET和POST携带数据的格式也有区别。当浏览器发出一个GET请求时,就意味着要么是用户自己在浏览器的地址栏输入,要不就是点击了html里a标签的href中的url。所以其实并不是GET只能用url,而是浏览器直接发出的GET只能由一个url触发。所以没办法,GET上要在url之外带一些参数就只能依靠url上附带querystring。

POST

浏览器的POST请求都来自表单提交。每次提交,表单的数据被浏览器用编码到HTTP请求的body里。浏览器发出的POST请求的body主要有有两种格式,一种是application/x-www-form-urlencoded用来传输简单的数据,大概就是"key1=value1&key2=value2"这样的格式。另外一种是传文件,会采用multipart/form-data格式。采用后者是因为application/x-www-form-urlencoded的编码方式对于文件这种二进制的数据非常低效。

浏览器在POST一个表单时,url上也可以带参数,只要里的url带querystring就行。只不过表单里面的那些用 等标签经过用户操作产生的数据都在会在body里。

接口中的GET和POST

这里是指通过浏览器的Ajax api,或者iOS/Android的App的http client,java的commons-httpclient/okhttp或者是curl,postman之类的工具发出来的GET和POST请求。此时GET/POST不光能用在前端和后端的交互中,还能用在后端各个子服务的调用中(即当一种RPC协议使用)。

太自由带来了另一种麻烦,开发人员不得不每次讨论确定参数是放url的path里,querystring里,body里,header里这种问题,太低效了。于是就有了一些列接口规范/风格。其中名气最大的当属REST。REST充分运用GET、POST、PUT和DELETE,约定了这4个接口分别获取、创建、替换和删除“资源”,REST最佳实践还推荐在请求体使用json格式。这样仅仅通过看HTTP的method就可以明白接口是什么意思,并且解析格式也得到了统一。

接口规范REST

GET

REST中GET和POST不是随便用的。在REST中, 【GET】 + 【资源定位符】被专用于获取资源或者资源列表,比如:

GET http://foo.com/books          获取书籍列表
GET http://foo.com/books/:bookId  根据bookId获取一本具体的书

与浏览器的场景类似,REST GET也不应该有副作用,于是可以被反复无脑调用。浏览器(包括浏览器的Ajax请求)对于这种GET也可以实现缓存(如果服务器端提示了明确需要Caching);但是如果用非浏览器,有没有缓存完全看客户端的实现了。当然,也可以从整个App角度,也可以完全绕开浏览器的缓存机制,实现一套业务定制的缓存框架。

POST

REST 【POST】+ 【资源定位符】则用于“创建一个资源”,比如:

POST http://foo.com/books
{
  "title": "大宽宽的碎碎念",
  "author": "大宽宽",
  ...
}

这里你就能留意到浏览器中用来实现表单提交的POST,和REST里实现创建资源的POST语义上的不同

顺便讲下REST POST和REST PUT的区别。有些api是使用PUT作为创建资源的Method。PUT与POST的区别在于,PUT的实际语义是“replace”。REST规范里提到PUT的请求体应该是完整的资源,包括id在内。比如上面的创建一本书的api也可以定义为:

PUT http://foo.com/books
{
  "id": "BOOK:affe001bbe0556a",
  "title": "大宽宽的碎碎念",
  "author": "大宽宽",
  ...
}

服务器应该先根据请求提供的id进行查找,如果存在一个对应id的元素,就用请求中的数据整体替换已经存在的资源;如果没有,就用“把这个id对应的资源从【空】替换为【请求数据】“。直观看起来就是“创建”了。

关于安全性

我们常听到GET不如POST安全,因为POST用body传输数据,而GET用url传输,更加容易看到。但是从攻击的角度,无论是GET还是POST都不够安全,因为HTTP本身是明文协议每个HTTP请求和返回的每个byte都会在网络上明文传播,不管是url,header还是body。这完全不是一个“是否容易在浏览器地址栏上看到“的问题。

为了避免传输中数据被窃取,必须做从客户端到服务器的端端加密。业界的通行做法就是https——即用SSL协议协商出的密钥加密明文的http数据。这个加密的协议和HTTP协议本身相互独立。如果是利用HTTP开发公网的站点/App,要保证安全,https是最最基本的要求。

回到HTTP本身,的确GET请求的参数更倾向于放在url上,因此有更多机会被泄漏。比如携带私密信息的url会展示在地址栏上,还可以分享给第三方,就非常不安全了。只要记得一般情况下,私密数据传输用POST + body就好。

关于编码

常见的说法有,比如GET的参数只能支持ASCII,而POST能支持任意binary,包括中文。但其实从上面可以看到,GET和POST实际上都能用url和body。因此所谓编码确切地说应该是http中url用什么编码,body用什么编码

url

url只能支持ASCII的说法源自于RFC1738。实际上这里规定的仅仅是一个ASCII的子集[a-zA-Z0-9$-_.+!*'(),]。它们是可以“不经编码”在url中使用。比如尽管空格也是ASCII字符,但是不能直接用在url里。

那这个“编码”是什么呢?如果有了特殊符号和中文怎么办呢?一种叫做percent encoding的编码方法就是干这个用的,这也就是为啥我们偶尔看到url里有一坨%和16位数字组成的序列。使用Percent Encoding,即使是binary data,也是可以通过编码后放在URL上的。

但要特别注意,这个编码方式只管把字符转换成URL可用字符,但是却不管字符集编码(比如中文到底是用UTF8还是GBK)这块早期一直都相当乱,也没有什么统一规范。比如有时跟网页编码一样,有的是操作系统的编码一样。最要命的是浏览器的地址栏是不受开发者控制的。这样,对于同样一个带中文的url,如果有的浏览器一定要用GBK(比如老的IE8),有的一定要用UTF8(比如chrome)。后端就可能认不出来。对此常用的办法是避免让用户输入这种带中文的url。如果有这种形式的请求,都改成用户界面上输入,然后通过Ajax发出的办法。Ajax发出的编码形式开发者是可以100%控制的。

不过目前基本上utf8已经大一统了。现在的开发者除非是被国家规定要求一定要用GB系列编码的场景,基本上不会再遇到这类问题了。

顺便说一句,尽管在浏览器地址栏可以看到中文。但这种url在发送请求过程中,浏览器会把中文用字符编码+Percent Encode翻译为真正的url,再发给服务器。浏览器地址栏里的中文只是想让用户体验好些而已。

Body

HTTP Body相对好些,因为有个Content-Type来比较明确的定义。比如:

POST xxxxxx HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded ; charset=UTF-8

这里Content-Type会同时定义请求body的格式(application/x-www-form-urlencoded)和字符编码(UTF-8)。

结论:

所以body和url都可以提交中文数据给后端,但是POST的规范好一些,相对不容易出错,容易让开发者安心。对于GET+url的情况,只要不涉及到在老旧浏览器的地址栏输入url,也不会有什么太大的问题。

POST

回到POST,浏览器直接发出的POST请求就是表单提交,而表单提交只有application/x-www-form-urlencoded针对简单的key-value场景;和multipart/form-data,针对只有文件提交,或者同时有文件和key-value的混合提交表单的场景。

如果是Ajax或者其他HTTP Client发出去的POST请求,其body格式就非常自由了,常用的有json,xml,文本,csv……甚至是你自己发明的格式。只要前后端能约定好即可。

关于URL的长度

因为上面提到了不论是GET和POST都可以使用URL传递数据,所以我们常说的“GET数据有长度限制“其实是指”URL的长度限制“。

HTTP协议本身对URL长度并没有做任何规定。实际的限制是由客户端/浏览器以及服务器端决定的。

先说浏览器。不同浏览器不太一样。比如我们常说的2048个字符的限制,其实是IE8的限制。Chrome的URL限制是2MB等。

除了浏览器,服务器这边也有限制,比如apache的LimieRequestLine指令。nginx用large_client_header_buffers 指令来分配请求头中的很长数据的buffer。这个buffer可以用来处理url,header value等。Tomcat的限制是web.xml里maxHttpHeaderSize来设置的,控制的是整个“请求头”的总长度。

为啥要限制呢?如果写过解析一段字符串的代码就能明白,解析的时候要分配内存。对于一个字节流的解析,必须分配buffer来保存所有要存储的数据。而URL这种东西必须当作一个整体看待,无法一块一块处理,于是就处理一个请求时必须分配一整块足够大的内存。如果URL太长,而并发又很高,就容易挤爆服务器的内存;同时,超长URL的好处并不多,我也只有处理老系统的URL时因为不敢碰原来的逻辑,又得追加更多数据,才会使用超长URL。

对于开发者来说,使用超长的URL完全是给自己埋坑,需要同时要考虑前后端,以及中间代理每一个环节的配置。此外,超长URL会影响搜索引擎的爬虫,有些爬虫甚至无法处理超过2000个字节的URL。这也就意味着这些URL无法被搜到,坑爹啊。

其实并没有太大必要弄清楚精确的URL最大长度限制。我个人的经验是,只要某个要开发的资源/api的URL长度有可能达到2000个bytes以上,就必须使用body来传输数据,除非有特殊情况。至于到底是GET + body还是POST + body可以看情况决定。

留意,1个汉字字符经过UTF8编码 + percent encoding后会变成9个字节,别算错哦。

总结

上面讲了一大堆,是希望读者不要死记硬背GET和POST的区别,而是能从更广的层面去看待和思考这个问题。

最后,协议都是人定的。只要客户端和服务器能彼此认同,就能工作。在常规的情况下,用符合规范的方式去实现系统可以减少很多工作量——大家都约定好了,就不要折腾了。但是,总会有一些情况用常规规范不合适,不满足需求。这时思路也不能被规范限制死,更不要死抠RFC。这些规范也许不能处理你遇到的特殊问题。比如:

  • Elastic Search的_search接口使用GET,却用body来表达查询,因为查询很复杂,用querystring很麻烦,必须用json格式才舒服,在请求体用json编码更加容易,不用折腾percent encoding。
  • 用POST写一个接口下单时可能也要考虑幂等,因为前端可能实现“下单按键”有bug,造成用户一次点击发出N个请求。你不能说因为POST by design应该是不幂等就不管了。

协议是死的,人是活的。遇到实际的问题时灵活的运用手上的工具满足需求就好。

资料来源

作者:大宽宽
链接:https://www.zhihu.com/question/28586791/answer/767316172
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted on 2020-07-03 14:10  kuotian  阅读(192)  评论(0编辑  收藏  举报