本文不討論技術(shù)選型,不介紹業(yè)務(wù)邏輯。
簡(jiǎn)單地介紹下開發(fā)中會(huì)遇到的一些技術(shù)點(diǎn),解決的一些方案。
粘包
因?yàn)槭腔赥CP的,而TCP的是流式傳輸?shù)?,不像UDP數(shù)據(jù)報(bào)傳輸是有邊界的,所以會(huì)有粘包問題。
然而這個(gè)問題是必須解決的,大致有三種方法。
1:固定包的長(zhǎng)度,每次讀數(shù)據(jù)的時(shí)候,固定讀取字節(jié)。這在實(shí)際使用中基本不現(xiàn)實(shí)。
2:服務(wù)器每次發(fā)送消息的時(shí)候,給每個(gè)包添加上分隔符,如/r,/n。GCDAsyncSocket也有方法按照此邏輯直接切割。但這個(gè)方案也非常不靠譜。
3:一般的處理方法是定義一個(gè)消息頭,消息頭中包含了一個(gè)包的長(zhǎng)度,先拿到包長(zhǎng)度再去讀取完整的包。
Socket通信定義
頭信息:2字節(jié) 版本號(hào)
功能代碼:2字節(jié) 功能代碼
是否壓縮: 1個(gè)字節(jié) 0不壓縮, 1壓縮
消息長(zhǎng)度:2字節(jié)
消息實(shí)體:
功能代碼是指消息的類型,定義了許多許多, 因?yàn)槊總€(gè)項(xiàng)目定義都不一樣,就不介紹了。
按照我們的通信定義,我是這么處理粘包的:
//解決粘包
//思路是拆分包頭得到長(zhǎng)度,判斷接得到的長(zhǎng)度和包頭的長(zhǎng)度是否一致,不夠就繼續(xù)拼接,相等就返回,大于的話就自己做下拆分。先
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (tag == 0) {
self.readBufferData = [data mutableCopy];
unsigned short dataLength;
if(data.length >= 7){
dataLength = [self getDataLength:data];
}else{
return;
}
[self.socketManager.socket readDataToLength:dataLength withTimeout:-1 tag:1];
}else if(tag == 1){
NSMutableData * completeData = self.readBufferData;
self.readBufferData = nil;
[completeData appendData:data];
[self handleData:completeData];
[self.socketManager.socket readDataToLength:7 withTimeout:-1 tag:0];
}
}
簡(jiǎn)單解釋下,上面的代理方法是GCDAsyncSocket讀取數(shù)據(jù)方法,tag可以區(qū)分我的讀數(shù)據(jù)請(qǐng)求,如上tag==0 是我發(fā)起的讀消息頭的返回,再用消息頭中的字節(jié)長(zhǎng)度去讀完整的包,即tag==1的返回。
乍看一下似乎沒有問題了,我也是這么以為的。同事之前寫過量級(jí)比較大的通信,他遇到過問題,因?yàn)榫W(wǎng)卡緩沖區(qū)有個(gè)最大值,MTU,如果一下子來N多消息,字節(jié)會(huì)溢出,有可能會(huì)導(dǎo)致字節(jié)的少讀多讀。所以要嚴(yán)格按照代碼以上的注釋寫代碼,時(shí)間有限,我這邊先不上代碼。
生成數(shù)據(jù)報(bào)和解數(shù)據(jù)報(bào)
dataLength = [self getDataLength:data];
上面的代碼有這樣一個(gè)方法。按照字節(jié)去拿數(shù)據(jù)。之前不太清楚蘋果有API可以調(diào)用。自己寫了2個(gè)壓縮字節(jié)的,估計(jì)還有些問題。
/** 將數(shù)值轉(zhuǎn)成字節(jié)。編碼方式:低位在前,高位在后 */
- (NSData *)bytesFromValue:(NSInteger)value byteCount:(int)byteCount
{
NSAssert(value <= 4294967295, @"bytesFromValue: (max value is 4294967295)");
NSAssert(byteCount <= 4, @"bytesFromValue: (byte count is too long)");
NSMutableData *valData = [[NSMutableData alloc] init];
NSUInteger tempVal = value;
int offset = 0;
while (offset < byteCount) {
unsigned char valChar = 0xff & tempVal;
[valData appendBytes:&valChar length:1];
tempVal = tempVal >> 8;
offset++;
}//while
return [self dataWithReverse:valData];
}
正確的做法如下
/**
* 生成數(shù)據(jù)報(bào)model
*/
-(NSData *)socketModelToData{
NSString * bodyString = @"";
if([self.body isKindOfClass:[NSDictionary class]]){
bodyString = [self dictionnaryObjectToString:self.body];
}
NSData * dataBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
unsigned short bVersion = htons((short)self.version);
NSMutableData *version = [[NSMutableData alloc] initWithBytes:&bVersion length:2];
unsigned short vFunctionCode = htons((short)self.functionCode);
[version appendBytes:&vFunctionCode length:2];
unsigned short typeIsZip = htons((short)0);
[version appendBytes:&typeIsZip length:1];
unsigned short vDataBodyLength = htons((short)dataBody.length);
[version appendBytes:&vDataBodyLength length:2];
[version appendData:dataBody];
return version;
}
/**
* 解析數(shù)據(jù)報(bào)model
*/
- (DXSocketModel *)dataToSocketModel:(NSData *)data{
DXSocketModel * socketModel = [[DXSocketModel alloc] init];
unsigned short version;
unsigned short functionCode;
unsigned short isGzip;
unsigned short dataLength;
[data getBytes:&version range:NSMakeRange(0,2)];
[data getBytes:&functionCode range:NSMakeRange(2,2)];
[data getBytes:&isGzip range:NSMakeRange(4,1)];
[data getBytes:&dataLength range:NSMakeRange(5,2)];
socketModel.version = ntohs(version);
socketModel.functionCode = ntohs(functionCode);
socketModel.isGzip = ntohs(isGzip);
socketModel.dataLength = ntohs(dataLength);
// if(socketModel.dataLength + 7 == data.length){
NSData * jsonData = [data subdataWithRange:NSMakeRange(7,socketModel.dataLength)];
NSString * jsonStr = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];
socketModel.body = [self dictionaryWithJsonString:jsonStr];
// }else{
// NSLog(@"包頭提示長(zhǎng)度和實(shí)際長(zhǎng)度不一樣");
// }
;
return socketModel;
}
網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序
如果你認(rèn)真看了的話,你會(huì)發(fā)現(xiàn)生成數(shù)據(jù)和解析數(shù)據(jù)的時(shí)候,我用了這兩個(gè)ntohs htons函數(shù),主要是生成的字節(jié)高低位和傳輸中的字節(jié)高低位不同。需要轉(zhuǎn)換。
大端和小端(網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序)
大端(Big Endian):即網(wǎng)絡(luò)字節(jié)序。
小端(Littile Endian):即主機(jī)字節(jié)序。
這里不得不感嘆下API的豐富,方便了我們一批API調(diào)用者。
詳解大端模式和小端模式
其他
至于一些比較基本的問題:心跳包、斷線重連(斷線重連要區(qū)分是自己主動(dòng)斷開還是網(wǎng)絡(luò)異常)、GCDAsyncSocket的一些代理方法,百度太多見,就不重復(fù)介紹了。
事后看該方案存在一個(gè)問題:少包
如果接收區(qū)不夠大,或者已經(jīng)被占滿,讀不到剩下所有的包內(nèi)容,需要增加一個(gè)buffer,保留不完整的包,繼續(xù)去讀取剩余的包內(nèi)容。