簡(jiǎn)單整理自知乎大佬的回答,推薦讀讀。
鏈接:https://www.zhihu.com/question/28586791/answer/767316172
HTTP最早被用來做瀏覽器與服務(wù)器之間<u>交互HTML</u>和<u>表單</u>的通訊協(xié)議;后來又被被廣泛的擴(kuò)充到<u>接口格式</u>的定義上。所以在討論GET和POST區(qū)別的時(shí)候,需要現(xiàn)確定下到底是瀏覽器使用的GET/POST還是用HTTP作為接口傳輸協(xié)議的場(chǎng)景。
瀏覽器的GET和POST
這里特指瀏覽器中非Ajax的HTTP請(qǐng)求,即從HTML和瀏覽器誕生就一直使用的HTTP協(xié)議中的GET/POST。瀏覽器用GET請(qǐng)求來獲取一個(gè)html頁(yè)面/圖片/css/js等資源;用POST來提交一個(gè)<form>表單,并得到一個(gè)結(jié)果的網(wǎng)頁(yè)。
GET
“讀取“一個(gè)資源。反復(fù)讀取不應(yīng)該對(duì)訪問的數(shù)據(jù)有副作用。沒有副作用被稱為“冪等“(Idempotent)。
因?yàn)镚ET因?yàn)槭亲x取,就可以對(duì)GET請(qǐng)求的數(shù)據(jù)做緩存。這個(gè)緩存可以做到瀏覽器本身上(徹底避免瀏覽器發(fā)請(qǐng)求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以減少帶寬消耗)。
POST
發(fā)出一個(gè)POST請(qǐng)求讓服務(wù)器做一件事,如提交訂單。這件事往往是有副作用的,不冪等的。不冪等也就意味著不能隨意多次執(zhí)行。因此也就不能緩存。
當(dāng)然,服務(wù)器的開發(fā)者完全可以把GET實(shí)現(xiàn)為有副作用;把POST實(shí)現(xiàn)為沒有副作用。
GET和POST攜帶數(shù)據(jù)的格式區(qū)別
當(dāng)瀏覽器發(fā)出一個(gè)GET請(qǐng)求時(shí),要么是用戶自己在瀏覽器的地址欄輸入,要不就是點(diǎn)擊了html里a標(biāo)簽的href中的url。其實(shí)并不是GET只能用url,而是瀏覽器直接發(fā)出的GET只能由一個(gè)url觸發(fā)。所以沒辦法,GET上要在url之外帶一些參數(shù)就只能依靠url上附帶querystring。但是HTTP協(xié)議本身并沒有這個(gè)限制。
瀏覽器的POST請(qǐng)求都來自表單提交。每次提交,表單的數(shù)據(jù)被瀏覽器用編碼到HTTP請(qǐng)求的body里。瀏覽器發(fā)出的POST請(qǐng)求的body主要有有兩種格式,一種是application/x-www-form-urlencoded用來傳輸簡(jiǎn)單的數(shù)據(jù),大概就是"key1=value1&key2=value2"這樣的格式。另外一種是傳文件,會(huì)采用multipart/form-data格式。采用后者是因?yàn)閍pplication/x-www-form-urlencoded的編碼方式對(duì)于文件這種二進(jìn)制的數(shù)據(jù)非常低效。
瀏覽器在POST一個(gè)表單時(shí),url上也可以帶參數(shù),只要 <form action="url" >里的url帶querystring就行。只不過表單里面的那些用<input> 等標(biāo)簽經(jīng)過用戶操作產(chǎn)生的數(shù)據(jù)都在會(huì)在body里。
因此我們一般會(huì)泛泛的說“GET請(qǐng)求沒有body,只有url,請(qǐng)求數(shù)據(jù)放在url的querystring中;POST請(qǐng)求的數(shù)據(jù)在body中“。但這種情況僅限于瀏覽器發(fā)請(qǐng)求的場(chǎng)景。
接口中的GET和POST
這里是指通過瀏覽器的Ajax api,或者iOS/Android的App的http client,java的commons-httpclient/okhttp或者是curl,postman之類的工具發(fā)出來的GET和POST請(qǐng)求。
此時(shí)GET/POST不光能用在前端和后端的交互中,還能用在后端各個(gè)子服務(wù)的調(diào)用中(即當(dāng)一種RPC協(xié)議使用)。HTTP協(xié)議在微服務(wù)中的使用是相當(dāng)普遍的。
當(dāng)用HTTP實(shí)現(xiàn)接口發(fā)送請(qǐng)求時(shí),就沒有瀏覽器中那么多限制了,只要是符合HTTP格式的就可以發(fā)。
HTTP請(qǐng)求由三部分組成,分別是:請(qǐng)求行、消息報(bào)頭、請(qǐng)求正文。格式大概是這樣的一個(gè)字符串(為了美觀,我在\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協(xié)議。
從協(xié)議本身看,并沒有什么限制說GET一定不能沒有body,POST就一定不能把參放到<URL>的querystring上。因此其實(shí)可以更加自由的去利用格式。
比如Elastic Search的_search api就用了帶body的GET;也可以自己開發(fā)接口讓POST一半的參數(shù)放在url的querystring里,另外一半放body里;你甚至還可以讓所有的參數(shù)都放Header里——可以做各種各樣的定制,只要請(qǐng)求的客戶端和服務(wù)器端能夠約定好。
針對(duì)如何使用這些方法,出現(xiàn)了一些列接口規(guī)范/風(fēng)格。其中名氣最大的當(dāng)屬REST。REST充分運(yùn)用GET、POST、PUT和DELETE,約定了這4個(gè)接口分別獲取、創(chuàng)建、替換和刪除“資源”,REST最佳實(shí)踐還推薦在請(qǐng)求體使用json格式。這樣僅僅通過看HTTP的method就可以明白接口是什么意思,并且解析格式也得到了統(tǒng)一。
json相對(duì)于x-www-form-urlencoded的優(yōu)勢(shì)在于1)可以有嵌套結(jié)構(gòu);以及 2)可以支持更豐富的數(shù)據(jù)類型。通過一些框架,json可以直接被服務(wù)器代碼映射為業(yè)務(wù)實(shí)體。用起來十分方便。但是如果是寫一個(gè)接口支持上傳文件,那么還是multipart/form-data格式更合適。
在REST中, 【GET】 + 【資源定位符】被專用于獲取資源或者資源列表,比如:
GET http://foo.com/books 獲取書籍列表
GET http://foo.com/books/:bookId 根據(jù)bookId獲取一本具體的書
【POST】+ 【資源定位符】則用于“創(chuàng)建一個(gè)資源”,比如:
POST http://foo.com/books
{
"title": "大寬寬的碎碎念",
"author": "大寬寬",
...
}
REST POST和REST PUT的區(qū)別。有些api是使用PUT作為創(chuàng)建資源的Method。PUT與POST的區(qū)別在于,PUT的實(shí)際語(yǔ)義是“replace” replace。REST規(guī)范里提到PUT的請(qǐng)求體應(yīng)該是完整的資源,包括id在內(nèi)。
到底用PUT還是POST創(chuàng)建資源,完全要看是不是提前可以知道資源所有的數(shù)據(jù)(尤其是id,指代主鍵),以及是不是完整替換。
(詳細(xì)解釋請(qǐng)見作者原文)
關(guān)于安全性
因?yàn)镠TTP本身是明文協(xié)議。每個(gè)HTTP請(qǐng)求和返回的每個(gè)byte都會(huì)在網(wǎng)絡(luò)上明文傳播,不管是url,header還是body。所以從攻擊的角度,無論是GET還是POST都不夠安全。
常聽到GET不如POST安全,因?yàn)镻OST用body傳輸數(shù)據(jù),而GET用url傳輸,更加容易看到。這完全不是一個(gè)“是否容易在瀏覽器地址欄上看到“的問題。
為了避免傳輸中數(shù)據(jù)被竊取,必須做從客戶端到服務(wù)器的端端加密。業(yè)界的通行做法就是https——即用SSL協(xié)議協(xié)商出的密鑰加密明文的http數(shù)據(jù)。這個(gè)加密的協(xié)議和HTTP協(xié)議本身相互獨(dú)立。如果是利用HTTP開發(fā)公網(wǎng)的站點(diǎn)/App,要保證安全,https是最最基本的要求。
關(guān)于數(shù)據(jù)不安全的點(diǎn):從客戶端到服務(wù)器端,有大量的中間節(jié)點(diǎn),包括網(wǎng)關(guān),代理等。他們的access log通常會(huì)輸出完整的url,比如nginx的默認(rèn)access log就是如此。敏感數(shù)據(jù)無論在url上攜帶,在body里,都可以被記錄下來的,因此如果請(qǐng)求要經(jīng)過不信任的公網(wǎng),避免泄密的唯一手段就是https。
(本弱雞接觸的安全知識(shí)不多,不敢賣弄揣度作者深意,詳細(xì)可見原文和查找相關(guān)資料)
關(guān)于編碼
通常討論的GET和POST編碼的區(qū)別,為GET的參數(shù)只能支持ASCII,而POST能支持任意binary,包括中文。但是GET和POST實(shí)際上都能用url和body。因此所謂編碼確切地說應(yīng)該是http中url用什么編碼,body用什么編碼。
url只能支持ASCII的解釋:
這里規(guī)定的僅僅是一個(gè)ASCII的子集[a-zA-Z0-9$-_.+!*'(),]。它們是可以“不經(jīng)編碼”在url中使用。比如盡管空格也是ASCII字符,但是不能直接用在url里。
特殊符號(hào)和中文怎么辦呢?使用一種叫做percent encoding的編碼方法,即使是binary data,也是可以通過編碼后放在URL上的。
詳盡解釋見作者原文。
再討論下Body。HTTP Body相對(duì)好些,因?yàn)橛袀€(gè)Content-Type來比較明確的定義。
POST xxxxxx HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded ; charset=UTF-8
總的來說,body和url都可以提交中文數(shù)據(jù)給后端,但是POST的規(guī)范好一些,相對(duì)不容易出錯(cuò),容易讓開發(fā)者安心。對(duì)于GET+url的情況,只要不涉及到在老舊瀏覽器的地址欄輸入url,也不會(huì)有什么太大的問題。
瀏覽器的POST需要發(fā)兩個(gè)請(qǐng)求嗎?
關(guān)于這個(gè)點(diǎn)要搞清楚幾個(gè)問題:
- 服務(wù)器端是怎樣處理POST請(qǐng)求?
- 哪些場(chǎng)景適合發(fā)一個(gè)請(qǐng)求?
- 哪些場(chǎng)景適合發(fā)兩次請(qǐng)求?
- 這樣選擇是基于哪方面的考慮?
上文中的"HTTP 格式“清楚的顯示了HTTP請(qǐng)求可以被大致分為“請(qǐng)求頭”和“請(qǐng)求體”兩個(gè)部分。使用HTTP時(shí)大家會(huì)有一個(gè)約定,即所有的“控制類”信息應(yīng)該放在請(qǐng)求頭中,具體的數(shù)據(jù)放在請(qǐng)求體里“。于是服務(wù)器端在解析時(shí),總是會(huì)先完全解析全部的請(qǐng)求頭部。這樣,服務(wù)器端總是希望能夠了解請(qǐng)求的控制信息后,就能決定這個(gè)請(qǐng)求怎么進(jìn)一步處理,是拒絕,還是根據(jù)content-type去調(diào)用相應(yīng)的解析器處理數(shù)據(jù),或者直接用zero copy轉(zhuǎn)發(fā)。
比如在用Java寫服務(wù)時(shí),請(qǐng)求處理代碼總是能從HttpSerlvetRequest里getParameter/Header/url。這些信息都是請(qǐng)求頭里的,框架直接就解析了。而對(duì)于請(qǐng)求體,只提供了一個(gè)inputstream,如果開發(fā)人員覺得應(yīng)該進(jìn)一步處理,就自己去讀取和解析請(qǐng)求體。這就能體現(xiàn)出服務(wù)器端對(duì)請(qǐng)求頭和請(qǐng)求體的不同處理方式。
舉個(gè)實(shí)際的例子,比如寫一個(gè)上傳文件的服務(wù),請(qǐng)求url中包含了文件名稱,請(qǐng)求體中是個(gè)尺寸為幾百兆的壓縮二進(jìn)制流。服務(wù)器端接收到請(qǐng)求后,就可以先拿到請(qǐng)求頭部,查看用戶是不是有權(quán)限上傳,文件名是不是符合規(guī)范等。如果不符合,就不再處理請(qǐng)求體的數(shù)據(jù)了,直接丟棄。而不用等到整個(gè)請(qǐng)求都處理完了再拒絕。
為了進(jìn)一步優(yōu)化,客戶端可以利用HTTP的Continued協(xié)議來這樣做:客戶端總是先發(fā)送所有請(qǐng)求頭給服務(wù)器,讓服務(wù)器校驗(yàn)。如果通過了,服務(wù)器回復(fù)“100 - Continue”,客戶端再把剩下的數(shù)據(jù)發(fā)給服務(wù)器。如果請(qǐng)求被拒了,服務(wù)器就回復(fù)個(gè)400之類的錯(cuò)誤,這個(gè)交互就終止了。這樣,就可以避免浪費(fèi)帶寬傳請(qǐng)求體。但是代價(jià)就是會(huì)多一次Round Trip。如果剛好請(qǐng)求體的數(shù)據(jù)也不多,那么一次性全部發(fā)給服務(wù)器可能反而更好。
基于此,客戶端就能做一些優(yōu)化,比如內(nèi)部設(shè)定一次POST的數(shù)據(jù)超過1KB就先只發(fā)“請(qǐng)求頭”,否則就一次性全發(fā)??蛻舳松踔吝€可以做一些Adaptive的策略,統(tǒng)計(jì)發(fā)送成功率,如果成功率很高,就總是全部發(fā)等等。不同瀏覽器,不同的客戶端(curl,postman)可以有各自的不同的方案。不管怎樣做,優(yōu)化目的總是在提高數(shù)據(jù)吞吐和降低帶寬浪費(fèi)上做一個(gè)折衷。
關(guān)于URL的長(zhǎng)度
因?yàn)樯厦嫣岬搅瞬徽撌荊ET和POST都可以使用URL傳遞數(shù)據(jù),所以我們常說的“GET數(shù)據(jù)有長(zhǎng)度限制“其實(shí)是指”URL的長(zhǎng)度限制“。
HTTP協(xié)議本身對(duì)URL長(zhǎng)度并沒有做任何規(guī)定。實(shí)際的限制是由客戶端/瀏覽器以及服務(wù)器端決定的。不同瀏覽器不太一樣。比如我們常說的2048個(gè)字符的限制,其實(shí)是IE8的限制。Chrome的URL限制是2MB。
為啥要限制呢?
如果寫過解析一段字符串的代碼就能明白,解析的時(shí)候要分配內(nèi)存。對(duì)于一個(gè)字節(jié)流的解析,必須分配buffer來保存所有要存儲(chǔ)的數(shù)據(jù)。而URL這種東西必須當(dāng)作一個(gè)整體看待,無法一塊一塊處理,于是就處理一個(gè)請(qǐng)求時(shí)必須分配一整塊足夠大的內(nèi)存。如果URL太長(zhǎng),而并發(fā)又很高,就容易擠爆服務(wù)器的內(nèi)存;同時(shí),超長(zhǎng)URL的好處并不多,我也只有處理老系統(tǒng)的URL時(shí)因?yàn)椴桓遗鲈瓉淼倪壿?,又得追加更多?shù)據(jù),才會(huì)使用超長(zhǎng)URL。
作者建議,只要某個(gè)要開發(fā)的資源/api的URL長(zhǎng)度有可能達(dá)到2000個(gè)bytes以上,就必須使用body來傳輸數(shù)據(jù),除非有特殊情況。至于到底是GET + body還是POST + body可以看情況決定。
總結(jié)
感謝大寬寬大佬,收益匪淺!
文中還有很多點(diǎn)本弱雞沒理解,感興趣的可以讀一讀原文!