http協(xié)議經(jīng)常被忽略的知識(shí)
一、前言
http協(xié)議,應(yīng)該是前端開(kāi)發(fā)同學(xué)最熟悉的網(wǎng)絡(luò)協(xié)議。在日常開(kāi)發(fā)過(guò)程中,各種狀態(tài)碼、請(qǐng)求頭、響應(yīng)頭常常在不經(jīng)意間使你掉頭,跨域請(qǐng)求、Cookie限制與安全(CSRF)、緩存問(wèn)題,也一直伴隨著幾乎每一次面試。然而,這些只是http的冰山一角。http協(xié)議中,有許多一直在被使用,你卻可能從未了解過(guò)的內(nèi)容。
注意:以下關(guān)于http標(biāo)準(zhǔn)的內(nèi)容是大部分瀏覽器或常見(jiàn)客戶(hù)端支持的,標(biāo)準(zhǔn)和實(shí)現(xiàn)可以是完全不同的,鼓勵(lì)大家遵循標(biāo)準(zhǔn),但是很多時(shí)候反模式還是挺香的。
二、Form表單
ajax技術(shù)從發(fā)明開(kāi)始,一直用一直爽。但在某些極致的場(chǎng)景下,如在2G弱網(wǎng)條件下的活動(dòng)頁(yè),基本沒(méi)必要使用任何js,一個(gè)html加一些內(nèi)聯(lián)樣式,就可以解決。
2.1 Content-Type
text/plain
對(duì)空格使用+號(hào)編碼,其他字符不編碼
application/x-www-form-urlencoded
對(duì)所有字符編碼
multipart/form-data
上傳文件時(shí)必須選擇form-data(base64上傳的當(dāng)我沒(méi)說(shuō)),不同字段用boundary=--- xxx隔開(kāi),boundary可以自定義,在content-type中指定
POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="description"
some text
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt"
Content-Type: text/plain
(content of the uploaded file foo.txt)
---------------------------974767299852498929531610575
問(wèn)題:為什么要使用Content-Type: application/json
三、CORS跨域
3.1 簡(jiǎn)單請(qǐng)求
某些請(qǐng)求不會(huì)觸發(fā) CORS 預(yù)檢請(qǐng)求 。本文稱(chēng)這樣的請(qǐng)求為“簡(jiǎn)單請(qǐng)求”,請(qǐng)注意,該術(shù)語(yǔ)并不屬于 Fetch (其中定義了 CORS)規(guī)范。若請(qǐng)求滿(mǎn)足所有下述條件,則該請(qǐng)求可視為“簡(jiǎn)單請(qǐng)求”:
使用下列方法之一:
GET
HEAD
POST
除了被用戶(hù)代理自動(dòng)設(shè)置的首部字段(例如 Connection , User-Agent )和在 Fetch 規(guī)范中定義為 禁用首部名稱(chēng) 的其他首部,允許人為設(shè)置的字段為 Fetch 規(guī)范定義的 對(duì) CORS 安全的首部字段集合 。該集合為:
Accept
Accept-Language
Content-Language
Content-Type (需要注意額外的限制)
DPR
Downlink
Save-Data
Viewport-Width
Width
Content-Type 的值僅限于下列三者之一:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded請(qǐng)求中的任意 XMLHttpRequestUpload 對(duì)象均沒(méi)有注冊(cè)任何事件監(jiān)聽(tīng)器;XMLHttpRequestUpload 對(duì)象可以使用 XMLHttpRequest.upload 屬性訪問(wèn)。
請(qǐng)求中沒(méi)有使用 ReadableStream 對(duì)象。
3.2 預(yù)檢請(qǐng)求
Access-Control-Allow-Credentials: 控制是否允許攜帶Cookie
Access-Control-Allow-Origin:支持的來(lái)源
Access-Control-Allow-Method: 支持方法
Access-Control-Allow-Headers: 支持的請(qǐng)求頭
Access-Control-Max-Age: 預(yù)檢有效時(shí)間

