溫馨提示×

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

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Socket粘包問(wèn)題的解決方法有哪些

發(fā)布時(shí)間:2021-10-20 16:29:24 來(lái)源:億速云 閱讀:130 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要講解了“Socket粘包問(wèn)題的解決方法有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Socket粘包問(wèn)題的解決方法有哪些”吧!

什么是 TCP 協(xié)議?

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

TCP 通過(guò)使用序列號(hào)和確認(rèn)消息,從發(fā)送節(jié)點(diǎn)提供有關(guān)傳輸?shù)侥繕?biāo)節(jié)點(diǎn)的數(shù)據(jù)包的傳遞的信息。TCP  確保數(shù)據(jù)的可靠性,端到端傳遞,重新排序和重傳,直到達(dá)到超時(shí)條件或接收到數(shù)據(jù)包的確認(rèn)為止。

Socket粘包問(wèn)題的解決方法有哪些

TCP 是 Internet 上最常用的協(xié)議,它也是實(shí)現(xiàn) HTTP(HTTP 1.0/HTTP 2.0)通訊的基礎(chǔ),當(dāng)我們?cè)跒g覽器中請(qǐng)求網(wǎng)頁(yè)時(shí),計(jì)算機(jī)會(huì)將  TCP 數(shù)據(jù)包發(fā)送到 Web 服務(wù)器的地址,要求它將網(wǎng)頁(yè)返還給我們,Web 服務(wù)器通過(guò)發(fā)送 TCP  數(shù)據(jù)包流進(jìn)行響應(yīng),然后瀏覽器將這些數(shù)據(jù)包縫合在一起以形成網(wǎng)頁(yè)。

TCP  的全部意義在于它的可靠性,它通過(guò)對(duì)數(shù)據(jù)包編號(hào)來(lái)對(duì)其進(jìn)行排序,而且它會(huì)通過(guò)讓服務(wù)器將響應(yīng)發(fā)送回瀏覽器說(shuō)“已收到”來(lái)進(jìn)行錯(cuò)誤檢查,因此在傳輸過(guò)程中不會(huì)丟失或破壞任何數(shù)據(jù)。

目前市場(chǎng)上主流的 HTTP 協(xié)議使用的版本是 HTTP/1.1,如下圖所示:

Socket粘包問(wèn)題的解決方法有哪些

什么是粘包和半包問(wèn)題?

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

Socket粘包問(wèn)題的解決方法有哪些

半包問(wèn)題是指,當(dāng)發(fā)送的消息是 ABC 時(shí),另一端卻接收到的是 AB 和 C 兩條信息,像這種情況就叫做半包。

Socket粘包問(wèn)題的解決方法有哪些

為什么會(huì)有粘包和半包問(wèn)題?

這是因?yàn)?TCP 是面向連接的傳輸協(xié)議,TCP 傳輸?shù)臄?shù)據(jù)是以流的形式,而流數(shù)據(jù)是沒(méi)有明確的開(kāi)始結(jié)尾邊界,所以 TCP  也沒(méi)辦法判斷哪一段流屬于一個(gè)消息。

粘包的主要原因:

  • 發(fā)送方每次寫(xiě)入數(shù)據(jù) < 套接字(Socket)緩沖區(qū)大小;

  • 接收方讀取套接字(Socket)緩沖區(qū)數(shù)據(jù)不夠及時(shí)。

半包的主要原因:

  • 發(fā)送方每次寫(xiě)入數(shù)據(jù) > 套接字(Socket)緩沖區(qū)大小;

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

小知識(shí)點(diǎn):什么是緩沖區(qū)?

緩沖區(qū)又稱為緩存,它是內(nèi)存空間的一部分。也就是說(shuō),在內(nèi)存空間中預(yù)留了一定的存儲(chǔ)空間,這些存儲(chǔ)空間用來(lái)緩沖輸入或輸出的數(shù)據(jù),這部分預(yù)留的空間就叫做緩沖區(qū)。

