中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Socket粘包問題的解決方法有哪些

發布時間:2021-10-20 16:29:24 來源:億速云 閱讀:136 作者:iii 欄目:web開發

這篇文章主要講解了“Socket粘包問題的解決方法有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Socket粘包問題的解決方法有哪些”吧!

什么是 TCP 協議?

TCP 全稱是 Transmission Control Protocol(傳輸控制協議),它由 IETF 的 RFC 793  定義,是一種面向連接的點對點的傳輸層通信協議。

TCP 通過使用序列號和確認消息,從發送節點提供有關傳輸到目標節點的數據包的傳遞的信息。TCP  確保數據的可靠性,端到端傳遞,重新排序和重傳,直到達到超時條件或接收到數據包的確認為止。

Socket粘包問題的解決方法有哪些

TCP 是 Internet 上最常用的協議,它也是實現 HTTP(HTTP 1.0/HTTP 2.0)通訊的基礎,當我們在瀏覽器中請求網頁時,計算機會將  TCP 數據包發送到 Web 服務器的地址,要求它將網頁返還給我們,Web 服務器通過發送 TCP  數據包流進行響應,然后瀏覽器將這些數據包縫合在一起以形成網頁。

TCP  的全部意義在于它的可靠性,它通過對數據包編號來對其進行排序,而且它會通過讓服務器將響應發送回瀏覽器說“已收到”來進行錯誤檢查,因此在傳輸過程中不會丟失或破壞任何數據。

目前市場上主流的 HTTP 協議使用的版本是 HTTP/1.1,如下圖所示:

Socket粘包問題的解決方法有哪些

什么是粘包和半包問題?

粘包問題是指當發送兩條消息時,比如發送了 ABC 和 DEF,但另一端接收到的卻是  ABCD,像這種一次性讀取了兩條數據的情況就叫做粘包(正常情況應該是一條一條讀取的)。

Socket粘包問題的解決方法有哪些

半包問題是指,當發送的消息是 ABC 時,另一端卻接收到的是 AB 和 C 兩條信息,像這種情況就叫做半包。

Socket粘包問題的解決方法有哪些

為什么會有粘包和半包問題?

這是因為 TCP 是面向連接的傳輸協議,TCP 傳輸的數據是以流的形式,而流數據是沒有明確的開始結尾邊界,所以 TCP  也沒辦法判斷哪一段流屬于一個消息。

粘包的主要原因:

  • 發送方每次寫入數據 < 套接字(Socket)緩沖區大小;

  • 接收方讀取套接字(Socket)緩沖區數據不夠及時。

半包的主要原因:

  • 發送方每次寫入數據 > 套接字(Socket)緩沖區大小;

  • 發送的數據大于協議的 MTU (Maximum Transmission Unit,最大傳輸單元),因此必須拆包。

小知識點:什么是緩沖區?

緩沖區又稱為緩存,它是內存空間的一部分。也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數據,這部分預留的空間就叫做緩沖區。

緩沖區的優勢以文件流的寫入為例,如果我們不使用緩沖區,那么每次寫操作 CPU  都會和低速存儲設備也就是磁盤進行交互,那么整個寫入文件的速度就會受制于低速的存儲設備(磁盤)。但如果使用緩沖區的話,每次寫操作會先將數據保存在高速緩沖區內存上,當緩沖區的數據到達某個閾值之后,再將文件一次性寫入到磁盤上。因為內存的寫入速度遠遠大于磁盤的寫入速度,所以當有了緩沖區之后,文件的寫入速度就被大大提升了。

粘包和半包問題演示

接下來我們用代碼來演示一下粘包和半包問題,為了演示的直觀性,我會設置兩個角色:

  • 服務器端用來接收消息;

  • 客戶端用來發送一段固定的消息。

然后通過打印服務器端接收到的信息來觀察粘包和半包問題。

服務器端代碼如下:

