在TCP傳輸中泵殴,當(dāng)我們使用長連接傳輸數(shù)據(jù)時涮帘,由于傳輸頻率快、緩沖區(qū)不足等問題笑诅,經(jīng)常會產(chǎn)生斷包调缨、粘包的問題,本文將基于java講述TCP協(xié)議中這兩個問題的解決吆你。
首先弦叶,簡單介紹一下粘包、斷包問題產(chǎn)生的原因:
粘包的產(chǎn)生:粘包可能在服務(wù)端產(chǎn)生也可能在客戶斷產(chǎn)生妇多。提交數(shù)據(jù)給tcp發(fā)送時伤哺,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間,看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去,造成粘包者祖;另一端在接收到數(shù)據(jù)庫后默责,放到緩沖區(qū)中,如果消息沒有被及時從緩存區(qū)取走咸包,下次在取數(shù)據(jù)的時候可能就會出現(xiàn)一次取出多個數(shù)據(jù)包的情況,造成粘包現(xiàn)象杖虾。
斷包的產(chǎn)生:使用TCP傳送數(shù)據(jù)時烂瘫,有可能數(shù)據(jù)過大,使得發(fā)送方緩沖區(qū)無法一次發(fā)送奇适,造成另一端只收到的數(shù)據(jù)不完整坟比,所以要等待數(shù)據(jù)完全接收到再解析數(shù)據(jù)。
以上均引用自:http://blog.csdn.net/chen199199/article/details/50564015
要處理粘包和斷包葛账,關(guān)鍵點是在傳輸?shù)囊粠瑪?shù)據(jù)中加入包頭和包尾,如果有需要可以加入幀長來表征數(shù)據(jù)長度皮仁。
廢話不多說籍琳,直接上代碼,首先贷祈,建立TCP連接趋急,為長連接做準(zhǔn)備。
//1.與設(shè)備建立連接
Socket realDataSck = new Socket();
SocketAddress socketAddress = new InetSocketAddress(deviceIp, devicePort);
//確保在網(wǎng)絡(luò)不通暢或設(shè)備故障的情況下也能持續(xù)連接
while (true) {
try {
realDataSck.connect(socketAddress, 2000);
break;
} catch (Exception e) {
System.out.println("重連");
realDataSck.close();
realDataSck = new Socket();
}
}
realDataSck.setSoTimeout(2000);
//2.準(zhǔn)備請求
OutputStream os = realDataSck.getOutputStream();
InputStream is = realDataSck.getInputStream();
接下來是處理粘包势誊、斷包的關(guān)鍵代碼了:
//開啟一個隊列用于存放TCP數(shù)據(jù)
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) {
//判斷遠端服務(wù)器是否斷開連接了
if (!isServerClose(realDataSck)) {
//3.接收雷達返回的數(shù)據(jù)
//判斷輸入流是否有數(shù)據(jù)呜达,如果沒有則等待10ms
if (is.available() > 0) {
int len = is.available();
byte buf[] = new byte[len];
is.read(buf, 0, len);
//將數(shù)據(jù)全部存入臨時緩沖區(qū)
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];
//找到了包頭包尾粟耻,則提取出一幀放入字節(jié)緩沖區(qū),如有多幀數(shù)據(jù)直接覆蓋查近,同時扔掉隊列緩沖區(qū)中包頭前的多余字節(jié)
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);
}
//解析雷達數(shù)據(jù)并存入數(shù)據(jù)庫
operateRealTimeData(interSectionId, dir, deviceNo, lanes, deviceIp, devicePort, conn, stmt, bytesFinal);
//粘包眉踱,即包尾后還有內(nèi)容,如果沒有粘包則繼續(xù)發(fā)送tcp請求收取下一幀數(shù)據(jù)
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) {
//斷包霜威,即接收到的包不完整谈喳,則跳出內(nèi)圈循環(huán),進入外圈循環(huán)侥祭,從輸入流中繼續(xù)讀取數(shù)據(jù)
System.out.println("斷包了");
Thread.sleep(10);
break;
} else if ((headIndex == -1 && tailIndex >= 0) || headIndex > tailIndex) {
//殘包叁执,即只找到包尾或包頭在包尾后面,則扔掉隊列緩沖區(qū)中包尾及其之前的多余字節(jié)
System.out.println("殘包了");
for (int i = 0; i < tailIndex + 10; i++) {
queueFinal.remove(0);
}
//如果扔掉后隊列緩沖區(qū)中為空矮冬,則可以直接進行下一輪tcp請求谈宛,否則跳出內(nèi)圈循環(huán),進入外圈循環(huán)胎署,從輸入流中繼續(xù)讀取數(shù)據(jù)
if (queueFinal.size() == 0) {
Thread.sleep(60);
os.write("ff".getBytes());
break;
}
}
}
} else {
Thread.sleep(10);
}
} else {
//如果斷開了吆录,持續(xù)重連
Thread.sleep(1000);
os.close();
realDataSck.close();
realDataSck = new Socket();
socketAddress = new InetSocketAddress(deviceIp, devicePort);
//確保在網(wǎng)絡(luò)不通暢或雷達設(shè)備故障的情況下也能持續(xù)連接
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>();
}
}
在上述代碼中,首先琼牧,定義一個隊列緩沖區(qū)用于存放數(shù)據(jù)恢筝,并記錄你的包頭包尾。隨后巨坊,通過外圈的while語句建立長連接撬槽,當(dāng)判斷出遠端服務(wù)器仍然連接著,便去讀取輸入流中接收的數(shù)據(jù)趾撵,否則持續(xù)重連侄柔,由于無關(guān)題目不再贅述。
處理粘包占调、斷包的思路大致如下暂题,先是將輸入流接到的數(shù)據(jù)放入隊列緩沖區(qū),隨后進入內(nèi)圈while循環(huán)究珊,從隊列中找包頭包尾薪者,根據(jù)找包頭包尾的情況來判斷數(shù)據(jù)幀是否發(fā)生粘包、斷包剿涮。
此時言津,可能會有以下多種情況:
1、找到了包頭包尾取试,且包頭在包尾之前形成了完整的一幀纺念,此時即可將此幀從隊列中拿出來扔到你的后續(xù)環(huán)節(jié)中處理。如果此時包頭前有數(shù)據(jù)無法形成完整一幀想括,則可以直接扔掉陷谱;包尾后有數(shù)據(jù)說明發(fā)生粘包,應(yīng)繼續(xù)內(nèi)圈循環(huán),判斷發(fā)生粘包的數(shù)據(jù)是否能形成一幀烟逊。
2渣窜、找到了包頭卻沒有包尾,又或者包頭包尾都找不到宪躯,此時可以統(tǒng)一處理乔宿,直接跳出內(nèi)圈循環(huán),到外圈循環(huán)里访雪,從輸入流中繼續(xù)讀取數(shù)據(jù)直至形成完整一幀详瑞。
3、找到了包尾卻沒有包頭臣缀,此時這一幀為殘包坝橡,可以扔掉隊列緩沖區(qū)中包尾及其之前的多余字節(jié)。
以上精置,基本可以處理粘包计寇、斷包的問題了,代碼中都有詳細注釋脂倦。
github傳送地址:https://github.com/JunJieDing666/CityBrainMiddleware
具體代碼路徑:src/com/djj/middleware/utils/RadarUtil.java
若有錯誤煩請指出番宁,有地方不理解歡迎討論。