緩沖區(qū)的優(yōu)勢(shì)以文件流的寫(xiě)入為例,如果我們不使用緩沖區(qū),那么每次寫(xiě)操作 CPU  都會(huì)和低速存儲(chǔ)設(shè)備也就是磁盤(pán)進(jìn)行交互,那么整個(gè)寫(xiě)入文件的速度就會(huì)受制于低速的存儲(chǔ)設(shè)備(磁盤(pán))。但如果使用緩沖區(qū)的話,每次寫(xiě)操作會(huì)先將數(shù)據(jù)保存在高速緩沖區(qū)內(nèi)存上,當(dāng)緩沖區(qū)的數(shù)據(jù)到達(dá)某個(gè)閾值之后,再將文件一次性寫(xiě)入到磁盤(pán)上。因?yàn)閮?nèi)存的寫(xiě)入速度遠(yuǎn)遠(yuǎn)大于磁盤(pán)的寫(xiě)入速度,所以當(dāng)有了緩沖區(qū)之后,文件的寫(xiě)入速度就被大大提升了。

粘包和半包問(wèn)題演示

接下來(lái)我們用代碼來(lái)演示一下粘包和半包問(wèn)題,為了演示的直觀性,我會(huì)設(shè)置兩個(gè)角色:

  • 服務(wù)器端用來(lái)接收消息;

  • 客戶端用來(lái)發(fā)送一段固定的消息。

然后通過(guò)打印服務(wù)器端接收到的信息來(lái)觀察粘包和半包問(wèn)題。

服務(wù)器端代碼如下:

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

客戶端代碼如下:

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

以上程序的通訊結(jié)果如下圖所示:

Socket粘包問(wèn)題的解決方法有哪些

通過(guò)上述結(jié)果我們可以看出,服務(wù)器端發(fā)生了粘包和半包的問(wèn)題,因?yàn)榭蛻舳税l(fā)送了 10 次固定的“Hi,Java.”的消息,正常的結(jié)果應(yīng)該是服務(wù)器端也接收到了  10 次固定的消息才對(duì),但現(xiàn)實(shí)的結(jié)果并非如此。

粘包和半包的解決方案

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

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 發(fā)送方和接收方規(guī)定固定大小的緩沖區(qū),也就是發(fā)送和接收都使用固定大小的 byte[] 數(shù)組長(zhǎng)度,當(dāng)字符長(zhǎng)度不夠時(shí)使用空字符彌補(bǔ);

  3. 在 TCP 協(xié)議的基礎(chǔ)上封裝一層數(shù)據(jù)請(qǐng)求協(xié)議,既將數(shù)據(jù)包封裝成數(shù)據(jù)頭(存儲(chǔ)數(shù)據(jù)正文大小)+  數(shù)據(jù)正文的形式,這樣在服務(wù)端就可以知道每個(gè)數(shù)據(jù)包的具體長(zhǎng)度了,知道了發(fā)送數(shù)據(jù)的具體邊界之后,就可以解決半包和粘包的問(wèn)題了;

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

那么接下來(lái)我們就來(lái)演示一下,以上解決方案的具體代碼實(shí)現(xiàn)。

解決方案1:固定緩沖區(qū)大小

固定緩沖區(qū)大小的實(shí)現(xiàn)方案,只需要控制服務(wù)器端和客戶端發(fā)送和接收字節(jié)的(數(shù)組)長(zhǎng)度相同即可。

服務(wù)器端實(shí)現(xiàn)代碼如下:

