一、封包
在iOS很多應(yīng)用開(kāi)發(fā)中,大部分用的網(wǎng)絡(luò)通信都是http/https協(xié)議,除非有特殊的需求會(huì)用到Socket網(wǎng)絡(luò)協(xié)議進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)傳輸,這時(shí)候在iOS客戶(hù)端就需要很好的第三方CocoaAsyncSocket 來(lái)進(jìn)行長(zhǎng)連接連接和傳輸數(shù)據(jù),讀者可以自行查閱資料搜索這個(gè)庫(kù)的用法。
一般在使用Socket的時(shí)候,后臺(tái)會(huì)對(duì)Socket傳輸數(shù)據(jù)有一個(gè)自定義的協(xié)議,協(xié)議可能有些差別不過(guò)基本上是大同小異。 如圖

也就是說(shuō)我們通過(guò)Socket發(fā)送給服務(wù)器的數(shù)據(jù),最終要轉(zhuǎn)換成二進(jìn)制流數(shù)據(jù),并且按照協(xié)議約定的格式。
下面我簡(jiǎn)單解釋下這個(gè)協(xié)議,因?yàn)橐婚_(kāi)始我自己也不是很理解。這個(gè)協(xié)議是指我們?cè)诎l(fā)送的數(shù)據(jù)包頭部開(kāi)辟一個(gè)4個(gè)字節(jié)長(zhǎng)度的空間,用來(lái)存儲(chǔ)服務(wù)號(hào)轉(zhuǎn)換成的二進(jìn)制數(shù)據(jù)。(將1轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)存儲(chǔ)進(jìn)去占4個(gè)字節(jié)長(zhǎng)度),然后再將數(shù)據(jù)包長(zhǎng)度轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)并存儲(chǔ)到后面開(kāi)辟的4個(gè)字節(jié)中(這里需要注意下如果數(shù)據(jù)要進(jìn)行加密傳輸,這里的長(zhǎng)度應(yīng)是加密后的長(zhǎng)度),最后將數(shù)據(jù)數(shù)據(jù)包轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)添加到后面,組成一個(gè)完整的數(shù)據(jù)包也就是封包。這里一定要按協(xié)議規(guī)定的順序不然服務(wù)器解析不了。
具體使用見(jiàn)代碼
NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
dictTemp[@"username"] = @"LD";
//先創(chuàng)建模型 --> 轉(zhuǎn)Json -->轉(zhuǎn)字符串
TestModel *model = [TestModel new];
model.type = 1;
model.userName = @"LD";
model.age = @"18";
model.message = @"Hellow";
model.Content = dictTemp;
//先將模型轉(zhuǎn)換成Json格式的數(shù)據(jù)這里根據(jù)自己項(xiàng)目情況來(lái)看是否需要轉(zhuǎn)成Json格式 使用到了MJExtension,
NSString * strJson = [[NSString alloc] initWithData :model.mj_JSONData encoding :NSUTF8StringEncoding];
Cs_Connect *connect = [Cs_Connect new];
connect.serverID = 1;
connect.message = strJson;
connect.length = (int)connect.message.length;
//將數(shù)據(jù)傳換成二進(jìn)制數(shù)據(jù),轉(zhuǎn)換之后的數(shù)據(jù)和協(xié)議順序是一致的(為什么不需要調(diào)整順序我也不知道,有興趣的的同學(xué)自己去研究下這個(gè)方法)
NSMutableData *dataModel = [socket RequestSpliceAttribute:connect];
// 通過(guò)Socket發(fā)出去
[socket sendMessage:dataModel];
轉(zhuǎn)為二進(jìn)制數(shù)據(jù)
// 將模型數(shù)據(jù)轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)
-(NSMutableData *)RequestSpliceAttribute:(id)obj{
_data = nil;//記得清空不然數(shù)據(jù)包會(huì)越來(lái)越大
if (obj == nil) {
self.object = self.data;
NSLog(@"傳入需轉(zhuǎn)二進(jìn)制的數(shù)據(jù)為空");
return nil;
}
unsigned int numIvars; //成員變量個(gè)數(shù)
objc_property_t *propertys = class_copyPropertyList(NSClassFromString([NSString stringWithUTF8String:object_getClassName(obj)]), &numIvars);
NSString *type = nil;
NSString *name = nil;
for (int i = 0; i < numIvars; i++) {
objc_property_t thisProperty = propertys[i];
name = [NSString stringWithUTF8String:property_getName(thisProperty)];
// NSLog(@"%d.name:%@",i,name);
type = [[[NSString stringWithUTF8String:property_getAttributes(thisProperty)] componentsSeparatedByString:@","] objectAtIndex:0]; //獲取成員變量的數(shù)據(jù)類(lèi)型
// NSLog(@"%d.type:%@",i,type);
id propertyValue = [obj valueForKey:[(NSString *)name substringFromIndex:0]];
// NSLog(@"%d.propertyValue:%@",i,propertyValue);
if ([type isEqualToString:TYPE_UINT8]) {
uint8_t i = [propertyValue charValue];// 8位
[self.data appendData:[DLSocketDataUtils byteFromUInt8:i]];
}else if([type isEqualToString:TYPE_UINT16]){
uint16_t i = [propertyValue shortValue];// 16位
[self.data appendData:[DLSocketDataUtils bytesFromUInt16:i]];
}else if([type isEqualToString:TYPE_UINT32]){
uint32_t i = [propertyValue intValue];// 32位
[self.data appendData:[DLSocketDataUtils bytesFromUInt32:i]];
}else if([type isEqualToString:TYPE_UINT64]){
uint64_t i = [propertyValue longLongValue];// 64位
[self.data appendData:[DLSocketDataUtils bytesFromUInt64:i]];
}else if([type isEqualToString:TYPE_STRING]){
NSData *data = [(NSString*)propertyValue \
dataUsingEncoding:NSUTF8StringEncoding];// 通過(guò)utf-8轉(zhuǎn)為data
[self.data appendData:data];
}else {
NSLog(@"RequestSpliceAttribute:未知類(lèi)型");
NSAssert(YES, @"RequestSpliceAttribute:未知類(lèi)型");
}
}
// hy: 記得釋放C語(yǔ)言的結(jié)構(gòu)體指針
free(propertys);
self.object = _data;
return _data;
}
轉(zhuǎn)為二進(jìn)制代碼鏈接:http://pan.baidu.com/s/1hsi7tNQ密碼: byiy
關(guān)于轉(zhuǎn)碼更詳細(xì)的說(shuō)明請(qǐng)看下面的鏈接
參考資料:iOS開(kāi)發(fā)之Socket通信實(shí)戰(zhàn)--Request請(qǐng)求數(shù)據(jù)包編碼模塊
二、粘包、拆包處理
我們一般使用的是基于TCP的流式Socket,因此本文也主要講解這一種方式,TCP是一種流協(xié)議(stream protocol)。這就意味著數(shù)據(jù)是以字節(jié)流的形式傳遞給接收者的,沒(méi)有固有的"報(bào)文"或"報(bào)文邊界"的概念。從這方面來(lái)說(shuō),讀取TCP數(shù)據(jù)就像從串行端口讀取數(shù)據(jù)一樣--無(wú)法預(yù)先得知在一次指定的讀調(diào)用中會(huì)返回多少字節(jié)(也就是說(shuō)能知道總共要讀多少,但是不知道具體某一次讀多少)
讓我們來(lái)看一個(gè)例子:我們假設(shè)在主機(jī)A和主機(jī)B的應(yīng)用程序之間有一條TCP連接,主機(jī)A有兩條報(bào)文D1,D2要發(fā)送到B主機(jī),并兩次調(diào)用send來(lái)發(fā)送,每條報(bào)文調(diào)用一次。

