HTTP1.1 是如何解決隊(duì)頭阻塞問(wèn)題
書(shū)接上文,HTTP 傳輸是基于請(qǐng)求-應(yīng)答的模式進(jìn)行的,報(bào)文必須是一發(fā)一收,任務(wù)被放在一個(gè)任務(wù)隊(duì)列中串行執(zhí)行,而所謂的HTTP隊(duì)頭阻塞問(wèn)題。是指一旦隊(duì)首的請(qǐng)求處理太慢,就會(huì)阻塞后面請(qǐng)求的處理。
- 并發(fā)連接
對(duì)于一個(gè)域名允許分配多個(gè)長(zhǎng)連接,那么相當(dāng)于增加了任務(wù)隊(duì)列,不至于一個(gè)隊(duì)伍的任務(wù)阻塞其它所有任務(wù)。在RFC2616規(guī)定過(guò)客戶端最多并發(fā) 2 個(gè)連接,不過(guò)、現(xiàn)在的瀏覽器標(biāo)準(zhǔn)中,這個(gè)上限要多很多,就比如Chrome 中是 6 個(gè)。 - 域名分片
一個(gè)域名不是可以并發(fā) 多個(gè)長(zhǎng)連接嗎?那我就多分幾個(gè)域名。
比如 content1.test.com 、content2.test.com。
這樣一個(gè)test.com域名下可以分出非常多的二級(jí)域名,而它們都指向同樣的一臺(tái)服務(wù)器,能夠并發(fā)的長(zhǎng)連接數(shù)更多了。事實(shí)上也更好地解決了隊(duì)頭阻塞的問(wèn)題。
即使是提高了并發(fā)連接,也滿足不了對(duì)性能的需求,而只有能夠并發(fā)的長(zhǎng)連接數(shù)更多了,才能更好地解決了隊(duì)頭阻塞的問(wèn)題。
HTTP 代理
HTTP 是基于請(qǐng)求-響應(yīng)模型的協(xié)議,一般由客戶端發(fā)請(qǐng)求,服務(wù)器來(lái)進(jìn)行響應(yīng)。
但是也有代理服務(wù)器的情況。引入代理之后,代理的服務(wù)器相當(dāng)于一個(gè)中間人的角色,對(duì)于客戶端而言,表現(xiàn)為服務(wù)器進(jìn)行響應(yīng);而對(duì)于源服務(wù)器,表現(xiàn)為客戶端發(fā)起請(qǐng)求,具有雙重身份。
- 能干嘛?
負(fù)載均衡??蛻舳说恼?qǐng)求只會(huì)先到達(dá)代理服務(wù)器,后面到底有多少源服務(wù)器,IP 都是多少,客戶端是不知道的。因此,代理服務(wù)器可以拿到這個(gè)請(qǐng)求之后,可以通過(guò)特定的算法分發(fā)給不同的源服務(wù)器,讓各臺(tái)源服務(wù)器的負(fù)載盡量平均。
保障安全。利用心跳機(jī)制監(jiān)控后臺(tái)的服務(wù)器,一旦發(fā)現(xiàn)故障機(jī)就將其踢出集群。并且對(duì)于上下行的數(shù)據(jù)進(jìn)行過(guò)濾,對(duì)非法 IP 限流,這些都是代理服務(wù)器的工作。
緩存代理。將內(nèi)容緩存到代理服務(wù)器,使得客戶端可以直接從代理服務(wù)器獲得而不用到源服務(wù)器那里。
- 相關(guān)頭部字段
Via
代理服務(wù)器需要標(biāo)明自己的身份,假如有兩臺(tái)代理服務(wù)器,在客戶端發(fā)送請(qǐng)求后會(huì)經(jīng)歷這樣一個(gè)過(guò)程:
客戶端 => 代理1 => 代理2 => 源服務(wù)器
源服務(wù)器收到請(qǐng)求后,會(huì)在請(qǐng)求頭拿到這個(gè)字段:
Via: proxy_server1, proxy_server2
而源服務(wù)器響應(yīng)時(shí),最終在客戶端會(huì)拿到這樣的響應(yīng)頭:
Via: proxy_server2, proxy_server1
Via中代理的順序即為在 HTTP 傳輸中 報(bào)文傳達(dá)的順序。X-Forwarded-For
是為誰(shuí)轉(zhuǎn)發(fā), 它記錄的是請(qǐng)求方的IP地址(和Via區(qū)分開(kāi)只記錄請(qǐng)求方這一個(gè)IP)。X-Real-IP
是一種獲取用戶真實(shí) IP 的字段,不管中間經(jīng)過(guò)多少代理,這個(gè)字段始終記錄最初的客戶端的IP。
相應(yīng)的,還有X-Forwarded-Host和X-Forwarded-Proto,分別記錄客戶端(注意哦,不包括代理)的域名和協(xié)議名。X-Forwarded-For產(chǎn)生的問(wèn)題
前面可以看到,只記錄了請(qǐng)求方的 IP,這意味著每經(jīng)過(guò)一個(gè)不同的代理,這個(gè)字段的名字都要變,由此產(chǎn)生了問(wèn)題:
- 意味著代理必須解析 HTTP 請(qǐng)求頭,然后修改,比直接轉(zhuǎn)發(fā)數(shù)據(jù)性能下降。
- 在 HTTPS 通信加密的過(guò)程中,原始報(bào)文是不允許修改的。
為了解決這個(gè)問(wèn)題產(chǎn)生了代理協(xié)議,一般使用明文版本,只需要在 HTTP 請(qǐng)求行上面加如下文本即可:
// PROXY + TCP4/TCP6 + 請(qǐng)求方地址 + 接收方地址 + 請(qǐng)求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
什么是跨域
scheme(協(xié)議)、host(主機(jī))和port(端口)都相同則為同源
說(shuō)到http 就得說(shuō)到跨域了,在前后端分離的開(kāi)發(fā)模式中,經(jīng)常會(huì)遇到跨域問(wèn)題,即 Ajax 請(qǐng)求發(fā)出去了,服務(wù)器也成功響應(yīng)了,前端就是拿不到這個(gè)響應(yīng)。
非同源站點(diǎn)有這樣一些限制
- 不能讀取和修改對(duì)方的 DOM
- 不讀訪問(wèn)對(duì)方的 Cookie、IndexDB 和 LocalStorage
- 限制 XMLHttpRequest 請(qǐng)求。
跨域請(qǐng)求是被瀏覽器攔截,響應(yīng)其實(shí)是成功到達(dá)客戶端了。那這個(gè)攔截是如何發(fā)生呢?
- 瀏覽器是多進(jìn)程的,以 Chrome 為例,進(jìn)程組成如下:
當(dāng)Ajax 請(qǐng)求準(zhǔn)備發(fā)送的時(shí)候,其實(shí)還只是在渲染進(jìn)程的處理。為了防止黑客通過(guò)腳本觸碰到系統(tǒng)資源,瀏覽器將每一個(gè)渲染進(jìn)程裝進(jìn)了沙箱,采取了站點(diǎn)隔離的手段。 - 在沙箱當(dāng)中的渲染進(jìn)程只能通過(guò)網(wǎng)絡(luò)進(jìn)程來(lái)發(fā)送網(wǎng)絡(luò)請(qǐng)。在數(shù)據(jù)傳遞給了瀏覽器主進(jìn)程,主進(jìn)程接收到后,才真正地發(fā)出相應(yīng)的網(wǎng)絡(luò)請(qǐng)求。
在服務(wù)端處理完數(shù)據(jù)后,將響應(yīng)返回,主進(jìn)程檢查到跨域,且沒(méi)有cors響應(yīng)頭,將響應(yīng)體全部丟掉,并不會(huì)發(fā)送給渲染進(jìn)程。這就達(dá)到了攔截?cái)?shù)據(jù)的目的。
解決方案
CORS
CORS 就是是跨域資源共享。它需要瀏覽器和服務(wù)器的共同支持,具體來(lái)說(shuō),非 IE 和 IE10 以上支持CORS,服務(wù)器需要附加特定的響應(yīng)頭,后面具體拆解。
簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求。
- 簡(jiǎn)單請(qǐng)求:
請(qǐng)求方法為 GET、POST 或者 HEAD
請(qǐng)求頭的取值范圍: Accept、Accept-Language、Content-Language、Content- Type(只限于三個(gè)值application/x-www-form-urlencoded、multipart/form-data、 text/plain)
請(qǐng)求發(fā)出去之前,瀏覽器會(huì)自動(dòng)在請(qǐng)求頭當(dāng)中,添加一個(gè)Origin字段,用來(lái)說(shuō)明 請(qǐng)求來(lái)自哪個(gè)源。服務(wù)器拿到請(qǐng)求之后,在回應(yīng)時(shí)對(duì)應(yīng)地添加Access-Control-Allow-Origin字段,如果Origin不在這個(gè)字段的范圍中,那么瀏覽器就會(huì)將響應(yīng)攔截。
Access-Control-Allow-Origin字段是服務(wù)器用來(lái)決定瀏覽器是否攔截這個(gè)響應(yīng),這是必需的字段。
Access-Control-Allow-Credentials表示是否允許發(fā)送 Cookie,對(duì)于跨域請(qǐng)求,瀏覽器對(duì)這個(gè)字段默認(rèn)值設(shè)為 false,而如果需要拿到瀏覽器的 Cookie,需要添加這個(gè)響應(yīng)頭并設(shè)為true, 并且在前端也需要設(shè)置withCredentials屬性:
let xhr = new XMLHttpRequest();xhr.withCredentials = true;
-
Access-Control-Expose-Headers。這個(gè)字段是給 XMLHttpRequest 對(duì)象賦值,讓它不僅可以拿到基本的 響應(yīng)頭字段,還能拿到這個(gè)字段聲明的響應(yīng)頭字段。比如這樣設(shè)置:
Access-Control-Expose-Headers: test
前端可以通過(guò) XMLHttpRequest.getResponseHeader('test') 拿到 test 這個(gè)字段的值。
-
非簡(jiǎn)單請(qǐng)求
除了上述的就是非簡(jiǎn)單請(qǐng)求,針對(duì)這種請(qǐng)求進(jìn)行不同的處理。
主要體現(xiàn)在兩個(gè)方面: 預(yù)檢請(qǐng)求和響應(yīng)字段。
我們以 PUT 方法為例。var url = 'http://xxx.com'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'xxx'); xhr.send()
- 預(yù)檢請(qǐng)求
OPTIONS / HTTP/1.1 Origin: 當(dāng)前地址 Host: xxx.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header
通過(guò)OPTIONS,同時(shí)會(huì)加上Origin源地址和Host目標(biāo)地址,同時(shí)也會(huì)加上兩個(gè)關(guān)鍵的字段:
Access-Control-Request-Method
, 列出 CORS 請(qǐng)求用到哪個(gè)HTTP方法
Access-Control-Request-Headers
,指定 CORS 請(qǐng)求將要加上什么請(qǐng)求頭- 響應(yīng)字段
- 預(yù)檢請(qǐng)求的響應(yīng)
HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0
Access-Control-Allow-Origin
: 表示可以允許請(qǐng)求的源,可以填具體的源名,也可以填*
Access-Control-Allow-Methods
: 表示允許的請(qǐng)求方法列表
Access-Control-Allow-Credentials
: 表示是否允許發(fā)送 Cookie
Access-Control-Allow-Headers
: 表示允許發(fā)送的請(qǐng)求頭字段
Access-Control-Max-Age
: 預(yù)檢請(qǐng)求的有效期,在此期間,不用發(fā)出另外一條預(yù)檢請(qǐng)求在預(yù)檢請(qǐng)求的響應(yīng)返回后,如果請(qǐng)求不滿足響應(yīng)頭的條件,則觸發(fā)XMLHttpRequest的onerror方法,當(dāng)然后面真正的CORS請(qǐng)求也不會(huì)發(fā)出去了。
之后和簡(jiǎn)單請(qǐng)求一樣瀏覽器自動(dòng)加上Origin字段,服務(wù)端響應(yīng)頭返回Access-Control-Allow-Origin。
JSONP
雖然XMLHttpRequest對(duì)象遵循同源政策,但script標(biāo)簽可以通過(guò) src 填上目標(biāo)地址從而發(fā)出 GET 請(qǐng)求,實(shí)現(xiàn)跨域請(qǐng)求并拿到響應(yīng)。這也就是 JSONP 的原理
和CORS相比,JSONP 最大的優(yōu)勢(shì)在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺點(diǎn)也很明顯,請(qǐng)求方法單一,只支持 GET 請(qǐng)求。
Nginx
Nginx通過(guò)反向代理服務(wù)器來(lái)輕松解決跨域問(wèn)題。
- 正向代理幫助客戶端訪問(wèn)客戶端自己訪問(wèn)不到的服務(wù)器(梯子),然后將結(jié)果返回給客戶端。
反向代理拿到客戶端的請(qǐng)求,將請(qǐng)求轉(zhuǎn)發(fā)給其他的服務(wù)器,主要的場(chǎng)景是維持服務(wù)器集群的負(fù)載均衡,反向代理幫其它的服務(wù)器拿到請(qǐng)求,然后選擇一個(gè)合適的服務(wù)器,將請(qǐng)求轉(zhuǎn)交給它。
那 Nginx 是如何來(lái)解決跨域的呢?
客戶端的域名為client.com,服務(wù)器的域名為server.com,客戶端向服務(wù)器發(fā)送 Ajax 請(qǐng)求,會(huì)跨域產(chǎn)生跨域問(wèn)題了,通過(guò)下面這個(gè)配置:
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
Nginx 相當(dāng)于起了一個(gè)跳板,這個(gè)跳板的名字也是client.com,讓客戶端首先訪問(wèn) client.com/api,這當(dāng)然沒(méi)有跨域,然后 Nginx 服務(wù)器作為反向代理,將請(qǐng)求轉(zhuǎn)發(fā)給server.com,當(dāng)響應(yīng)返回時(shí)又將響應(yīng)給到客戶端,這就完成整個(gè)跨域請(qǐng)求的過(guò)程。就像是在飯館點(diǎn)餐時(shí),服務(wù)員來(lái)回穿梭顧客和廚房是一樣的道理,服務(wù)員記錄了我們點(diǎn)的菜品,然后也將廚房的食物送到我們的餐桌上。