1、小故事與前言
最近返校做畢業設計,嵌入式和伺服器需要敲定一個通訊協議。
我問:"服務端選什麼協議,MQTT或者Websocket?"
他來句:"TCP"
我又確認了一遍:"用什麼通訊協議?"
他又來了句:"TCP"
給我氣笑了。最後在我的堅持下,他才勉強以他啥都能做,以我為準的理由妥協了。
簡單回想盤點下問題:
- TCP如何處理訊息半包,粘包
- TCP如何確認應用狀態
- TCP如何處理服務認證問題
他的回答:
- 訊息傳送,服務端加個標識字元檢驗,進行資料幀分割
- 發個心跳包
2、TCP與應用層協議
TCP位於傳輸層,而這位大哥想做的就是基於Socket去完成網路訊息傳輸。(Socket本身並不是協議,而是一個呼叫介面,透過Socket,我們才能使用TCP/IP協議)
MQTT,Websocket屬於應用層協議,基於TCP。
一.訊息半包、粘包
①介紹
首先,如果你使用過Java的原生Socket API,並進行過實驗,你就能知道Socket是無法處理半包與粘包的。
什麼是半包與粘包?可以參考我的Netty進階文章
②演示
測試演示:
服務端程式碼:
縮小服務端的接收緩衝區為20位元組,客戶端10次傳送"abc"資料。
請不要拿readUTF方法進行測試,sendUTF與readUTF是配套使用的,jdk原生進行了半包粘包的處理,對於傳送訊息新增了額外的訊息用於處理問題。
如readLine等方法都是基於特定條件的處理了半包粘包問題(如本方法為換行劃分一個訊息幀),不適用於通用訊息的網路傳輸。
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zko0
* @date 2023/3/5 0:02
* @description
*/
public class SimpleSocketServer {
public static void main(String[] args) {
DataInputStream dataInputStream=null;
ServerSocket serverSocket=null;
Socket accept=null;
try {
serverSocket= new ServerSocket(8081);
System.out.println("socket服務建立");
//accept
accept = serverSocket.accept();
System.out.println("client連線");
//只設定接收緩衝區大小
accept.setReceiveBufferSize(10);
InputStream inputStream = accept.getInputStream();
dataInputStream= new DataInputStream(inputStream);
while (true){
//建立byte陣列用於接收資料
byte[] bytes=new byte[10];
dataInputStream.read(bytes);
System.out.println(new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (dataInputStream!=null)dataInputStream.close();
if (serverSocket!=null)serverSocket.close();
if (accept!=null)accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客戶端程式碼:
public class SimpleSocketClient {
public static void main(String[] args) {
Socket socket =null;
DataOutputStream dataOutputStream=null;
try{
socket=new Socket("127.0.0.1", 8081);
System.out.println("socket連線");
OutputStream outputStream= socket.getOutputStream();
dataOutputStream= new DataOutputStream(outputStream);
for (int i = 0; i < 10; i++) {
dataOutputStream.writeBytes("abc");
}
dataOutputStream.flush();
while (true);
}catch (IOException e) {
e.printStackTrace();
}
}
}
測試結果:
如下圖所示,Server接收Client訊息存在著半包粘包問題,一次訊息abc無法被完整接收,在服務端第一次接收到了a,第二次接收到了bc+abc+ab一次完整+兩次一半的訊息。
很明顯,如果服務端直接透過獲取的訊息進行處理,是存在問題的,對於TCP訊息,需要進行半包和粘包的處理,才能使用訊息。
③問題解決
正如那位“高含金量選手”所說,要對半包粘包進行處理。但是這種處理不僅僅是直接使用識別符號就可以的,對於多出的額外資訊或者不足的訊息,是需要保留來拼湊出完整訊息的。這部分你可以參考一下Java中readLine()
方法
解決方式:
-
使用識別符號進行訊息幀分割
這種方式只適用於特定場景,因為在傳送的訊息中如果存在你的識別符號,那麼在進行幀解碼的時候,就會將這部分內容作為識別符號將字串分割。由於分割位置錯誤,一次訊息你可能會得到分割後的多個錯誤訊息。
就比如
readLine()
使用於讀取一行資料。 -
使用短連線
這種方式可以解決粘包的問題,但是無法解決半包問題。因為一次傳送後,連線就會中斷。那麼Server獲取的資料只會小於單次傳送的資料。
-
定長幀解碼器
在傳送資料中,定義協議。如設定前5個位元組代表傳送資料的長度。
如果傳送abc三位元組內容,傳送資料為:0 0 0 0 3 97 98 99
伺服器讀取前5個位元組,就知道需要再讀取3位元組內容,為一次完整資料。如果單次只讀取了2位元組,那麼會等待1個位元組內容,拼湊出最後的3位元組,單次完整訊息。
你可以參考
readUTF()
方法,思路相同。
二.心跳
如果你瞭解TCP,你就會知道TCP協議是自帶心跳的。
KEEP_ALIVE
在TCP協議裡,本身的心跳包機制SO_KEEPALIVE,系統預設設定的是7200秒的啟動時間,預設是關閉的。有三個引數可以設定:p_keepalive_time、tcp_keepalive_probes、tcp_keepalive_intvl
分別表示連線閒置多久才開始發心跳包、連發幾次心跳包沒有回應表示連線已斷開、心跳包之間間隔時間。
但是眾所周知,幾乎所有基於TCP協議的應用層協議,都自定義了心跳報文:如WebSocket,MQTT
Socket中Client關閉,Server能自動感受到連線斷開啊?:
當SocketClient應用關閉,系統呼叫Socket的close、或者程式結束。作業系統會傳送FIN
資料包給伺服器,所以Sevrer能夠關閉TCP連線。
如果你在Client執行中,拔掉網線或者關閉電源。這時Client是沒有機會傳送FIN
資料包的,這時Server就無法感知到Client已經斷開了連線。
為什麼TCP自帶心跳還需要應用層定義心跳:
- TCP心跳機制是傳輸層實現的,只要當前連線是可用的,對端就會ACK我們的心跳,而對於當前對端應用是否能正常提供服務,TCP層的心跳機制是無法獲知的。
- tcp_keepalive_time引數的設定是秒級,對於極端情況,我們可能想在毫秒級就檢測到連線的狀態,這個TCP心跳機制就無法辦到
- 通用性,應用層心跳功能不依賴於傳輸層協議,如果有一天我們想將傳輸層協議由TCP改為UDP,那麼傳輸層不提供心跳機制了,應用層的心跳是通用的,此時或許只用修改少量地方程式碼即可;
- 如果連線中存在Sock Proxy或者NAT,他們可能不會處理TCP的Keep Alive包
三.應用報文
這部分比較偏向應用層內容,僅為個人感想。
在應用層協議中,協議報文都是對應用每個功能定製,且縮小了資料量的。
以MQTT3.1舉例
MQTT第一次報文為CONNETCT報文。在一個網路連線上,客戶端只能傳送一次CONNECT報文。服務端必須將客戶端傳送的第二個CONNECT報文當作協議違規處理並斷開客戶端的連線 。
報文內容:
-
固定頭:1--->確定了報文的型別:CONNECT
-
可變頭:
- 第6位:使用者名稱標誌 User Name Flag
- 第7位:密碼標誌 Password Flag
-
有效載荷:CONNECT報文的有效載荷(payload)包含一個或多個以長度為字首的欄位,可變報頭中的標誌決定是否包含這些欄位。如果包含的話,必須按這個順序出現:客戶端識別符號,遺囑主題,遺囑訊息,使用者名稱,密碼。
僅僅是一個客戶端連線報文,就設計的非常全面且優雅,且完全契合應用目的。
3、小結
個人思考後,個人認為將應用層協議分為通用協議
與特定系統協議
倆種。
通用協議:如HTTP協議,Websocket,MQTT協議,搭載使用者自定義資料體進行通訊
特定系統協議:如Mysql協議,Redis的RESP協議,都是基於特定應用自己開發的一套網路協議
對於應用開發,個人認為通用協議更為合理,不僅進行了上述眾多問題的處理。而且還有對應協議的應用意義:如身份驗證等。
實在難以想象那種程式碼水平是否能將Socket自定義協議與訊息處理寫出基本的骨架出來。希望技術人員專注於技術,共同進步,不要盲目自大。
如果你有較好思考,歡迎指點。