那么,我們自然而然的希望兩條報(bào)文是作為兩個(gè)獨(dú)立的實(shí)體,在各自的分組中發(fā)送,如圖1:

這樣的話(huà),我們無(wú)需做任何特別的處理,便能夠很容易的區(qū)分每一個(gè)獨(dú)立的數(shù)據(jù),并根據(jù)需求分別做相應(yīng)的處理。但現(xiàn)實(shí)往往是有所偏差的,實(shí)際的數(shù)據(jù)傳輸過(guò)程很可能不會(huì)遵循這個(gè)模型。而是會(huì)采用以下四種方式之一進(jìn)行傳輸。如圖2:

- D1和D2數(shù)據(jù)作為兩個(gè)獨(dú)立的分組,分別到達(dá)主機(jī)B;
- D1和D2合為一個(gè)整體組,一起到達(dá)主機(jī)B;
- D1的部分?jǐn)?shù)據(jù)先到達(dá)主機(jī)B,剩下的D1數(shù)據(jù)和D2和在一組到達(dá)主機(jī)B;
- D1和D2的部分?jǐn)?shù)據(jù)先到達(dá)主機(jī)B, D2后到達(dá)主機(jī)B;
實(shí)際上,可能的情況還不止4種,這里我們就不做深入了解,以上就是造成粘包的原因。
解決思路:拆包
在上面說(shuō)到我們給每個(gè)數(shù)據(jù)包添加頭部,頭部中包含數(shù)據(jù)包的長(zhǎng)度,這樣接收到數(shù)據(jù)后,通過(guò)讀取頭部的長(zhǎng)度字段,便知道每一個(gè)數(shù)據(jù)包的實(shí)際長(zhǎng)度了,再根據(jù)長(zhǎng)度去讀取指定長(zhǎng)度的數(shù)據(jù)便能獲取到正確的數(shù)據(jù)了。
再來(lái)回顧一下 協(xié)議:

