前言
官方解釋,HTTP是一個應用層協議,由請求和響應構成,是一個標準的客戶端伺服器模型。HTTP是一個無狀態的協議。通俗來說,就是規定服務端和客戶端通訊的一種規則。更多的是基於瀏覽器環境下使用,那麼從你瀏覽器輸入地址開始到最終頁面的呈現,到底經過了哪些過程呢?廢話不多說,先貼一張圖,如下:
如上圖,就是http請求發起到返回的完證過程,本以為自己對http
的瞭解還算可以,但是乍一看圖還是很蒙的,比如說如果讓你優化http過程,你該從何下手呢?我想大部分還是比較關心request
和response
,以及資料返回之後的DOMContentLoad
和load
,至於http
中的一些配置並不是很清楚,其實通過優化配置,同樣能夠加速網頁的開啟速度,因此,我大致總結一些關於http
中會經常使用的配置項,以及這些使用一般會在哪些場景中使用到。
注意:上圖中的
domContentLoadedEventEnd
代表DOMContentLoaded事件完成的時間節點,也就是jQuery中的domready時間。load
代表的是onload
事件觸發和結束的時間節點。
HTTP之跨域相關內容
基本內容
這裡主要介紹JSONP
和CORS
跨域,現實場景中,以上兩種使用居多,所以其他跨域方案不做詳細介紹。造成跨域的主要原因主要是瀏覽器本身的同源策略
引起的。
JSONP
能夠實現跨域主要是因為瀏覽器上允許標籤上通過src/href載入外鏈路徑
,但是JSONP
只支援GET
請求,同時因為瀏覽器中url
長度的限制,因此JSONP
能傳輸的資料大小也有一定的限制。
CORS
跨域能夠支援的所有ajax的方法,當然,目前是支援ie9+,低版本暫時不支援,隨時網際網路的發展,相信低版本的瀏覽器會逐漸被淘汰。在只用CORS
只需要服務端能夠開啟允許跨域的頭設定即可,也就是Access-Control-Allow-Origin
。
跨域大致的流程圖如下:
注意:
JSONP
中的資料限制並不是GET
請求本身的限制,而是瀏覽器中url
本身有長度限制,GET
方法是沒有任何長度限制的;不管是JSONP
還是CORS
跨域,其實伺服器都可以接收來自客戶端的資料請求,並且也都成功返回了,只是瀏覽器本身有同源策略
的限制,才會進一步判斷返回的資料是否符合瀏覽器的限制。
這裡有個題外話,Access-Control-Allow-Origin
這個配置項預設支援配置單個域名或者*,為了安全起見,不建議配置*
,那麼如何配置才能支援多個域名跨域呢?有一個簡易的方法可以解決,主要思路是通過服務端定義可支援的跨域域名集合,通過迴圈判斷當前請求是否支援即可,片段程式碼如下:
// 服務端程式碼,以下是node服務做測試
const http = require('http')
const allowDomains = [
'http://www.a.com',
'http://www.b.com',
'http://www.c.com'
]
const server = http.createServer((req, res) => {
let acao = ''
for(let i = 0, l = allowDomains.length; i < l; i++) {
if(allowDomains[i].indexOf(req.headers.host) > -1) {
acao = allowDomains[i]
break
}
}
res.writeHead(200,
{
'Access-Control-Allow-Origin': acao
}
)
res.end('Hello World\n')
}).listen(3001)
console.log('server listen 3001')
複製程式碼
CORS跨域限制
- 預設允許的方法有
GET
、HEAD
、POST
。 - 預設允許的Content-Type有
text/plain
、multipart/form-data
、application/x-www-form-urlencoded
。 - HTTP的頭資訊包含
Accept
、Accept-Language
、Content-Language
、Last-Event-ID
。
在請求包含以上內容的時候,其實就是簡單請求,在跨域的情況下,瀏覽器預設是直接通過的,其餘剩下的稱之為複雜請求,瀏覽器會預設傳送一次預請求作為驗證,如果驗證通過則代表請求成功。
因此需要對上圖增加限制的修改,最終如下:
其實就是對於複雜請求
做了一次校驗,大致可以這樣解釋,如果在傳送請求時,例如額外帶了headers
的配置項,如果需要驗證通過就必須在服務端也要配置允許該headers
的返回,這樣預請求的驗證才會通過。也可以通過程式碼做一下驗證,基本如下:
// 後端服務程式碼
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200,
{
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'aa' // 通過設定了這個,才能使得預請求驗證通過
}
)
res.end('Hello World\n')
}).listen(3001)
console.log('server listen 3001')
// 前端服務程式碼
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const html = fs.readFileSync('index.html', 'utf8')
res.writeHead(200,
{
'Content-Type': 'text/html'
}
)
res.end(html)
}).listen(3000)
console.log('server listen 3000')
// index.html主要程式碼如下
fetch('http://localhost:3001', {
method: 'post',
headers: {
aa:'123'
}
})
複製程式碼
以上測試程式碼主要是在發起post
請求的時候額外攜帶了一個headers
引數,只有在服務端配置了允許該headers
傳輸才能使得瀏覽器預請求驗證通過,反之則會失敗。大家可以根據以上測試程式碼在自己的本機測試就能明白了。
注意:通過設定
Access-Control-Request-Method
可以配置其他的方法,例如PUT
、DELETE
等。
細心的同學可能會發現,根據以上程式碼的確可以通過預請求,但是如果再次重新整理網頁,會發現仍然還會存在預請求,對於第一次預請求已經通過了,為什麼同樣的請求還會再傳送一次呢?其實這裡可以做一個優化,減少預請求的傳送。
通過設定Access-Control-Max-Age
來確定預請求的有效時間,只要在有效時間內,就不會再次傳送預請求了。
HTTP之Cache-Control
Cache-Control
包含很多特性,其中no-cache
這個配置項肯定最熟悉,官方解釋是在釋放快取副本之前,強制快取記憶體將請求提交給原始伺服器進行驗證,其實就是代表沒有快取。但是其實它依然有很多特性,經過資料查詢,大致分為以下幾類,
介紹下一般常用的配置引數的文字解釋:
-
public
代表http從請求到返回的整個路徑上的都可以被快取,例如客戶端瀏覽器,經過的代理伺服器等等。 -
private
指發起的瀏覽器這一端才能進行快取,也就是代理伺服器是不能快取的。 -
no-cache
是否使用快取需要通過伺服器驗證後才能判斷。 -
max-age=<seconds>
最大能快取多少秒,過期之後,請求會再次傳送到服務端,對於返回的資料會再次被快取。 -
s-maxage=<seconds>
會覆蓋max-age
或者Expires
頭,應用於共享(如:代理伺服器)快取,並且在代理伺服器生效,客戶端不生效。 -
max-stale[=<seconds>]
表明客戶端願意接收一個已經過期的資源。即使max-age
已經過期,同樣會使用本地的過期快取。 -
must-revalidate
如果max-age
過期,必須通過服務端來驗證返回的資料是否真的過期。 -
proxy-revalidate
主要使用在代理伺服器端,對於過期的資料必須向服務端重新請求一遍。 -
no-store
本地和代理伺服器都不允許存快取。 -
no-transform
不得對資源進行轉換或轉變,主要使用在代理伺服器上。
具體每個配置的官方解釋參考具體說明
資源驗證
Last-Modified
表明請求的資源上次的修改時間,主要配合If-Modified-Since
(客戶端保留的資源上次的修改時間)進行使用,主要是在傳送請求的時候帶上。通過對比上次修改時間以驗證資源是否更新。Etag
資源的內容標識。(不唯一,通常為檔案的md5或者一段hash值,只要保證寫入和驗證時的方法一致即可),配合If-Match
或者If-None-Match
進行使用,對比資源的內容標識來判斷是否使用快取。
其實伺服器可以通過Etag
來區分返回哪些資料,具體可以參考下面的示例:
// server.js
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const url = req.url
const html = fs.readFileSync('index.html', 'utf8')
const etag = req.headers['if-none-match']
if(url === '/') {
res.writeHead(200,
{
'Content-Type': 'text/html',
'Cache-Control': 'max-age=2000, no-cache',
'Last-Modified': '123',
'Etag': '444'
}
)
res.end(html)
}
if(url === '/aa.js') {
if(etag === '444') {
res.writeHead(304,
{
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=2000, no-cache',
'Last-Modified': '123',
'Etag': '444'
}
)
res.end('888888')
} else {
res.writeHead(200,
{
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=2000, no-cache',
'Last-Modified': '123',
'Etag': '444'
}
)
res.end('1111111111')
}
}
}).listen(3000)
console.log('server listen 3000')
複製程式碼
<!-- html檔案 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="/aa.js"></script>
</body>
</html>
複製程式碼
第一次重新整理和第二次重新整理截圖如下:
根據測試程式碼能得出,在服務端通過etag === '444'
做了判斷,但是最終返回的依然是1111111111
,這是由於服務端選取了第一次的快取資料作為返回。雖然發生了一次請求,但請求內容長度減少了,節省了頻寬。
總結
其實以上所有的概念基本可以用一個圖來展示,大致分為2個主要部分:
1、本地快取,其實也就是本地資源的快取。 2、伺服器快取,包含驗證快取和非驗證快取。
HTTP之cookie
- 通過
Set-Cookie
來設定cookie
,下次請求的時候,會自動帶上之前設定的cookie
(同域情況下),取值型別是String/Array。 - 通過
max-age
和Expires
設定過期時間。 - 通過
Secure
來設定只能在https
的時候傳送。 - 通過
HttpOnly
來設定無法通過document.cookie
訪問。 - 通過
domain=
來設定該cookie
是否在同域下共享。
以上內容基本能彙總成一張圖解釋,如下:
HTTP之keep-alive
目前開發web網頁,所有請求預設都是Connection: keep-alive
,除非服務端手動去關閉配置(Connection: close
),使用keep-alive
可以複用之前的請求的通道,這樣減少tcp
三次握手的時間(同域情況下才能生效)。
HTTP之資料協商
web服務請求會攜帶以下資訊內容:
- Accept 想要的資料型別
- Accept-Encoding 限制服務端資料的壓縮方式(gzip、deflate、br)
- Accept-Language 服務端返回資料的語言型別
- User-Agent瀏覽器頭資訊內容
服務端返回資料會攜帶以下資訊內容:
- Content-type 返回的資料型別
- Content-Encoding 對應的是
Accept-Encoding
代表資料壓縮型別 - Content-Language 對應的是
Accept-Language
代表資料語言型別
當然,web請求也可以自定義Content-type
來傳輸資料,一般在form
表單中比較常用,例如上傳檔案,會指定Content-type:multipart/form-data
,這樣服務端就能接收上傳的檔案資訊內容。
HTTP之重定向
瀏覽器能識別的重定向code
碼有兩種,分別是301
和302
,兩者在使用上會有一定的區別,大致如下:
301
重定向是永久重定向
,使用者在訪問資源的時候,瀏覽器預設是從快取中獲取之前指定的跳轉資訊;302
重定向可以隨時取消,也就是使用者在訪問資源的時候,每次都會經過服務端並且在服務端通過跳轉邏輯進行跳轉;
可以使用一段程式碼來描述以上的不同之處,基本程式碼如下:
// 302跳轉測試程式碼
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const url = req.url
const html = fs.readFileSync('index.html', 'utf8')
console.log(url)
if(url === '/') {
res.writeHead(302,
{
Location: '/wq'
}
)
res.end('')
}
if(url === '/wq') {
res.writeHead(200,
{
'Content-Type': 'text/html',
}
)
res.end(html)
}
}).listen(3000)
console.log('server listen 3000')
複製程式碼
301
測試程式碼跟上面基本一樣,只要將302
改成301
即可,根據以上程式碼測試,你會發現,每次在重新整理頁面的時候,如果是302
跳轉,那麼console.log(url)
每次都會列印/ 和 /wq
,如果是301
的話,只會列印/wq
,這就說明,302
的跳轉是從服務端指定跳轉的,而301
的跳轉則是永久性的,除非清楚本地瀏覽器的快取,要麼無法改變。
HTTP之內容安全策略
配置內容安全策略涉及到新增Content-Security-Policy
,HTTP頭部到一個頁面,並配置相應的值,以控制使用者代理(瀏覽器等)可以為該頁面獲取哪些資源。更多詳細參考具體說明
常用示例如下:
// 一個網站管理者想要所有內容均來自站點的同一個源 (不包括其子域名)
'Content-Security-Policy': default-src 'self'
// 一個網站管理者允許內容來自信任的域名及其子域名 (域名不必須與CSP設定所在的域名相同)
'Content-Security-Policy': default-src 'self' *.trusted.com
// 該伺服器僅允許通過HTTPS方式並僅從onlinebanking.jumbobank.com域名來訪問文件
'Content-Security-Policy': default-src https://onlinebanking.jumbobank.com
// 限制向百度請求
'Content-Security-Policy': connect-src http://baidu.com
// 通過設定report-uri來指定上報伺服器地址
'Content-Security-Policy': default-src 'self'; report-uri http://reportcollector.example.com/collector.cgi
複製程式碼
HTTP之HTTP2
HTTP/2
是HTTP協議自1999年HTTP1.1釋出後的首個更新,主要基於SPDY
協議。它由網際網路工程任務組(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小組進行開發。該組織於2014年12月將HTTP/2標準提議遞交至IESG進行討論,於2015年2月17日被批准。HTTP/2標準於2015年5月以RFC 7540正式發表。
大致總結有以下特性:
- 二進位制分幀
- 多路複用:同域名下所有通訊都在單個連線上完成;單個連線可以承載任意數量的雙向資料流;資料流以訊息的形式傳送,而訊息又由一個或多個幀組成,多個幀之間可以亂序傳送,因為根據幀首部的流標識可以重新組裝;
- 伺服器推送:服務端可以在傳送頁面HTML時主動推送其它資源,而不用等到瀏覽器解析到相應位置,發起請求再響應。
- 頭部壓縮:HTTP/2對訊息頭採用HPACK(專為http2頭部設計的壓縮格式)進行壓縮傳輸,能夠節省訊息頭佔用的網路的流量。而HTTP/1.x每次請求,都會攜帶大量冗餘頭資訊,浪費了很多頻寬資源。
貼一張簡圖,可以更好的體現,更多詳情可以參考:
為了能更好的體驗實際場景中的效果,做了簡單的測試,使用express
配合node
開啟http2
來測試具體效果,在這之前,需要生成一個SSL
,生成程式碼如下:
// 直接在cmd中執行
openssl req -x509 -nodes -newkey rsa:2048 -keyout example.com.key -out example.com.crt
複製程式碼
編寫基礎的server.js
程式碼,基本如下:
const port = 3000
const spdy = require('spdy')
const express = require('express')
const path = require('path')
const fs = require('fs')
const resolve = file => path.resolve(__dirname, file)
const app = express()
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0
})
app.use('/html', serve('./html', true))
app.get('/', (req, res) => {
res.status(200)
res.send('hello world')
})
app.get('/timg.jpeg', (req, res) => {
const img = fs.readFileSync('/html/timg.jpeg')
res.writeHead(200,
{"Content-Type": 'image/jpeg'
});
res.send(img)
})
const options = {
key: fs.readFileSync(__dirname + '/example.com.key'),
cert: fs.readFileSync(__dirname + '/example.com.crt')
}
spdy
.createServer(options, app)
.listen(port, (error) => {
if (error) {
console.error(error)
return process.exit(1)
} else {
console.log('Listening on port: ' + port + '.')
}
})
複製程式碼
根目錄的靜態檔案定義fetch
方法來請求100
張圖片,這樣來測試請求的通道以及載入時長,基本程式碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>http2測試</title>
</head>
<body>
<script>
for(let i = 0; i < 100; i++) {
fetch('//localhost:3000/html/timg.jpeg')
}
</script>
</body>
</html>
複製程式碼
按照以上程式碼執行,並且對比HTTP/1.1
的效果如下(圖1是HTTP/1.1,圖2是HTTP2):
從圖1和圖2就能看出,圖2中Connection ID
只有一個,而圖1中Connection ID
會隨著請求的增加而增加,每增加一個通道,就會建立一次TCP
連結,也就是需要經過三次握手,並且還受限瀏覽器本身的併發數(其中谷歌瀏覽器的最大併發數是6個),所以才會出現等待的情況;從Size
那一列能看出,HTTP2
中的請求資料也會小(因為本身資料就小,所以不明顯),這樣能夠減少頻寬;從最終的Finsh
時間能看出,同樣是100張圖片的請求,HTTP2
耗時更少。
總結
以上內容主要是和HTTP
相關的內容,由於比較簡單,就不提供測試程式碼壓縮包,基本從示例中直接複製貼上就能本地測試執行,如果有什麼不正確的地方,歡迎提Issues
。