iOS 基于GCDAsyncSocket實現的即時聊天

本文不討論技術選型,不介紹業務邏輯。
簡單地介紹下開發中會遇到的一些技術點,解決的一些方案。

粘包

因為是基于TCP的,而TCP的是流式傳輸的,不像UDP數據報傳輸是有邊界的,所以會有粘包問題。
然而這個問題是必須解決的,大致有三種方法。
1:固定包的長度,每次讀數據的時候,固定讀取字節。這在實際使用中基本不現實。
2:服務器每次發送消息的時候,給每個包添加上分隔符,如/r,/nGCDAsyncSocket也有方法按照此邏輯直接切割。但這個方案也非常不靠譜。
3:一般的處理方法是定義一個消息頭,消息頭中包含了一個包的長度,先拿到包長度再去讀取完整的包。

Socket通信定義
 頭信息:2字節 版本號
 功能代碼:2字節  功能代碼
 是否壓縮: 1個字節 0不壓縮, 1壓縮
 消息長度:2字節
 消息實體:

功能代碼是指消息的類型,定義了許多許多, 因為每個項目定義都不一樣,就不介紹了。
按照我們的通信定義,我是這么處理粘包的:

    //解決粘包
    //思路是拆分包頭得到長度,判斷接得到的長度和包頭的長度是否一致,不夠就繼續拼接,相等就返回,大于的話就自己做下拆分。先
- (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可以區分我的讀數據請求,如上tag==0 是我發起的讀消息頭的返回,再用消息頭中的字節長度去讀完整的包,即tag==1的返回。

乍看一下似乎沒有問題了,我也是這么以為的。同事之前寫過量級比較大的通信,他遇到過問題,因為網卡緩沖區有個最大值,MTU,如果一下子來N多消息,字節會溢出,有可能會導致字節的少讀多讀。所以要嚴格按照代碼以上的注釋寫代碼,時間有限,我這邊先不上代碼。

生成數據報和解數據報

dataLength = [self getDataLength:data];

上面的代碼有這樣一個方法。按照字節去拿數據。之前不太清楚蘋果有API可以調用。自己寫了2個壓縮字節的,估計還有些問題。

/** 將數值轉成字節。編碼方式:低位在前,高位在后 */
- (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;
}

網絡字節序和主機字節序

如果你認真看了的話,你會發現生成數據和解析數據的時候,我用了這兩個ntohs htons函數,主要是生成的字節高低位和傳輸中的字節高低位不同。需要轉換。
大端和小端(網絡字節序和主機字節序)
大端(Big Endian):即網絡字節序。
小端(Littile Endian):即主機字節序。
這里不得不感嘆下API的豐富,方便了我們一批API調用者。
詳解大端模式和小端模式

其他

至于一些比較基本的問題:心跳包、斷線重連(斷線重連要區分是自己主動斷開還是網絡異常)、GCDAsyncSocket的一些代理方法,百度太多見,就不重復介紹了。

事后看該方案存在一個問題:少包

如果接收區不夠大,或者已經被占滿,讀不到剩下所有的包內容,需要增加一個buffer,保留不完整的包,繼續去讀取剩余的包內容。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容