這篇文章主要講解了“Socket粘包問(wèn)題的解決方法有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Socket粘包問(wèn)題的解決方法有哪些”吧!
成都網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)建站!專(zhuān)注于網(wǎng)頁(yè)設(shè)計(jì)、成都網(wǎng)站建設(shè)、微信開(kāi)發(fā)、微信小程序、集團(tuán)成都企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。核心團(tuán)隊(duì)均擁有互聯(lián)網(wǎng)行業(yè)多年經(jīng)驗(yàn),服務(wù)眾多知名企業(yè)客戶(hù);涵蓋的客戶(hù)類(lèi)型包括:成都白烏魚(yú)等眾多領(lǐng)域,積累了大量豐富的經(jīng)驗(yàn),同時(shí)也獲得了客戶(hù)的一致贊賞!
什么是 TCP 協(xié)議?
TCP 全稱(chēng)是 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)為止。
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,如下圖所示:
什么是粘包和半包問(wèn)題?
粘包問(wèn)題是指當(dāng)發(fā)送兩條消息時(shí),比如發(fā)送了 ABC 和 DEF,但另一端接收到的卻是 ABCD,像這種一次性讀取了兩條數(shù)據(jù)的情況就叫做粘包(正常情況應(yīng)該是一條一條讀取的)。
半包問(wèn)題是指,當(dāng)發(fā)送的消息是 ABC 時(shí),另一端卻接收到的是 AB 和 C 兩條信息,像這種情況就叫做半包。
為什么會(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ū)又稱(chēng)為緩存,它是內(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ū)的話(huà),每次寫(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)接收消息;
客戶(hù)端用來(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); // 獲取客戶(hù)端連接 Socket clientSocket = serverSocket.accept(); // 得到客戶(hù)端發(fā)送的流對(duì)象 try (InputStream inputStream = clientSocket.getInputStream()) { while (true) { // 循環(huán)獲取客戶(hù)端發(fā)送的信息 byte[] bytes = new byte[BYTE_LENGTH]; // 讀取客戶(hù)端發(fā)送的信息 int count = inputStream.read(bytes, 0, BYTE_LENGTH); if (count > 0) { // 成功接收到有效消息并打印 System.out.println("接收到客戶(hù)端的信息是:" + new String(bytes)); } count = 0; } } } }
客戶(hù)端代碼如下:
/** * 客戶(hù)端(只負(fù)責(zé)發(fā)送消息) */ static class ClientSocket { public static void main(String[] args) throws IOException { // 創(chuàng)建 Socket 客戶(hù)端并嘗試連接服務(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é)果如下圖所示:
通過(guò)上述結(jié)果我們可以看出,服務(wù)器端發(fā)生了粘包和半包的問(wèn)題,因?yàn)榭蛻?hù)端發(fā)送了 10 次固定的“Hi,Java.”的消息,正常的結(jié)果應(yīng)該是服務(wù)器端也接收到了 10 次固定的消息才對(duì),但現(xiàn)實(shí)的結(jié)果并非如此。
粘包和半包的解決方案
粘包和半包的解決方案有以下 3 種:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
發(fā)送方和接收方規(guī)定固定大小的緩沖區(qū),也就是發(fā)送和接收都使用固定大小的 byte[] 數(shù)組長(zhǎng)度,當(dāng)字符長(zhǎng)度不夠時(shí)使用空字符彌補(bǔ);
在 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)題了;
以特殊的字符結(jié)尾,比如以“\n”結(jié)尾,這樣我們就知道結(jié)束字符,從而避免了半包和粘包問(wèn)題(推薦解決方案)。
那么接下來(lái)我們就來(lái)演示一下,以上解決方案的具體代碼實(shí)現(xiàn)。
解決方案1:固定緩沖區(qū)大小
固定緩沖區(qū)大小的實(shí)現(xiàn)方案,只需要控制服務(wù)器端和客戶(hù)端發(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]; // 讀取客戶(hù)端發(fā)送的信息 int count = inputStream.read(bytes, 0, BYTE_LENGTH); if (count > 0) { // 接收到消息打印 System.out.println("接收到客戶(hù)端的信息是:" + new String(bytes).trim()); } count = 0; } } } }
客戶(hù)端實(shí)現(xiàn)代碼如下:
/** * 客戶(hù)端,改進(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); // 獲取客戶(hù)端連接 Socket clientSocket = serverSocket.accept(); // 使用線程池處理更多的客戶(hù)端 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)); threadPool.submit(() -> { // 消息處理 processMessage(clientSocket); }); } /** * 消息處理 * @param clientSocket */ private static void processMessage(Socket clientSocket) { // 獲取客戶(hù)端發(fā)送的消息流對(duì)象 try (BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(clientSocket.getInputStream()))) { while (true) { // 按行讀取客戶(hù)端發(fā)送的消息 String msg = bufferedReader.readLine(); if (msg != null) { // 成功接收到客戶(hù)端的消息并打印 System.out.println("接收到客戶(hù)端的信息:" + msg); } } } catch (IOException ioException) { ioException.printStackTrace(); } } }
PS:上述代碼使用了線程池來(lái)解決多個(gè)客戶(hù)端同時(shí)訪問(wèn)服務(wù)器端的問(wèn)題,從而實(shí)現(xiàn)了一對(duì)多的服務(wù)器響應(yīng)。
客戶(hù)端的實(shí)現(xiàn)代碼如下:
/** * 客戶(hù)端,改進(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)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!