TCP粘包、斷包處理
在TCP傳輸中,當我們使用長連線傳輸資料時,由於傳輸頻率快、緩衝區不足等問題,經常會產生 斷包、粘包 的問題,本文將基於java講述TCP協議中這兩個問題的解決。
首先,簡單介紹一下粘包、斷包問題 產生的原因 :
粘包的產生:粘包可能在服務端產生也可能在客戶斷產生。提交資料給tcp傳送時,TCP並不立刻傳送此段資料,而是等待一小段時間,看看在等待期間是否還有要傳送的資料,若有則會一次把這兩段資料傳送出去,造成粘包;另一端在接收到資料庫後,放到緩衝區中,如果訊息沒有被及時從快取區取走,下次在取資料的時候可能就會出現一次取出多個數據包的情況,造成粘包現象。
斷包的產生:使用TCP傳送資料時,有可能資料過大,使得傳送方緩衝區無法一次傳送,造成另一端只收到的資料不完整,所以要等待資料完全接收到再解析資料。

粘包、斷包示例
以上均引用自: ofollow,noindex">http://blog.csdn.net/chen199199/article/details/50564015
要處理粘包和斷包,關鍵點是在傳輸的一幀資料中加入包頭和包尾,如果有需要可以加入幀長來表徵資料長度。
廢話不多說,直接上程式碼,首先,建立TCP連線,為長連線做準備。
//1.與裝置建立連線 Socket realDataSck = new Socket(); SocketAddress socketAddress = new InetSocketAddress(deviceIp, devicePort); //確保在網路不通暢或裝置故障的情況下也能持續連線 while (true) { try { realDataSck.connect(socketAddress, 2000); break; } catch (Exception e) { System.out.println("重連"); realDataSck.close(); realDataSck = new Socket(); } } realDataSck.setSoTimeout(2000); //2.準備請求 OutputStream os = realDataSck.getOutputStream(); InputStream is = realDataSck.getInputStream();
接下來是處理粘包、斷包的關鍵程式碼了:
//開啟一個佇列用於存放TCP資料 List<Byte> queueFinal = new LinkedList<Byte>(); //定義包頭包尾 byte[] head = {-54, -53, -52, -51}; byte[] tail = {-22, -21, -20, -19, 97, 103, 92, 20, -119, -58}; int headIndex = -1; int tailIndex = -1; os.write("ff".getBytes()); //3.建立長連線 while (true) { //判斷遠端伺服器是否斷開連線了 if (!isServerClose(realDataSck)) { //3.接收雷達返回的資料 //判斷輸入流是否有資料,如果沒有則等待10ms if (is.available() > 0) { int len = is.available(); byte buf[] = new byte[len]; is.read(buf, 0, len); //將資料全部存入臨時緩衝區 for (byte b : buf) { queueFinal.add(b); } //4.處理斷包、粘包 while (true) { headIndex = RadarUtil.indexOfArray(queueFinal, head); tailIndex = RadarUtil.indexOfArray(queueFinal, tail); if (headIndex >= 0 && tailIndex >= 0 && headIndex < tailIndex) { byte[] bytesFinal = new byte[tailIndex + 10 - headIndex]; //找到了包頭包尾,則提取出一幀放入位元組緩衝區,如有多幀資料直接覆蓋,同時扔掉佇列緩衝區中包頭前的多餘位元組 for (int i = 0; i < headIndex; i++) { queueFinal.remove(0); } for (int i = 0; i < bytesFinal.length; i++) { bytesFinal[i] = queueFinal.get(0); queueFinal.remove(0); } //解析雷達資料並存入資料庫 operateRealTimeData(interSectionId, dir, deviceNo, lanes, deviceIp, devicePort, conn, stmt, bytesFinal); //粘包,即包尾後還有內容,如果沒有粘包則繼續傳送tcp請求收取下一幀資料 if (queueFinal.size() > 0) { System.out.println("粘包了"); } else { Thread.sleep(60); os.write("ff".getBytes()); break; } } else if (headIndex >= 0 && tailIndex == -1 || headIndex == -1 && tailIndex == -1) { //斷包,即接收到的包不完整,則跳出內圈迴圈,進入外圈迴圈,從輸入流中繼續讀取資料 System.out.println("斷包了"); Thread.sleep(10); break; } else if ((headIndex == -1 && tailIndex >= 0) || headIndex > tailIndex) { //殘包,即只找到包尾或包頭在包尾後面,則扔掉佇列緩衝區中包尾及其之前的多餘位元組 System.out.println("殘包了"); for (int i = 0; i < tailIndex + 10; i++) { queueFinal.remove(0); } //如果扔掉後佇列緩衝區中為空,則可以直接進行下一輪tcp請求,否則跳出內圈迴圈,進入外圈迴圈,從輸入流中繼續讀取資料 if (queueFinal.size() == 0) { Thread.sleep(60); os.write("ff".getBytes()); break; } } } } else { Thread.sleep(10); } } else { //如果斷開了,持續重連 Thread.sleep(1000); os.close(); realDataSck.close(); realDataSck = new Socket(); socketAddress = new InetSocketAddress(deviceIp, devicePort); //確保在網路不通暢或雷達裝置故障的情況下也能持續連線 while (true) { try { realDataSck.connect(socketAddress, 2000); break; } catch (Exception e) { System.out.println("重連"); realDataSck.close(); realDataSck = new Socket(); } } realDataSck.setSoTimeout(2000); os = realDataSck.getOutputStream(); is = realDataSck.getInputStream(); queueFinal = new LinkedList<Byte>(); } }
在上述程式碼中,首先,定義一個佇列緩衝區用於存放資料,並記錄你的包頭包尾。隨後,通過外圈的while語句建立長連線,當判斷出遠端伺服器仍然連線著,便去讀取輸入流中接收的資料,否則持續重連,由於無關題目不再贅述。
處理粘包、斷包的思路大致如下,先是將輸入流接到的資料放入佇列緩衝區,隨後進入內圈while迴圈,從佇列中找包頭包尾,根據找包頭包尾的情況來判斷資料幀是否發生粘包、斷包。
此時,可能會有以下多種情況:
1、找到了包頭包尾,且包頭在包尾之前形成了完整的一幀,此時即可將此幀從佇列中拿出來扔到你的後續環節中處理。如果此時包頭前有資料無法形成完整一幀,則可以直接扔掉;包尾後有資料說明發生粘包,應繼續內圈迴圈,判斷髮生粘包的資料是否能形成一幀。
2、找到了包頭卻沒有包尾,又或者包頭包尾都找不到,此時可以統一處理,直接跳出內圈迴圈,到外圈迴圈裡,從輸入流中繼續讀取資料直至形成完整一幀。
3、找到了包尾卻沒有包頭,此時這一幀為殘包,可以扔掉佇列緩衝區中包尾及其之前的多餘位元組。
以上,基本可以處理粘包、斷包的問題了,程式碼中都有詳細註釋。
github傳送地址: https://github.com/JunJieDing666/CityBrainMiddleware
具體程式碼路徑:src/com/djj/middleware/utils/RadarUtil.java
若有錯誤煩請指出,有地方不理解歡迎討論。