曾幾什麼時候,咱們從一些書上看到了這樣一個詞——粘包。粘包,包子粘在一塊兒了?這跟tcp有啥關係。
因此,咱們google
了一下,跳到了百度
,瞧到這樣一段解釋:html
網絡技術術語。指TCP協議中,發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。
看得我是一愣一愣的,TCP
啥時候有包這個概念了,不是一直都是字節流嗎?java
tcp
tcp
是什麼東西來的?
你們這東西聽多了吧,但讓你說一下這是啥東西,怎麼說呢?
咱們仍是抄一下百科上面的定義吧緩存
傳輸控制協議(TCP,Transmission Control Protocol)是一種面向鏈接的、可靠的、 基於字節流的傳輸層通訊協議,由IETF的RFC 793 [1] 定義。
TCP旨在適應支持多網絡應用的分層協議層次結構。 鏈接到不一樣但互連的計算機通訊網絡的主計算機中的成對進程之間依靠TCP提供可靠的通訊服務。TCP假設它能夠從較低級別的協議得到簡單的,可能不可靠的數據報服務。 原則上,TCP應該可以在從硬線鏈接到分組交換或電路交換網絡的各類通訊系統之上操做。
這裏咱們抽取幾個關鍵的點:網絡
這很簡單理解啦,就是在要傳輸以前會須要先創建鏈接。怎麼創建?三次握手啊。這個網上不少文章啦,你們能夠去看看。
爲何須要三次呢?我這裏大概解釋一下:框架
- 客戶端發起鏈接,這只是一次初始的
- 服務端收到鏈接創建的要求,代表本身接收是OK的,客戶端發送也是OK的,但本身的發送和客戶端的接收能力是咋樣的,這還不清楚。
- 服務端要確認一下本身的發送能力和客戶端的接收能力,所以再發送一次回覆進行確認。若是客戶端收到了,證實兩方的能力都正常,這時鏈接才正式創建。
爲何叫可靠呢?是由於它能夠幫咱們 重傳, 數據校驗。這些不在咱們這次的重點內。
這時咱們此次的 重頭戲,正是由於tcp
是基於字節流的,纔會致使出現一些奇怪的問題,好比上一次發送的東西跟下一次的合在一塊兒了,致使解析的時候出現一些奇怪的東西。
好比第一次客戶端發了一句: 你何時到那裏的?,那假設咱們服務端的解析方式不對,致使了發送的內容被切割了,那麼就有可能會變成: 你何時到,後面才收到 那裏的,致使變成了 你何時到?那裏的?。語義可能就徹底不同了,要是跟女友或老婆大人聊天的時候變成這樣,估計晚上就要回去跪鍵盤了。
基於上面咱們的分析,tcp
是基於字節流的,沒有所謂的包,那這裏的粘包是啥東西來的?咱們仍是直接經過google一下來到百度百科(想一想就以爲奇怪)socket
網絡技術術語。指TCP協議中,發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。
tcp
協議中?包數據?奇怪咧,tcp
不是字節流來的嗎?哪來的包。
咱們再在網上找找資料,發現粘包基本上都是中文資料纔有的,從哪裏來的咱們也找不到了,但在國外的資料裏面咱們都看不到相似的說法。難道這個說法是錯的?咱們先不肯定哈,咱們先來看一下java
裏面的Socket
的示例,再來講說所謂的粘包究竟對不對。tcp
public class ClientSocketTest { public static void main(String[] args) throws IOException { //創建和服務端的鏈接 Socket socket = new Socket("localhost", 8080); //發送消息給服務端 socket.getOutputStream().write("helloworld".getBytes()); socket.getOutputStream().write("helloworld".getBytes()); socket.close(); } }
咱們簡單說明一下這裏作的事情,咱們發送了兩條消息到服務端,按咱們的理解,這確定是要在服務端分兩次接收才能夠的。學習
public class ServerSocketTest { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); //這裏會阻塞一直到有鏈接創建 Socket socket = serverSocket.accept(); socket.getInputStream(); //這裏讀取由客戶端發過來的內容 byte[] bytes = new byte[1024]; int length = socket.getInputStream().read(bytes); System.out.println(new String(bytes, 0, length)); socket.close(); serverSocket.close(); } }
這裏服務端只是簡單的進行一次接收,而接收的長度就是1024長度,由於咱們但願特地營造一種粘包的狀況。ui
對於屢次tcp
的發送狀況來講,咱們以兩次來舉例:google
這裏字節流一切正常,並無發生合併的狀況
這裏咱們看到兩個字節流徹底合併了——也就是咱們上面的例子中寫到的狀況。
這裏咱們看到第一個字節流的
部分被合併第二個字節流了,就是相似被
粘過去了。但要很是注意,這並非包,而
tcp
也並無明確的包的概念。
這裏咱們看到第一個字節流把第二個字節流的 部分字節給合併過來了,跟上面的狀況相似。
因爲上面的狀況要一一復現會比較麻煩,咱們這裏就不詳細寫示例,你們能夠相似上面去寫示例。這些狀況涉及到比較多的狀況,包括網絡順暢狀況等。
咱們就來講一下爲何會出現上面的狀況,若是咱們的tcp
是一個個的包,那麼一個個的包,確定會有本身的界限,也就不可能會出現所謂的粘包狀況。惟一能夠解釋的就是tcp
根本就不是一個個的包,這也是咱們正常學習tcp
的時候學到的知識,tcp
是字節流,沒有明顯的界限,因此當緩存區滿了以後,網卡就會把內容傳輸到服務端,而服務端也並無明確知道這些流應該怎麼分割,因此當多個字節流因爲某種緣由粘在了一塊兒,那麼就會出現了內容錯誤的狀況了。
那咱們都已經知道有這樣的問題,那應該怎麼解決呢?咱們來聊聊正常狀況下咱們的處理方案。
咱們先看看致使字節流粘在一塊兒的緣由是什麼?是由於咱們不知道怎麼去切割消息。
那咱們是否是讓服務端知道怎麼分割消息就行了,那要讓服務端知道怎麼分割消息,咱們有幾種思路:
這裏咱們能夠經過在消息體最前面的byte
中增長當前消息體的長度,在解析的過程當中,咱們先解析最前面的一個byte
,而後按照該長度去解析後面的內容,這樣就能夠達到分割消息的做用了。
咱們這裏給個小示例:
public class LengthServerSocketTest { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); //這裏會阻塞一直到有鏈接創建 Socket socket = serverSocket.accept(); process(socket); process(socket); socket.close(); serverSocket.close(); } /** * 處理客戶端輸入 * @param socket * @throws IOException */ private static void process(Socket socket) throws IOException { byte[] bytes = new byte[1]; socket.getInputStream().read(bytes); bytes = new byte[(int)bytes[0]]; socket.getInputStream().read(bytes); System.out.println(new String(bytes)); } }
這裏比較簡單,咱們就是先讀第一位byte
,就能夠拿到這次傳輸的消息字節流的長度,而後咱們再讀指定長度的字節流,那麼咱們就把當次的消息讀完了。
而客戶端的話咱們就只是在前面加上單次消息的長度:
public class LengthClientSocketTest { public static void main(String[] args) throws IOException { //創建和服務端的鏈接 Socket socket = new Socket("localhost", 8080); //發送消息給服務端 process(socket); process(socket); socket.close(); } /** * 發送消息體 * @param socket * @throws IOException */ private static void process(Socket socket) throws IOException { byte[] bytes = new byte[1 + "helloworld".getBytes().length]; bytes[0] = (byte)("helloworld".length()); for (int i = 1; i < bytes.length; i ++) { bytes[i] = "helloworld".getBytes()[i - 1]; } socket.getOutputStream().write(bytes); } }
因爲是示例,這裏寫法比較飄逸,你們就不要太講究了哈。
這裏咱們看個例子:
服務端代碼以下:
public class LineBreakServerSocketTest { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); //這裏會阻塞一直到有鏈接創建 Socket socket = serverSocket.accept(); process(socket); process(socket); socket.close(); serverSocket.close(); } /** * 處理客戶端的輸入 * @param socket * @throws IOException */ private static void process(Socket socket) throws IOException { byte[] bytes = new byte[1024]; int idx = 0; socket.getInputStream().read(bytes, idx, 1); while ("\n".getBytes()[0] != (int)bytes[idx ++]) { socket.getInputStream().read(bytes, idx, 1); } //去掉末尾的\n byte[] newBytes = new byte[idx - 1]; for (int i = 0; i < newBytes.length; i ++) { newBytes[i] = bytes[i]; } System.out.println(new String(newBytes)); } }
這裏咱們能夠看到,咱們是循環的讀每一位,當遇到咱們約定的\n
符時,咱們認爲是一次消息的結束,此時咱們就輸出,再繼續處理下一個輸入字節流。
而客戶端代碼比較簡單,就是在輸入後面加上\n
做爲結尾。
public class LinkBreakClientSocketTest { public static void main(String[] args) throws IOException { //創建和服務端的鏈接 Socket socket = new Socket("localhost", 8080); //發送消息給服務端 socket.getOutputStream().write("helloworld\n".getBytes()); socket.getOutputStream().write("helloworld\n".getBytes()); socket.close(); } }
既然服務端不知道每一個消息應該怎麼分割,那麼咱們全部消息同樣長,那不就能夠了,反正服務端每次都讀這麼多消息,超過的我也無論了。
基於這種思想,咱們就能夠定義一個固定的長度,每次發送消息都是按這樣的長度,也就不會致使消息粘在一塊兒了。
咱們來看一下例子。
服務端代碼以下:
public class FixLengthServerSocketTest { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); //這裏會阻塞一直到有鏈接創建 Socket socket = serverSocket.accept(); process(socket); process(socket); socket.close(); serverSocket.close(); } /** * 處理客戶端輸入 * @param socket * @throws IOException */ private static void process(Socket socket) throws IOException { byte[] bytes = new byte[1024]; socket.getInputStream().read(bytes); int newByteLength = 0; for (byte b:bytes) { if (b != 0) { newByteLength++; } } byte[] newBytes = new byte[newByteLength]; IntStream.range(0, newBytes.length).forEach(idx -> newBytes[idx] = bytes[idx]); System.out.println(new String(newBytes)); } }
這裏咱們能夠看到,好比簡單,就是按照固定的長度讀取輸入,而後拿到真正的內容(爲0的咱們認爲他是空閒的,固然真正實現時可能不該該這樣)。
而客戶端咱們也須要配套:
public class FixLengthClientSocketTest { public static void main(String[] args) throws IOException { //創建和服務端的鏈接 Socket socket = new Socket("localhost", 8080); //發送消息給服務端 process(socket); process(socket); socket.close(); } /** * 發送消息體 * @param socket * @throws IOException */ private static void process(Socket socket) throws IOException { byte[] bytes = new byte[1024]; byte[] exactBytes = "helloworld".getBytes(); for (int i = 0; i < exactBytes.length; i ++) { bytes[i] = exactBytes[i]; } socket.getOutputStream().write(bytes); } }
咱們把客戶端每次的消息都限制爲1024個byte
,超出的咱們也沒辦法處理了。這樣在客戶端和服務端的配合下,咱們就能夠保證消息被正常處理。
爲了解決這個字節流粘在一塊兒的問題,每次都要寫那麼一堆代碼,這好像也不是咱們想要的。因此業界的一些比較流行的框架,如netty
,它會爲咱們作好這些事情,它提供了一些通用的處理邏輯。如:
FixedLengthFrameDecoder
定長解析器,相似咱們上面的
FixLength
處理邏輯,固然,工業化的處理方式確定沒有咱們上面那麼簡單
LineBasedFrameDecoder
換行解析器,相似咱們上面的
LineBreak
處理邏輯。
DelimiterBasedFrameDecoder
分割符解析器,它的底層實際上也是經過
LineBaseFrameDecoder
,只是它能夠定義多個,而且會選擇一個最爲合適的分割符。
LengthFieldBasedFrameDecoder
域長度解析器,能夠理解爲相似咱們上面的
Length
的處理邏輯,固然這裏的處理邏輯沒那麼簡單,有興趣的能夠去了解一下。
固然,除了上面的一些,還有一些使用本身的處理方案的,如protobuffer
,thrift
等,他們使用本身的方案,但底層大同小異。你們能夠本身瞭解一下。
今天,咱們聊了一下tcp
的解析相關的,固然,主要集中在流的粘上面,其餘的咱們並無太多涉及,咱們後面有機會再細談。