CocoaAsyncSocket + Protobuf 處理粘包和拆包問題
在上一篇文章《iOS之ProtocolBuffer搭建和示例demo》分享環境的搭建, 我們和伺服器進行IM通訊用了github有名的框架CocoaAsynSocket, 然後和伺服器之間的資料媒介是ProtoBuf。然後後面在開發的過程中也碰到了拆包和粘包問題,這方面網上資料很少,曲折了一下才解決,這裡分享一下問題的解決過程!
首先描述下碰到的問題:
1、伺服器傳送內容很長的資料過來的時候,GCDAsyncSocket監聽收到的一個包解析不了,一直要接收好幾個包拼接才是這條資料的完整包,即所謂的拆包/斷包;
2、伺服器快速傳送多條資料過來,傳到客戶端這邊的時候幾條資料合成了一個包,即所謂的粘包。所以想解析這些粘在一起的資料,必須知道每條資料的長度,才能正確切割解析
先上關鍵程式碼,解決讀取每條資料的頭部位元組,根據頭部位元組讀取這條資料的內容長度。這樣才能完美的解決粘包問題。由於根據資料的長度不一樣,導致頭部位元組佔用的長度也會不一樣,比如說我這裡反覆測試的結果頭部佔用位元組一般為1和2,短內容資料頭部佔用位元組長度為1,長內容資料頭部佔用位元組長度為2。這裡的程式碼參考了谷歌提供的Protobuf的objectivec版的原始碼。
/** 關鍵程式碼:獲取data資料的內容長度和頭部長度: index --> 頭部佔用長度 (頭部佔用長度1-4個位元組) */ - (int32_t)getContentLength:(NSData *)data withHeadLength:(int32_t *)index{ int8_t tmp= [self readRawByte:data headIndex:index]; if (tmp >= 0) return tmp; int32_t result = tmp & 0x7f; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 7; } else { result |= (tmp & 0x7f) << 7; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 14; } else { result |= (tmp & 0x7f) << 14; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 21; } else { result |= (tmp & 0x7f) << 21; result |= (tmp = [self readRawByte:data headIndex:index]) << 28; if (tmp < 0) { for (int i = 0; i < 5; i++) { if ([self readRawByte:data headIndex:index] >= 0) { return result; } } result = -1; } } } } return result; } /** 讀取位元組 */ - (int8_t)readRawByte:(NSData *)data headIndex:(int32_t *)index{ if (*index >= data.length) return -1; *index = *index + 1; return ((int8_t *)data.bytes)[*index - 1]; }
解決了讀取每條資料的頭部佔用位元組,和內容的長度,粘包問題就好解決了。
再上比較完整的程式碼:從客戶端監聽伺服器傳送過來的資料到處理拆包和粘包問題,然後解析成自定義的protobuf模型類。
View Code--------------------- print log test start ---------------------
在GCDAsyncSocket監聽伺服器代理方法裡面列印日誌,檢視下正常包、粘包、拆包的日誌資訊:
1、正常包的列印日誌(伺服器傳送一條訊息達到客戶端只有一個包: 1 + 49 = 50 ):
實際接收總包長度:50, 當前接收包長度:50, 讀取頭部佔用長度: 1, 讀取內容長度:49, 當前包位元組: <3108c901 122c0a0a 74363330 32303137 30361203 43636318 01220c64 656e6773 6f6e676e 616e782a 00300038 9af6ccd5 b72b>
2、粘包情況下的日誌 (傳送的4條訊息合到了一個包裡:(1 + 49) * 4 = 200 ):
實際接收總包長度:200, 當前接收包長度:200, 讀取頭部佔用長度: 1, 讀取內容長度:49, 當前包位元組: <3108c901 122c0a0a 74363330 32303137 30361203 46666618 01220c64 656e6773 6f6e676e 616e782a 00300038 f2cfccd5 b72b3108 c901122c 0a0a7436 33303230 31373036 12034767 67180122 0c64656e 67736f6e 676e616e 782a0030 003898d0 ccd5b72b 3108c901 122c0a0a 74363330 32303137 30361203 5a7a7a18 01220c64 656e6773 6f6e676e 616e782a 00300038 add0ccd5 b72b3108 c901122c 0a0a7436 33303230 31373036 12035878 78180122 0c64656e 67736f6e 676e616e 782a0030 0038c1d0 ccd5b72b>
3、拆包情況下的日誌 (傳送一條內容較長的訊息,最後到達客戶端時被拆成了三個接收包: 4200 + 1400 + 292 = 2+5890 ):