爲何咱們要熟悉這些通訊協議? 【精讀】

打個廣告,歡迎加入咱們的前端開發交流羣:

  • 微信羣:

clipboard.png

  • QQ羣

clipboard.png

前端的最重要的基礎知識點是什麼?

  • 原生javaScriptHTML,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客戶端和服務器端成功地創建鏈接,能夠開始傳輸數據了。
如圖所示:

clipboard.png

TCP的四次揮手:
  • 創建一個鏈接須要三次握手,而終止一個鏈接要通過四次握手,這是由TCP的半關閉(half-close)形成的。具體過程以下圖所示。
  • 某個應用進程首先調用close,稱該端執行「主動關閉」(active close)。該端的TCP因而發送一個FIN分節,表示數據發送完畢。
  • 接收到這個FIN的對端執行 「被動關閉」(passive close),這個FIN由TCP確認。

注意:FIN的接收也做爲一個文件結束符(end-of-file)傳遞給接收端應用進程,放在已排隊等候該應用進程接收的任何其餘數據以後,由於,FIN的接收意味着接收端應用進程在相應鏈接上再無額外數據可接收。css

  • 一段時間後,接收到這個文件結束符的應用進程將調用close關閉它的套接字。這致使它的TCP也發送一個FIN。
  • 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。 [3]

既然每一個方向都須要一個FIN和一個ACK,所以一般須要4個分節。html

特別提示: SYN報文用來通知, FIN報文是用來同步的

clipboard.png

以上就是面試官常問的三次握手,四次揮手,可是這不只僅面試題,上面僅僅答到了一點皮毛,學習這些是爲了讓咱們後續方便了解他的優缺點。

TCP鏈接創建後,咱們能夠有多種協議的方式通訊交換數據:

最古老的方式一:http 1.0

  • 早先1.0的HTTP版本,是一種無狀態、無鏈接的應用層協議。
  • HTTP1.0規定瀏覽器和服務器保持短暫的鏈接,瀏覽器的每次請求都須要與服務器創建一個TCP鏈接,服務器處理完成後當即斷開TCP鏈接(無鏈接),服務器不跟蹤每一個客戶端也不記錄過去的請求(無狀態)。
  • 這種無狀態性能夠藉助cookie/session機制來作身份認證和狀態記錄。而下面兩個問題就比較麻煩了。
  • 首先,無鏈接的特性致使最大的性能缺陷就是沒法複用鏈接。每次發送請求的時候,都須要進行一次TCP的鏈接,而TCP的鏈接釋放過程又是比較費事的。這種無鏈接的特性會使得網絡的利用率很是低。
  • 其次就是隊頭阻塞(headoflineblocking)。因爲HTTP1.0規定下一個請求必須在前一個請求響應到達以前才能發送。假設前一個請求響應一直不到達,那麼下一個請求就不發送,一樣的後面的請求也給阻塞了。
Http 1.0的致命缺點,就是沒法複用 TCP鏈接和並行發送請求,這樣每次一個請求都須要三次握手,並且其實創建鏈接和釋放鏈接的這個過程是最耗時的,傳輸數據相反卻不那麼耗時。還有本地時間被修改致使響應頭 expires的緩存機制失效的問題~(後面會詳細講)
  • 常見的請求報文~

clipboard.png

因而出現了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鏈接~

clipboard.png

  • 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的性能對比:

clipboard.png

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 通訊。java

  • 當使用 SSL 時,則演變成先和 SSL 通訊,再由 SSL和 TCP 通訊了。簡言之,所謂 HTTPS,其實就是身披 SSL 協議這層外殼的HTTP。
  • 在採用 SSL 後,HTTP 就擁有了 HTTPS 的加密、證書和完整性保護這些功能。SSL 是獨立於 HTTP 的協議,因此不光是 HTTP 協議,其餘運行在應用層的 SMTP和 Telnet 等協議都可配合 SSL 協議使用。能夠說 SSL 是當今世界上應用最爲普遍的網絡安全術。

clipboard.png

相互交換密鑰的公開密鑰加密技術 -----對稱加密

clipboard.png

  • 在對 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加速器的功效,以分擔負載。react

爲何不一直使用 HTTPS

  • 既然 HTTPS 那麼安全可靠,那爲什麼全部的 Web 網站不一直使用 HTTPS?

其中一個緣由是,由於與純文本通訊相比,加密通訊會消耗更多的 CPU 及內存資源。若是每次通訊都加密,會消耗至關多的資源,平攤到一臺計算機上時,可以處理的請求數量一定也會隨之減小。webpack

  • 所以,若是是非敏感信息則使用 HTTP 通訊,只有在包含我的信息等敏感數據時,才利用 HTTPS 加密通訊。

特別是每當那些訪問量較多的 Web 網站在進行加密處理時,它們所承擔着的負載不容小覷。在進行加密處理時,並不是對全部內容都進行加密處理,而是僅在那些須要信息隱藏時纔會加密,以節約資源。git

  • 除此以外,想要節約購買證書的開銷也是緣由之一。

要進行 HTTPS 通訊,證書是必不可少的。而使用的證書必須向認證機構(CA)購買。證書價格可能會根據不一樣的認證機構略有不一樣。一般,一年的受權須要數萬日元(如今一萬日元大約摺合 600 人民幣)。那些購買證書並不合算的服務以及一些我的網站,可能只會選擇採用HTTP 的通訊方式。github

clipboard.png

複習完了基本的協議,介紹下報文格式:

  • 請求報文格式

clipboard.png

  • 響應報文格式

clipboard.png

所謂響應頭,請求頭,其實均可以本身添加字段,只要先後端給對應的處理機制便可

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))
                    }
}

響應頭的詳解:

clipboard.png

本人的開源項目,手寫的Node.js靜態資源服務器,https://github.com/JinJieTan/...,歡迎 star~

瀏覽器的緩存策略:

  • 首次請求:

clipboard.png

  • 非首次請求:

clipboard.png

  • 用戶行爲與緩存:

clipboard.png

不能緩存的請求:

沒法被瀏覽器緩存的請求以下:

  • 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的報文格式有一些不同:

![圖片上傳中...]web

  • 客戶端和服務端進行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傳輸數據。

上面的一些代碼,都在個人開源項目中:

以爲寫得不錯,能夠點個贊支持下,文章也借鑑了一下其餘大佬的文章,可是地址都貼上來了~ 歡迎 gitHub點個 star哦~
相關文章
相關標籤/搜索