本文不討論技術選型擎颖,不介紹業(yè)務邏輯慢哈。
簡單地介紹下開發(fā)中會遇到的一些技術點蔓钟,解決的一些方案。
粘包
因為是基于TCP的卵贱,而TCP的是流式傳輸的滥沫,不像UDP數據報傳輸是有邊界的侣集,所以會有粘包問題。
然而這個問題是必須解決的兰绣,大致有三種方法世分。
1:固定包的長度,每次讀數據的時候缀辩,固定讀取字節(jié)臭埋。這在實際使用中基本不現實。
2:服務器每次發(fā)送消息的時候臀玄,給每個包添加上分隔符,如/r,/n
瓢阴。GCDAsyncSocket
也有方法按照此邏輯直接切割。但這個方案也非常不靠譜健无。
3:一般的處理方法是定義一個消息頭荣恐,消息頭中包含了一個包的長度,先拿到包長度再去讀取完整的包睬涧。
Socket通信定義
頭信息:2字節(jié) 版本號
功能代碼:2字節(jié) 功能代碼
是否壓縮: 1個字節(jié) 0不壓縮募胃, 1壓縮
消息長度:2字節(jié)
消息實體:
功能代碼是指消息的類型,定義了許多許多畦浓, 因為每個項目定義都不一樣痹束,就不介紹了。
按照我們的通信定義讶请,我是這么處理粘包的:
//解決粘包
//思路是拆分包頭得到長度,判斷接得到的長度和包頭的長度是否一致祷嘶,不夠就繼續(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];
}
}
簡單解釋下,上面的代理方法是GCDAsyncSocket
讀取數據方法风响,tag可以區(qū)分我的讀數據請求嘉汰,如上tag==0
是我發(fā)起的讀消息頭的返回,再用消息頭中的字節(jié)長度去讀完整的包状勤,即tag==1
的返回鞋怀。
乍看一下似乎沒有問題了,我也是這么以為的持搜。同事之前寫過量級比較大的通信密似,他遇到過問題,因為網卡緩沖區(qū)有個最大值葫盼,MTU残腌,如果一下子來N多消息,字節(jié)會溢出,有可能會導致字節(jié)的少讀多讀抛猫。所以要嚴格按照代碼以上的注釋寫代碼蟆盹,時間有限,我這邊先不上代碼邑滨。
生成數據報和解數據報
dataLength = [self getDataLength:data];
上面的代碼有這樣一個方法日缨。按照字節(jié)去拿數據钱反。之前不太清楚蘋果有API可以調用掖看。自己寫了2個壓縮字節(jié)的,估計還有些問題面哥。
/** 將數值轉成字節(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];
}
正確的做法如下
/**
* 生成數據報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;
}
/**
* 解析數據報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(@"包頭提示長度和實際長度不一樣");
// }
;
return socketModel;
}
網絡字節(jié)序和主機字節(jié)序
如果你認真看了的話尚卫,你會發(fā)現生成數據和解析數據的時候归榕,我用了這兩個ntohs
htons
函數,主要是生成的字節(jié)高低位和傳輸中的字節(jié)高低位不同吱涉。需要轉換刹泄。
大端和小端(網絡字節(jié)序和主機字節(jié)序)
大端(Big Endian):即網絡字節(jié)序。
小端(Littile Endian):即主機字節(jié)序怎爵。
這里不得不感嘆下API的豐富特石,方便了我們一批API調用者。
詳解大端模式和小端模式
其他
至于一些比較基本的問題:心跳包鳖链、斷線重連(斷線重連要區(qū)分是自己主動斷開還是網絡異常)姆蘸、GCDAsyncSocket的一些代理方法,百度太多見芙委,就不重復介紹了逞敷。
事后看該方案存在一個問題:少包
如果接收區(qū)不夠大,或者已經被占滿灌侣,讀不到剩下所有的包內容推捐,需要增加一個buffer,保留不完整的包侧啼,繼續(xù)去讀取剩余的包內容牛柒。