/**  * 服務(wù)器端,改進(jìn)版本一(只負(fù)責(zé)接收消息)  */ static class ServSocketV1 {     private static final int BYTE_LENGTH = 1024;  // 字節(jié)數(shù)組長(zhǎng)度(收消息用)     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];                 // 讀取客戶端發(fā)送的信息                 int count = inputStream.read(bytes, 0, BYTE_LENGTH);                 if (count > 0) {                     // 接收到消息打印                     System.out.println("接收到客戶端的信息是:" + new String(bytes).trim());                 }                 count = 0;             }         }     } }

客戶端實(shí)現(xiàn)代碼如下:

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

以上代碼的執(zhí)行結(jié)果如下圖所示:

優(yōu)缺點(diǎn)分析

從以上代碼可以看出,雖然這種方式可以解決粘包和半包的問(wèn)題,但這種固定緩沖區(qū)大小的方式增加了不必要的數(shù)據(jù)傳輸,因?yàn)檫@種方式當(dāng)發(fā)送的數(shù)據(jù)比較小時(shí)會(huì)使用空字符來(lái)彌補(bǔ),所以這種方式就大大的增加了網(wǎng)絡(luò)傳輸?shù)呢?fù)擔(dān),所以它也不是最佳的解決方案。

解決方案二:封裝請(qǐng)求協(xié)議

這種解決方案的實(shí)現(xiàn)思路是將請(qǐng)求的數(shù)據(jù)封裝為兩部分:數(shù)據(jù)頭+數(shù)據(jù)正文,在數(shù)據(jù)頭中存儲(chǔ)數(shù)據(jù)正文的大小,當(dāng)讀取的數(shù)據(jù)小于數(shù)據(jù)頭中的大小時(shí),繼續(xù)讀取數(shù)據(jù),直到讀取的數(shù)據(jù)長(zhǎng)度等于數(shù)據(jù)頭中的長(zhǎng)度時(shí)才停止。

因?yàn)檫@種方式可以拿到數(shù)據(jù)的邊界,所以也不會(huì)導(dǎo)致粘包和半包的問(wèn)題,但這種實(shí)現(xiàn)方式的編碼成本較大也不夠優(yōu)雅,因此不是最佳的實(shí)現(xiàn)方案,因此我們這里就略過(guò),直接來(lái)看最終的解決方案吧。

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

以特殊字符結(jié)尾就可以知道流的邊界了,因此也可以用來(lái)解決粘包和半包的問(wèn)題,此實(shí)現(xiàn)方案是我們推薦最終解決方案。

這種解決方案的核心是,使用 Java 中自帶的 BufferedReader 和  BufferedWriter,也就是帶緩沖區(qū)的輸入字符流和輸出字符流,通過(guò)寫(xiě)入的時(shí)候加上 \n 來(lái)結(jié)尾,讀取的時(shí)候使用 readLine  按行來(lái)讀取數(shù)據(jù),這樣就知道流的邊界了,從而解決了粘包和半包的問(wèn)題。

服務(wù)器端實(shí)現(xiàn)代碼如下:

/**  * 服務(wù)器端,改進(jìn)版三(只負(fù)責(zé)收消息)  */ static class ServSocketV3 {     public static void main(String[] args) throws IOException {         // 創(chuàng)建 Socket 服務(wù)器端         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) {         // 獲取客戶端發(fā)送的消息流對(duì)象         try (BufferedReader bufferedReader = new BufferedReader(                 new InputStreamReader(clientSocket.getInputStream()))) {             while (true) {                 // 按行讀取客戶端發(fā)送的消息                 String msg = bufferedReader.readLine();                 if (msg != null) {                     // 成功接收到客戶端的消息并打印                     System.out.println("接收到客戶端的信息:" + msg);                 }             }         } catch (IOException ioException) {             ioException.printStackTrace();         }     } }

PS:上述代碼使用了線程池來(lái)解決多個(gè)客戶端同時(shí)訪問(wèn)服務(wù)器端的問(wèn)題,從而實(shí)現(xiàn)了一對(duì)多的服務(wù)器響應(yīng)。

客戶端的實(shí)現(xiàn)代碼如下:

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

感謝各位的閱讀,以上就是“Socket粘包問(wèn)題的解決方法有哪些”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)Socket粘包問(wèn)題的解決方法有哪些這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI