iOS 基于GCDAsyncSocket實(shí)現(xiàn)的即時(shí)聊天

本文不討論技術(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,/nGCDAsyncSocket也有方法按照此邏輯直接切割。但這個(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)容。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容