完整的數(shù)據(jù)包 = 服務(wù)號(hào) + 數(shù)據(jù)包長(zhǎng)度 + 數(shù)據(jù)
數(shù)據(jù)包頭 = Id(4B) + length(4B) 共占用8字節(jié)
數(shù)據(jù)包 = length(假設(shè)占100個(gè)字節(jié))
所以這條消息的長(zhǎng)度就是108字節(jié)可以看到,要想知道一條完整數(shù)據(jù)的邊界,關(guān)鍵就是數(shù)據(jù)包頭中的length字段
實(shí)現(xiàn)代碼
-(void) didReadData:(NSData *)data {
//將接收到的數(shù)據(jù)保存到緩存數(shù)據(jù)中
[self.cacheData appendData:data];;
// 取出4-8位保存的數(shù)據(jù)長(zhǎng)度,計(jì)算數(shù)據(jù)包長(zhǎng)度
NSData *dataLength = [_cacheData subdataWithRange:NSMakeRange(4, 4)];
int dataLenInt = CFSwapInt32BigToHost(*(int*)([dataLength bytes]));
NSInteger lengthInteger = 0;
lengthInteger = (NSInteger)dataLenInt;
NSInteger complateDataLength = lengthInteger + 8;//算出一個(gè)包完整的長(zhǎng)度(內(nèi)容長(zhǎng)度+頭長(zhǎng)度)
NSLog(@"data = %ld ---- length = %d ",data.length,dataLenInt);
//因?yàn)榉?wù)號(hào)和長(zhǎng)度字節(jié)占8位,所以大于8才是一個(gè)正確的數(shù)據(jù)包
while (_cacheData.length > 8) {
if (_cacheData.length < complateDataLength) { //如果緩存中的數(shù)據(jù)長(zhǎng)度小于包頭長(zhǎng)度 則繼續(xù)拼接
[[SingletonSocket sharedInstance].socket readDataWithTimeout:-1 tag:0];//socket讀取數(shù)據(jù)
break;
}else {
//截取完整數(shù)據(jù)包
NSData *dataOne = [_cacheData subdataWithRange:NSMakeRange(0, complateDataLength)];
[self handleTcpResponseData:dataOne];//處理包數(shù)據(jù)
[_cacheData replaceBytesInRange:NSMakeRange(0, complateDataLength) withBytes:nil length:0];
if (_cacheData.length > 8) {
[self didReadData:nil];
}
}
}
}
由于公司項(xiàng)目是游戲開(kāi)發(fā),所以對(duì)于數(shù)據(jù)傳輸高效、穩(wěn)定性有一定的要求需要數(shù)據(jù)的實(shí)時(shí)更新,所以這次用到了Socket通信。因?yàn)橹巴耆珱](méi)有這方面的經(jīng)驗(yàn),前期遇到很多坑。所以在這里把自己遇到的一些問(wèn)題和解決方式總結(jié)出來(lái),希望能給后面用到的人一些幫助。