問(wèn)題:如何減少或者避免options預(yù)檢請(qǐng)求?
四、協(xié)商緩存(Cache-Control: no-cache)
4.1 If-Modified-Since
只用于GET和HEAD請(qǐng)求,時(shí)間和文件的last-modified不一樣,返回200和文件內(nèi)容
curl 'http://127.0.0.1:2048/modify.html' -H 'If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT'
時(shí)間和文件的last-modified一致,返回304
curl 'http://127.0.0.1:2048/modify.html' -H 'If-Modified-Since: Tue, 24 Nov 2020 12:21:59 GMT'
如果同時(shí)存在If-None-Match,If-Modified-Since會(huì)被忽略
4.2 If-None-Match
用于GET和HEAD請(qǐng)求時(shí),后面接的值是一個(gè)或多個(gè)Etag,當(dāng)Etag與線上文件匹配上,返回304,否則返回200和文件內(nèi)容
// 304
curl -I http://127.0.0.1:2048/modify.html -H 'If-None-Match: "5fbcfae7-263", "5fbcfae7-264"'
Etag值為*號(hào)時(shí),可以用于判斷文件是否存在,文件存在時(shí)返回304,不存在返回404
// 文件存在,上傳失敗,返回304
curl -X PUT -v -F 'file=@/Users/mooncat/http/modify.html;filename=modify.html;type=application/octet-stream' http://127.0.0.1:2048/temp -H 'If-None-Match: "*"'
// 文件不存在,上傳成功,返回200
curl -X PUT -v -F 'file=@/Users/mooncat/http/modify.html;filename=modify.html;type=application/octet-stream' http://127.0.0.1:2048/temp -H 'If-None-Match: "*"'
如果是不安全的請(qǐng)求方法(如POST),Etag不配置時(shí),返回412(Precondition Failed)
4.3 If-UnModified-Since
并發(fā)控制
作用:與不安全的請(qǐng)求方法配置,控制并發(fā),確保文件未被修改,如果文件已經(jīng)被修改,則返回412(Precondition Failed) 錯(cuò)誤 應(yīng)用場(chǎng)景: 編輯線上文件,但發(fā)布文件已經(jīng)發(fā)生變化
斷點(diǎn)續(xù)傳
與Range請(qǐng)求頭配合使用,獲取文件內(nèi)容時(shí),如果文件發(fā)生變化,返回412(Precondition Failed) 錯(cuò)誤 應(yīng)用場(chǎng)景:文件下載過(guò)程中,線上文件發(fā)生修改
// 412 Precondition Failed
curl 'http://127.0.0.1:2048/modify.html' -H 'If-Unmodified-Since: Sun, 12th Nov 2020 12:12:12 GMT'
// 200 ok
curl 'http://127.0.0.1:2048/modify.html' -H 'If-Unmodified-Since: Tue, 24 Nov 2020 22:21:59 GMT'
4.4 If-Match
并發(fā)控制
作用:與不安全的請(qǐng)求方法配置,控制并發(fā),確保文件未被修改,如果文件已經(jīng)被修改,則返回
與Range配合使用,如果發(fā)現(xiàn)not match的情況,返回412(Precondition Failed)
4.5 Etag
Etag一般作為線上文件指紋
W/可選
'W/' (大小寫(xiě)敏感) 表示使用 弱驗(yàn)證器 。弱驗(yàn)證器很容易生成,但不利于比較。強(qiáng)驗(yàn)證器是比較的理想選擇,但很難有效地生成。相同資源的兩個(gè)弱 Etag 值可能語(yǔ)義等同,但不是每個(gè)字節(jié)都相同。
"<etag_value>"
實(shí)體標(biāo)簽唯一地表示所請(qǐng)求的資源。它們是位于雙引號(hào)之間的ASCII字符串(如“675af34563dc-tr34”)。沒(méi)有明確指定生成ETag值的方法。通常,使用內(nèi)容的散列,最后修改時(shí)間戳的哈希值,或簡(jiǎn)單地使用版本號(hào)。例如,MDN使用wiki內(nèi)容的十六進(jìn)制數(shù)字的哈希值。
Nginx的Etag生成規(guī)則
> curl -I http://127.0.0.1:2048/
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Wed, 25 Nov 2020 15:46:35 GMT
Content-Type: text/html
Content-Length: 1871
Last-Modified: Wed, 25 Nov 2020 15:32:19 GMT
Connection: keep-alive
ETag: "5fbe7903-74f"
Accept-Ranges: bytes
問(wèn)題:Etag怎么生成?
五、斷點(diǎn)續(xù)傳
http協(xié)議支持?jǐn)帱c(diǎn)續(xù)傳,前提是客戶(hù)端和服務(wù)端都支持,任何一端不支持,都會(huì)引起全量傳輸,在某些瀏覽器上可能會(huì)導(dǎo)致一些異常(如音視頻無(wú)法播放)
5.1 請(qǐng)求或響應(yīng)頭
Accept-Range: none/bytes
none時(shí),瀏覽器會(huì)禁止接收bytes
Content-Range
Content-Range: <unit> <range-start>-<range-end>/<size>
Content-Range: <unit> <range-start>-<range-end>/*
Content-Range: <unit> */<size>
Range
Http協(xié)議通過(guò)Range請(qǐng)求頭控制續(xù)傳的范圍
$ curl -v 'http://127.0.0.1:2048/' -H 'Range: bytes=0-3'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2048 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:2048
> User-Agent: curl/7.64.1
> Accept: */*
> Range: bytes=0-3
>
< HTTP/1.1 206 Partial Content
< Server: nginx/1.19.0
< Date: Wed, 25 Nov 2020 15:59:19 GMT
< Content-Type: text/html
< Content-Length: 4
< Last-Modified: Wed, 25 Nov 2020 15:32:19 GMT
< Connection: keep-alive
< ETag: "5fbe7903-74f"
< Content-Range: bytes 0-3/1871
<
* Connection #0 to host 127.0.0.1 left intact
<!DO* Closing connection 0
Range的范圍也可以是多個(gè)片斷如,多個(gè)片斷時(shí)用boundary隔離
curl -v 'http://127.0.0.1:2048/' -H 'Range: bytes=0-3, 8-9'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2048 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:2048
> User-Agent: curl/7.64.1
> Accept: */*
> Range: bytes=0-3, 8-9
>
< HTTP/1.1 206 Partial Content
< Server: nginx/1.19.0
< Date: Wed, 25 Nov 2020 16:00:27 GMT
< Content-Type: multipart/byteranges; boundary=00000000000000000032
< Content-Length: 202
< Last-Modified: Wed, 25 Nov 2020 15:32:19 GMT
< Connection: keep-alive
< ETag: "5fbe7903-74f"
<
--00000000000000000032
Content-Type: text/html
Content-Range: bytes 0-3/1871
<!DO
--00000000000000000032
Content-Type: text/html
Content-Range: bytes 8-9/1871
E
--00000000000000000032--
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0
5.2 狀態(tài)碼
206
Partial Content416
Range Not Satisfiable
協(xié)議常用套路
提一個(gè)問(wèn)題:http協(xié)議如何判斷內(nèi)容傳輸完成?
六、跳轉(zhuǎn)的江湖:301、302、303、307、308
6.1 301 Moved Permanently
建議用在Get和Head方法,其他方法在某些瀏覽器301后方法會(huì)被改變,如:Post方法, 301后會(huì)變Get
6.2 302 Found
建議用在Get和Head方法,301一樣,方法會(huì)被改變, Set-Cookie大部分瀏覽器會(huì)被傳遞到到reidirect后的地址,這一特性可能用來(lái)傳遞很多不可描述的參數(shù),舉個(gè)栗子,在微信登陸認(rèn)證302跳轉(zhuǎn)時(shí),所有的參數(shù)都會(huì)被截?cái)啵@時(shí)候是可以通過(guò)set-cookie把參數(shù)寫(xiě)入到客戶(hù)端,跳回來(lái)時(shí),還是可以拿到的。
6.3 303 See Other
post改變成get方法,這是個(gè)feature
6.4 307 Temporary Redirect
修復(fù)302跳轉(zhuǎn)時(shí),請(qǐng)求方法被改變的問(wèn)題
6.5 308 Permanent Redirect
修復(fù)301跳轉(zhuǎn)時(shí),請(qǐng)求方法被改變的問(wèn)題
問(wèn)題:304狀態(tài)碼在什么場(chǎng)景下發(fā)生?強(qiáng)緩存什么狀態(tài)碼?
七、代理、隧道有啥不同
正向代理
同樣是通過(guò)第三方代理,隱藏訪問(wèn)來(lái)源,如通過(guò)代理,訪問(wèn)某些小網(wǎng)站 反向代理
通過(guò)第三方代理請(qǐng)求,隱藏背后真實(shí)的服務(wù),前端常用于解決域名問(wèn)題 隧道
在兩端加上編碼或協(xié)議轉(zhuǎn)換工具,工具之前自由傳輸,到端后,再解碼出來(lái)
CONNECT realserver.com:443 HTTP/1.0
User-Agent: GoProxy

八、總結(jié)
http協(xié)議的設(shè)計(jì),非常地靈活,拓展性非常好,雖然很多時(shí)間有些修修補(bǔ)補(bǔ)的跡象。由于瀏覽器之間的競(jìng)爭(zhēng),其實(shí)標(biāo)準(zhǔn)本身對(duì)實(shí)現(xiàn)沒(méi)有強(qiáng)約束,有很多怪異行為,這一點(diǎn)沒(méi)有IP/TCP協(xié)議那么嚴(yán)格(雖然IP/TCP協(xié)議也有不少過(guò)度設(shè)計(jì),計(jì)劃趕不上變化)。
參考資料:CORS簡(jiǎn)單請(qǐng)求
●●●
●●●


