文章目錄
- 1、 什么是 WebSocket?與 Http 協議的區別是什么?
- 2、 Http 是如何升級為 WebSocket 的?
- 3、 為什么 WebSocket 可以進行全雙工模式的消息傳輸,而 Http 不可以?
- 4、 什么是 TCP 的沾包和拆包?
- 5、 WebSocket 與 Socket 套接字的區別是什么?
- 6、 keep-alive 是什么?
- 7、 網線被一腳踢斷了,TCP 連接會發生什么事情?
- 8、SSE協議了解嗎?為什么用websocket而不用SSE?
- 9、 Websocket如何保證消息可靠性?(重點強調不是安全性)
1、 什么是 WebSocket?與 Http 協議的區別是什么?
WebSocket 是一種應用層協議,用于在 Web 應用程序中創建實時、雙向的通信通道。傳統的 HTTP 請求通常是一次請求一次響應的,而 WebSocket 則可以建立一個持久連接,允許服務器即時向客戶端推送數據,同時也可以接收客戶端發送的數據。WebSocket 相比于傳統的輪詢或長輪詢方式,能夠顯著減少網絡流量和延遲,提高數據傳輸的效率和速度。
WebSocket 可以在瀏覽器和服務器之間建立一條雙向通信的通道,實現服務器主動向瀏覽器推送消息,而無需瀏覽器向服務器不斷發送請求。其原理是在瀏覽器和服務器之間建立一個“套接字”,通過“握手”的方式進行數據傳輸。由于該協議需要瀏覽器和服務器都支持,因此需要在應用程序中對其進行判斷和處理。
WebSocket 建立在 HTTP 協議之上
,所有的 WebSocket 請求都會通過普通的 HTTP 協議發送出去,然后在服務器端根據 HTTP 協議識別特定的頭信息 Upgrade,服務端也會判斷請求信息中 Upgrade 是否存在。 這里面 HTTP 是必不可少的,不然 WebSocket 根本無法建立。特別的,WebSocket 在握手時采用了 Sec-WebSocket-Key 加密處理,并采用 SHA-1 (hash)簽名。
一旦建立了 WebSocket 連接,客戶端和服務器端就可以互相發送二進制流或 Unicode 字符串。所有的數據都是經過 mask 處理過的,mask 的值是由服務器端隨機生成的。在數據進行發送之前,必須先進行 mask 處理,這樣可以有效防止數據被第三方惡意篡改。
最后需要說明一下的是,WebSocket 的通信協議是基于幀(數據包)的。在數據發送時,一個完整的數據包可以分為多個幀進行發送(拆包/沾包),而每一個幀都包含了數據的一部分,同時還包含了幀頭信息。
WebSocket 與 Http 協議的區別
- HTTP (Hypertext Transfer Protocol) 是一個基于請求和響應模式的協議,最早用于 Web 應用。
- WebSocket 是一種雙向通信協議,可以在客戶端和服務器之間建立持久的連接,以實現實時通信。
- WebSocket 協議在建立連接時需要使用 HTTP 協議。 具體來說,當客戶端想要建立 WebSocket 連接時,它們需要通過 HTTP 請求發送一個握手請求。如果服務器同意握手,它將發送一個握手響應,HTTP 協議隨后會升級到 WebSocket 協議。
- HTTP 和 WebSocket 協議在用途上也有所不同。 HTTP 協議主要用于客戶端和服務器之間的請求和響應通信,而 WebSocket 協議主要用于實現實時通信和服務器推送。通俗的說,HTTP 協議是“一問一答”,WebSocket 協議是“對話”。
2、 Http 是如何升級為 WebSocket 的?
3、 為什么 WebSocket 可以進行全雙工模式的消息傳輸,而 Http 不可以?
你可能會問,既然HTTP 與WebSocket 底層都是通過 TCP 進行消息傳輸的,為什么 Http 協議卻不能服務端主動給客戶端進行主動進行消息推送?其實這并不是 TCP 不支持全雙工的工作模式,HTTP的設計初衷是文檔傳輸,不是實時通信,所以沒有考慮持續的雙向通信需求。因此這是由于應用層協議設計的問題導致的 Http 協議不支持全雙工的工作模式。
4、 什么是 TCP 的沾包和拆包?
在回答這個問題之前我們需要明確沾包和拆包的定義是什么,如下
- 粘包(Sticky Packets)
發送方連續發送多個數據包時,接收方可能一次性收到多個包"粘"在一起的數據(如:發送 A|B,接收AB)。 - 拆包(Unpacking)
發送方的一個數據包可能被拆分成多次接收(如:發送 C,接收為 C1 和 C2)
為什么會出現沾包和拆包這個問題呢?其根本原因是 TCP 面向的是字節流(字節流是你可以認為是一個一直在流水的水管)的協議,TCP 壓根就不知道你想發送的每個數據包之間的邊界是什么,那么如果你發送的一段信息過長,超過了 TCP 的緩沖區,那就會導致你的一個大的數據包被拆分為若干小的數據包進行發送,這就是拆包;當你的一個數據包過小,那么 TCP 會認為頻繁發送小數據包會頻繁進行網絡傳輸,因此會使用 Nagle 算法合并小數據包,這就是沾包。
解決方案
(1) 固定消息長度 - 每個數據包長度固定(如1024字節),不足部分用空位填充。
- 缺點:浪費帶寬,不靈活。
(2) 分隔符協議 - 用特殊字符(如\n、\r\n)標記消息邊界。
- 示例:Redis協議使用\r\n分割命令。
(3) 消息頭聲明長度 - 在消息頭部添加長度字段(如4字節int),明確后續數據的長度。
- 示例:HTTP協議通過Content-Length頭指定正文長度。
在本項目中 netty 框架已經幫我們在應用層方面解決了這個問題,Netty框架:內置解碼器如LengthFieldBasedFrameDecoder 自動處理粘包/拆包。
5、 WebSocket 與 Socket 套接字的區別是什么?
首先,Socket套接字,屬于傳輸層的API,像TCP和UDP都是基于Socket實現的。它允許應用程序通過網絡進行通信,是操作系統提供的接口。而WebSocket是一種應用層協議,建立在HTTP之上,提供全雙工通信,通常用于瀏覽器和服務器之間的實時交互。換句話說二者不屬于同一層面的東西。
6、 keep-alive 是什么?
TCP Keepalive機制 是一種用于檢測空閑連接是否存活的傳輸層保活策略。它通過定期發送探測報文,確認對端是否仍在線,避免因網絡中斷或對端崩潰導致連接長期占用資源。
- Keepalive 的工作原理
(1) 觸發條件
- 空閑超時:當連接在 tcp_keepalive_time 時間內無數據交互,觸發Keepalive探測。
- 探測過程:發送空數據包(ACK標志,序列號為當前期望值減1),強制對端響應。
(2) 探測流程 - 發送探測包:每隔 tcp_keepalive_intvl 秒發送一次探測包。
- 等待響應:若收到有效ACK,重置計時器,連接保持。
- 失敗判定:連續 tcp_keepalive_probes 次無響應,判定連接死亡,發送RST斷開。
# 臨時生效(重啟后失效)
sysctl -w net.ipv4.tcp_keepalive_time=600 # 空閑10分鐘后探測
sysctl -w net.ipv4.tcp_keepalive_intvl=30 # 每隔30秒發送探測
sysctl -w net.ipv4.tcp_keepalive_probes=3 # 最多探測3次# 永久生效:將配置寫入 /etc/sysctl.conf
echo "net.ipv4.tcp_keepalive_time = 600" >> /etc/sysctl.conf
sysctl -p
7、 網線被一腳踢斷了,TCP 連接會發生什么事情?
首先,我們要回憶一下 TCP 的基礎知識,TCP 在會在什么情況下斷開一個鏈接呢?
- 連接雙方的一端主動進行 TCP 連接的關閉,就是我們常說的發送 FIN 包,進行四次揮手;
- 重傳次數達到閾值,TCP有重傳機制,如果多次重傳失敗,導致 TCP 連接斷開;
- Keepalive 機制如果啟用,在探測無響應后也會斷開連接;
- 接收到 RST 終止報文
…
既然我們已經知道了 TCP 鏈接可能被關閉的情況,那么我們就可以對這個問題進行回答。首先分情況進行討論,如果是網線被踢開了,那么我們軟件系統其實是無法馬上感知物理鏈路已經斷開了,TCP 連接狀態仍然ESTABLISHED狀態,此時如果你馬上把斷掉的網線插回去,那么其實 TCP 會當作什么都沒發生,因為他根本沒有感知到剛才的那次網線的斷開。如果你在網線斷開的時候,進行了消息的發送,那么由于物理網絡的斷開,那么你這條消息一定是發送不出去的,會觸發 TCP 的重傳機制,進行重傳,如果你在重傳時,將網線插回,那么這個 TCP 連接仍然不會斷開,因為重傳成功了,否則 TCP 連接將會被斷開。那么如果網線一直沒有被插回去,并且客戶端與服務端一直沒有通過這個 TCP 進行通信,那么這個 TCP 連接將會一直存在,那有沒有一個好的方法解決這個問題呢?答案是開啟Keepalive機制,Keepalive 機制簡單的說,就是定期的向 TCP 連接中發送探測數據包,來確保這條 TCP 連接是正常存活的,就像我們在 IM 項目的長鏈接模塊中,加入了心跳機制一樣。如果 Keepalive 探測數據包沒有得到響應,會幫我們自動關閉這條連接。
8、SSE協議了解嗎?為什么用websocket而不用SSE?
SSE是一種基于HTTP協議的服務器推送技術,允許服務器單向地向客戶端推送事件流數據。它的主要特點是:
- 單向通信: 數據只能從服務器流向客戶端。
- 基于HTTP: 無需新的協議,復用HTTP連接。
- 文本協議: 主要傳輸UTF-8編碼的文本數據。
- 自動重連: 瀏覽器標準中定義了斷線重連機制。
- 實現相對簡單: 相比WebSocket,服務器端實現可能更簡單一些。
我們項目選擇WebSocket而不是SSE,主要是基于以下考慮:
- 核心需求是雙向通信: 我們的項目是一個即時通訊(IM)系統。用戶不僅需要接收來自服務器的消息推送(如新消息、在線狀態更新),還需要能夠實時地向服務器發送消息、發送已讀回執、發送正在輸入狀態等操作。WebSocket提供了全雙工通信能力,允許客戶端和服務器在建立連接后,隨時互相發送數據。而SSE是單向的,客戶端無法通過同一個SSE連接向服務器發送數據,如果需要客戶端發送,就必須發起新的HTTP請求,這違背了IM系統低延遲、高實時性的要求。
- 協議開銷和效率: 雖然WebSocket的握手階段比SSE復雜,但在連接建立后,其數據幀的協議開銷相對較小,尤其適合頻繁、小數據包的傳輸場景,這與IM的消息傳輸特性相符。如果使用SSE推送、HTTP發送,會增加額外的連接建立和請求開銷。
- 功能豐富性: WebSocket不僅支持文本數據,還原生支持二進制數據傳輸,這為未來可能擴展的功能(如傳輸圖片、文件、音視頻信令等)提供了更好的基礎。
總結來說, 對于需要客戶端和服務端進行高頻實時雙向交互的IM應用場景,WebSocket的全雙工通信能力是其相比SSE最核心的優勢,也是我們選擇它的根本原因。SSE更適用于只需要服務器向客戶端單向推送信息的場景,例如股票行情更新、新聞推送等。
9、 Websocket如何保證消息可靠性?(重點強調不是安全性)
WebSocket協議本身是建立在TCP之上的,TCP協議提供了傳輸層的可靠性,包括數據包的順序保證和丟包重傳機制。但這并不等同于應用層消息的可靠性。比如,TCP連接可能意外斷開,或者服務器/客戶端在處理消息時崩潰,或者網絡長時間抖動導致TCP認為連接超時。因此,為了確保WebSocket上的應用層消息傳遞真正可靠(即消息不丟失、不重復、基本有序),我們在應用層(也就是在 Real-TimeCommunicationService 和客戶端之間)需要實現額外的機制:
1.消息確認機制 (ACK): 這是最核心的機制。
- 發送方: 每發送一條需要保證可靠性的消息時,會附帶一個唯一的、單調遞增的消息ID(或序列號)。同時啟動一個定時器,等待接收方的確認應答(ACK)。
- 接收方: 收到消息后,進行處理。處理成功后,向發送方回復一個ACK消息,其中包含收到的消息ID。
- 超時與重傳: 如果發送方在定時器超時前沒有收到對應的ACK,就認為消息可能丟失,會進行重傳(可能需要設定最大重傳次數和退避策略)。
- 消息序列號 (Sequence Number): 為了保證消息的基本有序性(尤其是在重傳發生時),發送方可以為每個會話(或用戶連接)維護一個遞增的序列號。接收方可以根據序列號來判斷消息是否重復或失序。對于失序的消息,可以先緩存,等待前面的消息到達后再處理,或者直接丟棄(取決于業務要求)。
3.** 冪等性保證**: 由于存在重傳機制,接收方可能會收到重復的消息。接收方需要具備冪等處理能力。通常可以通過檢查收到的消息ID是否已經被處理過來實現。例如,在Redis中記錄最近處理過的一批消息ID,或者在數據庫存儲消息時利用唯一ID約束。 - 心跳機制 (Heartbeat/Ping-Pong): 定期由客戶端或服務器(或雙方)發送心跳消息(Ping),對方收到后回復Pong。如果在一定時間內沒有收到對方的心跳響應,就認為連接已經失效。這有助于及早發現“假死”連接,觸發重連和后續的消息同步/重發邏輯。Netty本身提供了 IdleStateHandler 來方便地實現心跳檢測。
- 重連后的消息同步: 當連接斷開并重新建立后,客戶端需要與服務器同步狀態,拉取在離線期間可能錯過的消息。在我們的設計中,這部分主要是通過登錄時(包括重連后的自動登錄)與 OfflineDataStore 通過拉取離線消息來實現。對于重連前的“飛行中”消息(已發出但未確認ACK的),可以通過上述的ACK和序列號機制來決定是否需要重發。
通過這些應用層的機制組合,我們在TCP提供的基礎傳輸可靠性之上,構建了更強的應用層消息可靠性保障,確保IM消息能夠盡可能地準確、完整、有序地送達。