使用CFNetwork進(jìn)行HTTP請(qǐng)求

背景

CFNetwork是比BSD套接字層級(jí)高,比Foundation的NSURLSession層級(jí)低的網(wǎng)絡(luò)API踢涌。CFNetwork更側(cè)重于網(wǎng)絡(luò)協(xié)議,而Foundation級(jí)別API側(cè)重于數(shù)據(jù)訪問序宦,例如通過HTTP或FTP傳輸數(shù)據(jù)睁壁。雖然NSURLSession使用起來更方便,但是對(duì)網(wǎng)絡(luò)協(xié)議的可控性較低互捌,這在iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持中針對(duì)服務(wù)器使用多個(gè)域名和證書問題卻沒有解決辦法潘明,需要依靠低一層的CFNetwork去解決這個(gè)問題。

關(guān)鍵流程

創(chuàng)建請(qǐng)求

在握手之前設(shè)置SNI(iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持第四個(gè)注意事項(xiàng))秕噪∏担客戶端在發(fā)起 SSL 握手請(qǐng)求時(shí)(具體說來,是客戶端發(fā)出 SSL 請(qǐng)求中的 ClientHello 階段)腌巾,就提交請(qǐng)求的 Host 信息遂填,使得服務(wù)器能夠切換到正確的域并返回相應(yīng)的證書铲觉。

// HTTPS請(qǐng)求處理SNI場(chǎng)景
if ([self isHTTPSScheme]) {
    // 設(shè)置SNI host信息
    NSString *host = [self.swizzleRequest.allHTTPHeaderFields objectForKey:@"host"];
    if (!host) {
        host = self.originalRequest.URL.host;
    }
    [self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
    NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
    [self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
}

然后通過wireshare抓取SSL握手中clientHello報(bào)文,查看其中的Server Name Indication extension字段的內(nèi)容進(jìn)行驗(yàn)證:


屏幕快照 2019-05-27 上午12.37.17.png

目前有疑問:
1> 使用Safari進(jìn)行IP直連城菊,SNI中是IP地址备燃;使用Chrome進(jìn)行IP直連,沒有設(shè)置SNI凌唬。

讀取數(shù)據(jù)流

使用CFNetwork與NSURLSession的的最大區(qū)別就是需要自己來維護(hù)數(shù)據(jù)的讀取:

{
    // 創(chuàng)建CFHTTPMessage對(duì)象的輸入流
    CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfRequest);
    self.inputStream = (__bridge_transfer NSInputStream *) readStream;
    
   // 打開流
    __weak typeof(self) weakSelf = self;
    self.runloop = [NSRunLoop currentRunLoop];
    [self startTimer];
    [self.inputStream setDelegate:weakSelf];
    [self.inputStream scheduleInRunLoop:self.runloop forMode:[self runloopMode]];
    [self.inputStream open];
}

在從流中讀取數(shù)據(jù)的時(shí)候漏麦,可能會(huì)等待很長時(shí)間客税,如果使用同步讀取,那么app會(huì)強(qiáng)制等待數(shù)據(jù)傳輸撕贞,因此需要使用非阻塞讀取數(shù)據(jù)的方法更耻,iOS推薦使用runLoop來實(shí)現(xiàn)非阻塞讀取∧笈颍“-scheduleInRunLoop:forMode:”就實(shí)現(xiàn)了通過runLoop來避免阻塞讀取秧均。
大致看一下"-scheduleInRunLoop:forMode:"實(shí)現(xiàn)了一個(gè)什么效果,runLoop是當(dāng)前線程的runLoop号涯,當(dāng)前線程為:

(lldb) po [NSThread currentThread]
<NSThread: 0x600001ad9100>{number = 3, name = com.apple.CFNetwork.CustomProtocols}

通過觀察"-scheduleInRunLoop:forMode:"執(zhí)行前后runLoop中多出來的東西目胡,就可以判斷出該方法向runLoop中注冊(cè)了什么內(nèi)容,經(jīng)過驗(yàn)證链快,是向runLoop中注冊(cè)了一個(gè)source0:

<CFRunLoopSource 0x600003a53a80 [0x111416b68]>{signalled = Yes, valid = Yes, order = 0, context = (
    "<__NSCFInputStream: 0x600003d5b3c0>",
    "<__NSCFInputStream: 0x600003d53330>",
    "<__NSCFOutputStream: 0x600003d522e0>"
)

當(dāng)有數(shù)據(jù)可讀的時(shí)候誉己,當(dāng)前線程上的source0就會(huì)被激活,然后當(dāng)前線程的runLoop被喚醒域蜗,執(zhí)行source0的回調(diào)巨双,這個(gè)回調(diào)中就會(huì)執(zhí)行self.inputStream的
delegate的方法"-stream:handleEvent:"。在有數(shù)據(jù)可讀的時(shí)候霉祸,讀取數(shù)據(jù)筑累,保存進(jìn)本地緩存self.resultData中。

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    switch (eventCode) {
        case NSStreamEventOpenCompleted:
            //NSLog(@"InputStream opened success.");
            break;
        case NSStreamEventHasBytesAvailable:
        {
            if (![self analyseResponse]) {
                return;
            }
            UInt8 buffer[BUFFER_SIZE];
            NSInteger numBytesRead = 0;
            NSInputStream *inputstream = (NSInputStream *) aStream;
            // Read data
            do {
                numBytesRead = [inputstream read:buffer maxLength:sizeof(buffer)];
                if (numBytesRead > 0) {
                    [self.resultData appendBytes:buffer length:numBytesRead];
                }
            } while (numBytesRead > 0);
        }
            break;
        case NSStreamEventErrorOccurred:
            self.completed = YES;
            [self.delegate task:self didCompleteWithError:[aStream streamError]];
            break;
        case NSStreamEventEndEncountered:
            self.completed = YES;
            if (!self.responseAlreadyAnalysed) {
                if (![self analyseResponse]) {
                    return;
                }
            }
            [self handleResult];
            break;
        default:
            break;
    }
}
處理數(shù)據(jù)

在self.inputStream的代理delegate的方法"-stream:handleEvent:"中eventCode為NSStreamEventEndEncountered時(shí)丝蹭,標(biāo)識(shí)數(shù)據(jù)讀取完成慢宗,這時(shí)需要處理數(shù)據(jù),處理數(shù)據(jù)分為兩部分半夷,第一部分是響應(yīng)頭婆廊,第二部分是實(shí)體主體。

處理響應(yīng)頭

首先從self.inputStream中讀取響應(yīng)頭

CFReadStreamRef readStream = (__bridge CFReadStreamRef) self.inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
    return NO;
}
result = CFHTTPMessageIsHeaderComplete(message);