/**  * 服務器端(只負責接收消息)  */ class ServSocket {     // 字節數組的長度     private static final int BYTE_LENGTH = 20;       public static void main(String[] args) throws IOException {         // 創建 Socket 服務器         ServerSocket serverSocket = new ServerSocket(9999);         // 獲取客戶端連接         Socket clientSocket = serverSocket.accept();         // 得到客戶端發送的流對象         try (InputStream inputStream = clientSocket.getInputStream()) {             while (true) {                 // 循環獲取客戶端發送的信息                 byte[] bytes = new byte[BYTE_LENGTH];                 // 讀取客戶端發送的信息                 int count = inputStream.read(bytes, 0, BYTE_LENGTH);                 if (count > 0) {                     // 成功接收到有效消息并打印                     System.out.println("接收到客戶端的信息是:" + new String(bytes));                 }                 count = 0;             }         }     } }

客戶端代碼如下:

/**  * 客戶端(只負責發送消息)  */ static class ClientSocket {     public static void main(String[] args) throws IOException {         // 創建 Socket 客戶端并嘗試連接服務器端         Socket socket = new Socket("127.0.0.1", 9999);         // 發送的消息內容         final String message = "Hi,Java.";          // 使用輸出流發送消息         try (OutputStream outputStream = socket.getOutputStream()) {             // 給服務器端發送 10 次消息             for (int i = 0; i < 10; i++) {                 // 發送消息                 outputStream.write(message.getBytes());             }         }     } }

以上程序的通訊結果如下圖所示:

Socket粘包問題的解決方法有哪些

通過上述結果我們可以看出,服務器端發生了粘包和半包的問題,因為客戶端發送了 10 次固定的“Hi,Java.”的消息,正常的結果應該是服務器端也接收到了  10 次固定的消息才對,但現實的結果并非如此。

粘包和半包的解決方案

粘包和半包的解決方案有以下 3 種:

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2. 發送方和接收方規定固定大小的緩沖區,也就是發送和接收都使用固定大小的 byte[] 數組長度,當字符長度不夠時使用空字符彌補;

  3. 在 TCP 協議的基礎上封裝一層數據請求協議,既將數據包封裝成數據頭(存儲數據正文大小)+  數據正文的形式,這樣在服務端就可以知道每個數據包的具體長度了,知道了發送數據的具體邊界之后,就可以解決半包和粘包的問題了;

  4. 以特殊的字符結尾,比如以“\n”結尾,這樣我們就知道結束字符,從而避免了半包和粘包問題(推薦解決方案)。

那么接下來我們就來演示一下,以上解決方案的具體代碼實現。

解決方案1:固定緩沖區大小

固定緩沖區大小的實現方案,只需要控制服務器端和客戶端發送和接收字節的(數組)長度相同即可。

服務器端實現代碼如下:

/**  * 服務器端,改進版本一(只負責接收消息)  */ static class ServSocketV1 {     private static final int BYTE_LENGTH = 1024;  // 字節數組長度(收消息用)     public static void main(String[] args) throws IOException {         ServerSocket serverSocket = new ServerSocket(9091);         // 獲取到連接         Socket clientSocket = serverSocket.accept();         try (InputStream inputStream = clientSocket.getInputStream()) {             while (true) {                 byte[] bytes = new byte[BYTE_LENGTH];                 // 讀取客戶端發送的信息                 int count = inputStream.read(bytes, 0, BYTE_LENGTH);                 if (count > 0) {                     // 接收到消息打印                     System.out.println("接收到客戶端的信息是:" + new String(bytes).trim());                 }                 count = 0;             }         }     } }

客戶端實現代碼如下:

/**  * 客戶端,改進版一(只負責接收消息)  */ static class ClientSocketV1 {     private static final int BYTE_LENGTH = 1024;  // 字節長度     public static void main(String[] args) throws IOException {         Socket socket = new Socket("127.0.0.1", 9091);         final String message = "Hi,Java."; // 發送消息         try (OutputStream outputStream = socket.getOutputStream()) {             // 將數據組裝成定長字節數組             byte[] bytes = new byte[BYTE_LENGTH];             int idx = 0;             for (byte b : message.getBytes()) {                 bytes[idx] = b;                 idx++;             }             // 給服務器端發送 10 次消息             for (int i = 0; i < 10; i++) {                 outputStream.write(bytes, 0, BYTE_LENGTH);             }         }     } }

以上代碼的執行結果如下圖所示:

優缺點分析

從以上代碼可以看出,雖然這種方式可以解決粘包和半包的問題,但這種固定緩沖區大小的方式增加了不必要的數據傳輸,因為這種方式當發送的數據比較小時會使用空字符來彌補,所以這種方式就大大的增加了網絡傳輸的負擔,所以它也不是最佳的解決方案。

解決方案二:封裝請求協議

這種解決方案的實現思路是將請求的數據封裝為兩部分:數據頭+數據正文,在數據頭中存儲數據正文的大小,當讀取的數據小于數據頭中的大小時,繼續讀取數據,直到讀取的數據長度等于數據頭中的長度時才停止。

因為這種方式可以拿到數據的邊界,所以也不會導致粘包和半包的問題,但這種實現方式的編碼成本較大也不夠優雅,因此不是最佳的實現方案,因此我們這里就略過,直接來看最終的解決方案吧。

解決方案三:特殊字符結尾,按行讀取

以特殊字符結尾就可以知道流的邊界了,因此也可以用來解決粘包和半包的問題,此實現方案是我們推薦最終解決方案。

這種解決方案的核心是,使用 Java 中自帶的 BufferedReader 和  BufferedWriter,也就是帶緩沖區的輸入字符流和輸出字符流,通過寫入的時候加上 \n 來結尾,讀取的時候使用 readLine  按行來讀取數據,這樣就知道流的邊界了,從而解決了粘包和半包的問題。

服務器端實現代碼如下:

/**  * 服務器端,改進版三(只負責收消息)  */ static class ServSocketV3 {     public static void main(String[] args) throws IOException {         // 創建 Socket 服務器端         ServerSocket serverSocket = new ServerSocket(9092);         // 獲取客戶端連接         Socket clientSocket = serverSocket.accept();         // 使用線程池處理更多的客戶端         ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100,                 TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));         threadPool.submit(() -> {             // 消息處理             processMessage(clientSocket);         });     }     /**      * 消息處理      * @param clientSocket      */     private static void processMessage(Socket clientSocket) {         // 獲取客戶端發送的消息流對象         try (BufferedReader bufferedReader = new BufferedReader(                 new InputStreamReader(clientSocket.getInputStream()))) {             while (true) {                 // 按行讀取客戶端發送的消息                 String msg = bufferedReader.readLine();                 if (msg != null) {                     // 成功接收到客戶端的消息并打印                     System.out.println("接收到客戶端的信息:" + msg);                 }             }         } catch (IOException ioException) {             ioException.printStackTrace();         }     } }

PS:上述代碼使用了線程池來解決多個客戶端同時訪問服務器端的問題,從而實現了一對多的服務器響應。

客戶端的實現代碼如下:

/**  * 客戶端,改進版三(只負責發送消息)  */ static class ClientSocketV3 {     public static void main(String[] args) throws IOException {         // 啟動 Socket 并嘗試連接服務器         Socket socket = new Socket("127.0.0.1", 9092);         final String message = "Hi,Java."; // 發送消息         try (BufferedWriter bufferedWriter = new BufferedWriter(                 new OutputStreamWriter(socket.getOutputStream()))) {             // 給服務器端發送 10 次消息             for (int i = 0; i < 10; i++) {                 // 注意:結尾的 \n 不能省略,它表示按行寫入                 bufferedWriter.write(message + "\n");                 // 刷新緩沖區(此步驟不能省略)                 bufferedWriter.flush();             }         }     } }

感謝各位的閱讀,以上就是“Socket粘包問題的解決方法有哪些”的內容了,經過本文的學習后,相信大家對Socket粘包問題的解決方法有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

普定县| 鄢陵县| 雷山县| 黑龙江省| 松阳县| 青田县| 延寿县| 金秀| 兴义市| 海盐县| 墨江| 淮北市| 竹北市| 铜鼓县| 留坝县| 朝阳县| 醴陵市| 彰化县| 肇东市| 全椒县| 诸城市| 金昌市| 东乌| 车险| 通辽市| 长治市| 山阴县| 黄梅县| 岳西县| 容城县| 资源县| 鞍山市| 北辰区| 庄浪县| 上犹县| 五常市| 洛扎县| 桐乡市| 阿勒泰市| 成武县| 乌海市|