為什么我們要熟悉這些通信協議?
前端的重要的基礎知識點是什么?
- 原生javaScript,HTML,CSS.
- Dom操作
- EventLoop和渲染機制
- 各類工程化的工具原理以及使用,根據需求定制編寫插件和包。(webpack的plugin和babel的預設包)
- 數據結構和算法(特別是IM以及超大型高并發網站應用等,例如B站)
- 最后便是通信協議
在使用某個技術的時候,一定要去追尋原理和底層的實現,長此以往堅持,只要自身底層的基礎扎實,無論技術怎么變化,學習起來都不會太累,總的來說就是拒絕5分鐘技術。
從輸入一個url地址,到顯示頁面發生了什么出發:
1.瀏覽器向 DNS 服務器請求解析該 URL 中的域名所對應的 IP 地址;
2.建立TCP連接(三次握手);
3.瀏覽器發出讀取文件(URL 中域名后面部分對應的文件)的HTTP 請求,該請求報文作為 TCP 三次握手的第三個報文的數據發送給服務器;
4.服務器對瀏覽器請求作出響應,并把對應的 html 文本發送給瀏覽器;
5.瀏覽器將該 html 文本并顯示內容;
6.釋放 TCP連接(四次揮手)。
目前常見的通信協議都是建立在TCP鏈接之上
那么什么是TCP呢?
TCP是因特網中的傳輸層協議,使用三次握手協議建立連接。當主動方發出SYN連接請求后,等待對方回答。
TCP三次握手的過程如下:
- 客戶端發送SYN報文給服務器端,進入SYN_SEND狀態。
- 服務器端收到SYN報文,回應一個SYN(SEQ=y)ACK(ACK=x+1)報文,進入SYN_RECV狀態。
- 客戶端收到服務器端的SYN報文,回應一個ACK(ACK=y+1)報文,進入Established狀態。
- 三次握手完成,TCP客戶端和服務器端成功地建立連接,可以開始傳輸數據了。
如圖所示:
TCP的四次揮手:
- 建立一個連接需要三次握手,而終止一個連接要經過四次握手,這是由TCP的半關閉(half-close)造成的。具體過程如下圖所示。
- 某個應用進程首先調用close,稱該端執行“主動關閉”(active close)。該端的TCP于是發送一個FIN分節,表示數據發送完畢。
- 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP確認。
注意:FIN的接收也作為一個文件結束符(end-of-file)傳遞給接收端應用進程,放在已排隊等候該應用進程接收的任何其他數據之后,因為,FIN的接收意味著接收端應用進程在相應連接上再無額外數據可接收。
- 一段時間后,接收到這個文件結束符的應用進程將調用close關閉它的套接字。這導致它的TCP也發送一個FIN。
- 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。 [3]
既然每個方向都需要一個FIN和一個ACK,因此通常需要4個分節。
特別提示: SYN報文用來通知,FIN報文是用來同步的
以上就是面試官常問的三次握手,四次揮手,但是這不僅僅面試題,上面僅僅答到了一點皮毛,學習這些是為了讓我們后續方便了解他的優缺點。
在TCP連接建立后,我們可以有多種協議的方式通信交換數據:
最古老的方式一:http 1.0
- 早先1.0的HTTP版本,是一種無狀態、無連接的應用層協議。
- HTTP1.0規定瀏覽器和服務器保持短暫的連接,瀏覽器的每次請求都需要與服務器建立一個TCP連接,服務器處理完成后立即斷開TCP連接(無連接),服務器不跟蹤每個客戶端也不記錄過去的請求(無狀態)。
- 這種無狀態性可以借助cookie/session機制來做身份認證和狀態記錄。而下面兩個問題就比較麻煩了。
- 首先,無連接的特性導致最大的性能缺陷就是無法復用連接。每次發送請求的時候,都需要進行一次TCP的連接,而TCP的連接釋放過程又是比較費事的。這種無連接的特性會使得網絡的利用率非常低。
- 其次就是隊頭阻塞(headoflineblocking)。由于HTTP1.0規定下一個請求必須在前一個請求響應到達之前才能發送。假設前一個請求響應一直不到達,那么下一個請求就不發送,同樣的后面的請求也給阻塞了。
Http 1.0的致命缺點就是無法復用TCP連接和并行發送請求,這樣每次一個請求都需要三次握手,而且其實建立連接和釋放連接的這個過程是最耗時的,傳輸數據相反卻不那么耗時。還有本地時間被修改導致響應頭expires的緩存機制失效的問題~(后面會詳細講)
- 常見的請求報文~
于是出現了Http 1.1,這也是技術的發展必然結果~
- Http 1.1出現,繼承了Http1.0的優點,也克服了它的缺點,出現了keep-alive這個頭部字段,它表示會在建立TCP連接后,完成首次的請求,并不會立刻斷開TCP連接,而是保持這個連接狀態~進而可以復用這個通道。
- Http 1.1并且支持請求管道化,“并行”發送請求,但是這個并行,也不是真正意義上的并行,而是可以讓我們把先進先出隊列從客戶端(請求隊列)遷移到服務端(響應隊列)。
例如:客戶端同時發了兩個請求分別來獲取html和css,假如說服務器的css資源先準備就緒,服務器也會先發送html再發送css。
- B站首頁,就有keep-alive,因為他們也有IM的成分在里面。需要大量復用TCP連接~
- HTTP1.1好像還是無法解決隊頭阻塞的問題
實際上,現階段的瀏覽器廠商采取了另外一種做法,它允許我們打開多個TCP的會話。也就是說,上圖我們看到的并行,其實是不同的TCP連接上的HTTP請求和響應。這也就是我們所熟悉的瀏覽器對同域下并行加載6~8個資源的限制。而這,才是真正的并行!
Http 1.1的致命缺點:
1.明文傳輸。
2.其實還是沒有解決無狀態連接的。
3.當有多個請求同時被掛起的時候就會擁塞請求通道,導致后面請求無法發送。
4.臃腫的消息首部:HTTP/1.1能壓縮請求內容,但是消息首部不能壓縮;在現今請求中,消息首部占請求絕大部分(甚至是全部)也較為常見。
我們也可以用dns-prefetch和 preconnect tcp來優化~
- <link rel="preconnect" href="//example.com" crossorigin>
- <link rel="dns=prefetch" href="//example.com">
- Tip: webpack可以做任何事情,這些都可以用插件實現
基于這些缺點,出現了Http 2.0
相較于HTTP1.1,HTTP2.0的主要優點有采用二進制幀封裝,傳輸變成多路復用,流量控制算法優化,服務器端推送,首部壓縮,優先級等特點。
HTTP1.x的解析是基于文本的,基于文本協議的格式解析存在天然缺陷,文本的表現形式有多樣性,要做到健壯性考慮的場景必然很多。而HTTP/2會將所有傳輸的信息分割為更小的消息和幀,然后采用二進制的格式進行編碼,HTTP1.x的頭部信息會被封裝到HEADER frame,而相應的RequestBody則封裝到DATAframe里面。不改動HTTP的語義,使用二進制編碼,實現方便且健壯。
多路復用
所有的請求都是通過一個 TCP 連接并發完成。HTTP/1.x 雖然通過 pipeline 也能并發請求,但是多個請求之間的響應會被阻塞的,所以 pipeline 至今也沒有被普及應用,而 HTTP/2 做到了真正的并發請求。同時,流還支持優先級和流量控制。當流并發時,就會涉及到流的優先級和依賴。即:HTTP2.0對于同一域名下所有請求都是基于流的,不管對于同一域名訪問多少文件,也只建立一路連接。優先級高的流會被優先發送。圖片請求的優先級要低于 CSS 和 SCRIPT,這個設計可以確保重要的東西可以被優先加載完。
流量控制
TCP協議通過sliding window的算法來做流量控制。發送方有個sending window,接收方有receive window。http2.0的flow control是類似receive window的做法,數據的接收方通過告知對方自己的flow window大小表明自己還能接收多少數據。只有Data類型的frame才有flow control的功能。對于flow control,如果接收方在flow window為零的情況下依然更多的frame,則會返回block類型的frame,這張場景一般表明http2.0的部署出了問題。
服務器端推送
服務器端的推送,就是服務器可以對一個客戶端請求發送多個響應。除了對最初請求的響應外,服務器還可以額外向客戶端推送資源,而無需客戶端明確地請求。當瀏覽器請求一個html,服務器其實大概知道你是接下來要請求資源了,而不需要等待瀏覽器得到html后解析頁面再發送資源請求。
首部壓縮
- HTTP 2.0 在客戶端和服務器端使用“首部表”來跟蹤和存儲之前發送的鍵-值對,對于相同的數據,不再通過每次請求和響應發送;通信期間幾乎不會改變的通用鍵-值對(用戶代理、可接受的媒體類型,等等)只需發送一次。事實上,如果請求中不包含首部(例如對同一資源的輪詢請求),那么 首部開銷就是零字節。此時所有首部都自動使用之前請求發送的首部。
- 如果首部發生變化了,那么只需要發送變化了數據在Headers幀里面,新增或修改的首部幀會被追加到“首部表”。首部表在 HTTP 2.0 的連接存續期內始終存在,由客戶端和服務器共同漸進地更新 。
- 本質上,當然是為了減少請求啦,通過多個js或css合并成一個文件,多張小圖片拼合成Sprite圖,可以讓多個HTTP請求減少為一個,減少額外的協議開銷,而提升性能。當然,一個HTTP的請求的body太大也是不合理的,有個度。文件的合并也會犧牲模塊化和緩存粒度,可以把“穩定”的代碼or 小圖 合并為一個文件or一張Sprite,讓其充分地緩存起來,從而區分開迭代快的文件。
Demo的性能對比:
Http的那些致命缺陷,并沒有完全解決,于是有了https,也是目前應用最廣的協議之一。
HTTP+ 加密 + 認證 + 完整性保護 =HTTPS ?
可以這樣認為~HTTP 加上加密處理和認證以及完整性保護后即是 HTTPS。
- 如果在 HTTP 協議通信過程中使用未經加密的明文,比如在 Web 頁面中輸入信用卡號,如果這條通信線路遭到竊聽,那么信用卡號就暴露了。
- 另外,對于 HTTP 來說,服務器也好,客戶端也好,都是沒有辦法確認通信方的。
因為很有可能并不是和原本預想的通信方在實際通信。并且還需要考慮到接收到的報文在通信途中已經遭到篡改這一可能性。
- 為了統一解決上述這些問題,需要在 HTTP 上再加入加密處理和認證等機制。我們把添加了加密及認證機制的 HTTP 稱為 HTTPS。
不加密的重要內容被wireshark這類工具抓到包,后果很嚴重~
HTTPS 是身披 SSL 外殼的 HTTP
HTTPS 并非是應用層的一種新協議。只是 HTTP 通信接口部分用 SSL(SecureSocket Layer)和 TLS(Transport Layer Security)協議代替而已。
通常,HTTP 直接和 TCP 通信。
- 當使用 SSL 時,則演變成先和 SSL 通信,再由 SSL和 TCP 通信了。簡言之,所謂 HTTPS,其實就是身披 SSL 協議這層外殼的HTTP。
- 在采用 SSL 后,HTTP 就擁有了 HTTPS 的加密、證書和完整性保護這些功能。SSL 是獨立于 HTTP 的協議,所以不光是 HTTP 協議,其他運行在應用層的 SMTP和 Telnet 等協議均可配合 SSL 協議使用??梢哉f SSL 是當今世界上應用最為廣泛的網絡安全術。
相互交換密鑰的公開密鑰加密技術 -----對稱加密
- 在對 SSL 進行講解之前,我們先來了解一下加密方法。SSL 采用一種叫做公開密鑰加密(Public-key cryptography)的加密處理方式。
- 近代的加密方法中加密算法是公開的,而密鑰卻是保密的。通過這種方式得以保持加密方法的安全性。
加密和解密都會用到密鑰。沒有密鑰就無法對密碼解密,反過來說,任何人只要持有密鑰就能解密了。如果密鑰被攻擊者獲得,那加密也就失去了意義。
HTTPS 采用混合加密機制
- HTTPS 采用共享密鑰加密和公開密鑰加密兩者并用的混合加密機制。
- 但是公開密鑰加密與共享密鑰加密相比,其處理速度要慢。所以應充分利用兩者各自的優勢,將多種方法組合起來用于通信。在交換密鑰環節使用公開密鑰加密方式,之后的建立通信交換報文階段則使用共享密鑰加密方式。
HTTPS雖好,非對稱加密雖好,但是不要濫用
HTTPS 也存在一些問題,那就是當使用 SSL 時,它的處理速度會變慢。
SSL 的慢分兩種。一種是指通信慢。另一種是指由于大量消耗 CPU 及內存等資源,導致處理速度變慢。
- 和使用 HTTP 相比,網絡負載可能會變慢 2 到 100 倍。除去和 TCP 連接、發送 HTTP 請求 ? 響應以外,還必須進行 SSL 通信,因此整體上處理通信量不可避免會增加。
- 另一點是 SSL 必須進行加密處理。在服務器和客戶端都需要進行加密和解密的運算處理。因此從結果上講,比起 HTTP 會更多地消耗服務器和客戶端的硬件資源,導致負載增強。
針對速度變慢這一問題,并沒有根本性的解決方案,我們會使用 SSL 加速器這種(專用服務器)硬件來改善該問題。該硬件為 SSL 通信專用硬件,相對軟件來講,能夠提高數倍 SSL 的計算速度。僅在 SSL 處理時發揮 SSL加速器的功效,以分擔負載。
為什么不一直使用 HTTP
- 既然 HTTPS 那么安全可靠,那為何所有的 Web 網站不一直使用 HTTPS?
其中一個原因是,因為與純文本通信相比,加密通信會消耗更多的 CPU 及內存資源。如果每次通信都加密,會消耗相當多的資源,平攤到一臺計算機上時,能夠處理的請求數量必定也會隨之減少。
- 因此,如果是非敏感信息則使用 HTTP 通信,只有在包含個人信息等敏感數據時,才利用 HTTPS 加密通信。
特別是每當那些訪問量較多的 Web 網站在進行加密處理時,它們所承擔著的負載不容小覷。在進行加密處理時,并非對所有內容都進行加密處理,而是僅在那些需要信息隱藏時才會加密,以節約資源。
- 除此之外,想要節約購買證書的開銷也是原因之一。
要進行 HTTPS 通信,證書是必不可少的。而使用的證書必須向認證機構(CA)購買。證書價格可能會根據不同的認證機構略有不同。通常,一年的授權需要數萬日元(現在一萬日元大約折合 600 人民幣)。那些購買證書并不合算的服務以及一些個人網站,可能只會選擇采用HTTP 的通信方式。
復習完了基本的協議,介紹下報文格式:
- 請求報文格式
- 響應報文格式
所謂響應頭,請求頭,其實都可以自己添加字段,只要前后端給對應的處理機制即可。
Node.js代碼實現響應頭的設置
- if (config.cache.expires) {
- res.setHeader("expries", new Date(Date.now() + (config.cache.maxAge * 1000)))
- }
- if (config.cache.lastModified) {
- res.setHeader("last-modified", stat.mtime.toUTCString())
- }
- if (config.cache.etag) {
- res.setHeader('Etag', etagFn(stat))
- }
響應頭的詳解:
本人的開源項目,手寫的Node.js靜態資源服務器,https://github.com/JinJieTan/...,歡迎 star~
瀏覽器的緩存策略:
- 首次請求:
- 非首次請求:
- 用戶行為與緩存:
不能緩存的請求:
無法被瀏覽器緩存的請求如下:
- HTTP信息頭中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告訴瀏覽器不用緩存的請求。
- 需要根據Cookie,認證信息等決定輸入內容的動態請求是不能被緩存的。
- 經過HTTPS安全加密的請求(有人也經過測試發現,ie其實在頭部加入Cache-Control:max-age信息,firefox在頭部加入Cache-Control:Public之后,能夠對HTTPS的資源進行緩寸)。
- 經過HTTPS安全加密的請求(有人也經過測試發現,ie其實在頭部加入Cache-Control:max-age信息,firefox在頭部加入Cache-Control:Public之后,能夠對HTTPS的資源進行緩存,參考《HTTPS的七個誤解》)。
- POST請求無法被緩存。
- HTTP響應頭中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的請求無法被緩存。
即時通訊協議
從最初的沒有websocket協議開始:
傳統的協議無法服務端主動push數據,于是有了這些騷操作:
- 輪詢,在一個定時器中不停向服務端發送請求。
- 長輪詢,發送請求給服務端,直到服務端覺得可以返回數據了再返回響應,否則這個請求一直掛起~
- 以上兩種都有瑕疵,而且比較明顯,這里不再描述。
為了解決實時通訊,數據同步的問題,出現了webSocket。
- webSockets的目標是在一個單獨的持久連接上提供全雙工、雙向通信。在Javascript創建了Web Socket之后,會有一個HTTP請求發送到瀏覽器以發起連接。在取得服務器響應后,建立的連接會將HTTP升級從HTTP協議交換為WebSocket協議。
- webSocket原理: 在TCP連接第一次握手的時候,升級為ws協議。后面的數據交互都復用這個TCP通道。
- 客戶端代碼實現:
- const ws = new WebSocket('ws://localhost:8080');
- ws.onopen = function () {
- ws.send('123')
- console.log('open')
- }
- ws.onmessage = function () {
- console.log('onmessage')
- }
- ws.onerror = function () {
- console.log('onerror')
- }
- ws.onclose = function () {
- console.log('onclose')
- }
- 服務端使用 Node.js語言實現
- const express = require('express')
- const { Server } = require("ws");
- const app = express()
- const wsServer = new Server({ port: 8080 })
- wsServer.on('connection', (ws) => {
- ws.onopen = function () {
- console.log('open')
- }
- ws.onmessage = function (data) {
- console.log(data)
- ws.send('234')
- console.log('onmessage' + data)
- }
- ws.onerror = function () {
- console.log('onerror')
- }
- ws.onclose = function () {
- console.log('onclose')
- }
- });
- app.listen(8000, (err) => {
- if (!err) { console.log('監聽OK') } else {
- console.log('監聽失敗')
- }
- })
webSocket的報文格式有一些不一樣:
![圖片上傳中...]
- 客戶端和服務端進行Websocket消息傳遞是這樣的:
- 客戶端:將消息切割成多個幀,并發送給服務端。
- 服務端:接收消息幀,并將關聯的幀重新組裝成完整的消息。
即時通訊的心跳檢測:
pingandpong
- 服務端Go實現:
- package main
- import (
- "net/http"
- "time"
- "github.com/gorilla/websocket"
- )
- var (
- //完成握手操作
- upgrade = websocket.Upgrader{
- //允許跨域(一般來講,websocket都是獨立部署的)
- CheckOrigin:func(r *http.Request) bool {
- return true
- },
- }
- )
- func wsHandler(w http.ResponseWriter, r *http.Request) {
- var (
- conn *websocket.Conn
- err error
- data []byte
- )
- //服務端對客戶端的http請求(升級為websocket協議)進行應答,應答之后,協議升級為websocket,http建立連接時的tcp三次握手將保持。
- if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
- return
- }
- //啟動一個協程,每隔5s向客戶端發送一次心跳消息
- go func() {
- var (
- err error
- )
- for {
- if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
- return
- }
- time.Sleep(5 * time.Second)
- }
- }()
- //得到websocket的長鏈接之后,就可以對客戶端傳遞的數據進行操作了
- for {
- //通過websocket長鏈接讀到的數據可以是text文本數據,也可以是二進制Binary
- if _, data, err = conn.ReadMessage(); err != nil {
- goto ERR
- }
- if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
- goto ERR
- }
- }
- ERR:
- //出錯之后,關閉socket連接
- conn.Close()
- }
- func main() {
- http.HandleFunc("/ws", wsHandler)
- http.ListenAndServe("0.0.0.0:7777", nil)
- }
客戶端的心跳檢測(Node.js實現):
- this.heartTimer = setInterval(() => {
- if (this.heartbeatLoss < MAXLOSSTIMES) {
- events.emit('network', 'sendHeart');
- this.heartbeatLoss += 1;
- this.phoneLoss += 1;
- } else {
- events.emit('network', 'offline');
- this.stop();
- }
- if (this.phoneLoss > MAXLOSSTIMES) {
- this.PhoneLive = false;
- events.emit('network', 'phoneDisconnect');
- }
- }, 5000);
自定義即時通信協議:
從new Socket開始:
- 目前即時通訊大都使用現有大公司成熟的SDK接入,但是逼格高些還是自己重寫比較好。
- 打個小廣告,我們公司就是自己定義的即時通訊協議~招聘一位高級前端,地點深圳-深南大道,做跨平臺IM桌面應用開發的~
- 客戶端代碼實現(Node.js):
- const {Socket} = require('net')
- const tcp = new Socket()
- tcp.setKeepAlive(true);
- tcp.setNoDelay(true);
- //保持底層tcp鏈接不斷,長連接
- 指定對應域名端口號鏈接
- tcp.connect(80,166.166.0.0)
- 建立連接后
- 根據后端傳送的數據類型 使用對應不同的解析
- readUInt8 readUInt16LE readUInt32LE readIntLE等處理后得到myBuf
- const myBuf = buffer.slice(start);//從對應的指針開始的位置截取buffer
- const header = myBuf.slice(headstart,headend)//截取對應的頭部buffer
- const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
- //精確截取數據體的buffer,并且轉化成js對象
即時通訊強烈推薦使用Golang,GRPC,Prob傳輸數據。
上面的一些代碼,都在我的開源項目中:
- 手寫的靜態資源服務器,https://github.com/JinJieTan/...
- webpack-electron-react-websocket的Demo, https://github.com/JinJieTan/...
覺得寫得不錯,可以點個贊支持下,文章也借鑒了一下其他大佬的文章,但是地址都貼上來了~ 歡迎gitHub點個star哦~