然后判斷是否需要進(jìn)行重定向巫橄,如果返回狀態(tài)碼為301淘邻,302,303則進(jìn)行重定向湘换,

- (BOOL)needRedirection {
    BOOL needRedirect = NO;
    switch (self.response.statusCode) {
            // 永久重定向
        case 301:
            // 暫時(shí)重定向
        case 302:
            // POST重定向GET
        case 303:
        {
            NSString *location = self.response.headerFields[@"Location"];
            if (location) {
                NSURL *url = [[NSURL alloc] initWithString:location];
                NSMutableURLRequest *mRequest = [self.swizzleRequest mutableCopy];
                mRequest.URL = url;
                if ([[self.swizzleRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
                    // POST重定向?yàn)镚ET
                    mRequest.HTTPMethod = @"GET";
                    mRequest.HTTPBody = nil;
                }
                [mRequest setValue:nil forHTTPHeaderField:@"host"];
                self.redirectRequest = mRequest;
                needRedirect = YES;
                break;
            }
        }
            // POST不重定向?yàn)镚ET宾舅,詢問用戶是否攜帶POST數(shù)據(jù)(很少使用)
            //case 307:
            //    break;
        default:
            break;
    }
    return needRedirect;
}

如果是HTTPS協(xié)議统阿,則需要校驗(yàn)證書,校驗(yàn)證書的時(shí)候需要獲取request的header中的host字段的值(iOS下IP直連避免DNS劫持第一個(gè)注意事項(xiàng))來與服務(wù)器證書中的域名進(jìn)行比較(iOS下IP直連避免DNS劫持第三個(gè)注意事項(xiàng))筹我。

// HTTPS校驗(yàn)證書
if ([self isHTTPSScheme]) {
    SecTrustRef trust = (__bridge SecTrustRef) [self.inputStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
    SecTrustResultType res = kSecTrustResultInvalid;
    NSMutableArray *policies = [NSMutableArray array];
    NSString *domain = [[self.swizzleRequest allHTTPHeaderFields] valueForKey:@"host"];
    if (domain) {
        [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
    } else {
        [policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
    }
    // 綁定校驗(yàn)策略到服務(wù)端的證書上
    SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies);
    if (SecTrustEvaluate(trust, &res) != errSecSuccess) {
        [self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
        result = NO;
    } else if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
        // 證書驗(yàn)證不通過
        [self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
        result = NO;
    }
}
處理實(shí)體主體

處理實(shí)體主體需要注意的只有1點(diǎn)扶平,就是當(dāng)響應(yīng)頭中的"Content-Encoding"為"gzip"時(shí),需要進(jìn)行解壓蔬蕊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末结澄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子岸夯,更是在濱河造成了極大的恐慌麻献,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猜扮,死亡現(xiàn)場(chǎng)離奇詭異勉吻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)旅赢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門齿桃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人煮盼,你說我怎么就攤上這事短纵。” “怎么了孕似?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵踩娘,是天一觀的道長。 經(jīng)常有香客問我喉祭,道長养渴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任泛烙,我火速辦了婚禮理卑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔽氨。我一直安慰自己藐唠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布鹉究。 她就那樣靜靜地躺著宇立,像睡著了一般。 火紅的嫁衣襯著肌膚如雪自赔。 梳的紋絲不亂的頭發(fā)上妈嘹,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音绍妨,去河邊找鬼润脸。 笑死柬脸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的毙驯。 我是一名探鬼主播倒堕,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼爆价!你這毒婦竟也來了垦巴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤允坚,失蹤者是張志新(化名)和其女友劉穎魂那,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稠项,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年鲜结,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了展运。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡精刷,死狀恐怖拗胜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情怒允,我是刑警寧澤埂软,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站纫事,受9級(jí)特大地震影響勘畔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丽惶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一炫七、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钾唬,春花似錦万哪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至儒士,卻和暖如春的止,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乍桂。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工冲杀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留效床,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓权谁,卻偏偏與公主長得像剩檀,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子旺芽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容