CFNetwork背景簡介
CFNetwork是ISO中一個比較底層的網絡框架,C語言編寫,可以控制一些更底層的東西,如各種常用網絡協議、socket通訊等,我們通常使用的NSURL則更傾向于API數據請求等,雖然框架也提供了一些操作,但是遠不如CFNetwork豐富。CFNetwork已經接近于UNIX系統的socket通信了,使用CFHttpMessageRef進行HTTP連接的好處就是控制的粒度更細了,例如你可以設置SSL連接的PeerName,證書驗證的方式,還可以控制每個響應包的接收。不過CFNetwork本質上還是應用層上的封裝的通用API。使用者可以不用關心底層協議的實際細節。下圖是CFNetwork在iOS系統中的位置(圖片來源于官方文檔)。
由上圖可以看出目前iOS的網絡編程分四層:
WebKit:屬于Cocoa層,蘋果很多地方用到的頁面渲染引擎WKWebview;
NSURL:也屬于Cocoa層,對各類URL請求的封裝(NSURLRequest);
CFNetwork:屬于Core Foundation層,基于C的封裝,同樣的還有CFNetServices(write/readstream);
BSD sockets:屬于OS層,也是基于C的封裝;
CFNetwork結構
上圖也是官方文檔的圖片,描述了CFNetwork的結構,下面逐一講解。
CFSocket API
Socket是網絡通訊的底層基礎,兩個socket端口可以互發數據。我們通常使用的是BSD socket,CFSocket則是BSD socket的抽象,基本上實現了幾乎所有BSD socket的功能,并且還融入了run loop。
CFStream API
CFStream API提供了數據讀寫的方法,即讀寫流,使用它可以為內存、文件、網絡(使用socket)的數據建立stream,我們進行網絡請求就是對數據的讀寫,CFStream提供API對兩種CFType對象提供抽象:CFReadStream and CFWriteStream。它同時也是CFHTTP和CFFTP的基礎。stream有一個很重要的特性就是一旦數據流被提供或者被消耗,就不能從流中重新取出。比如這樣
uint8_t d[1024] = {0};
//循環條件:流中是否有可用數據(被讀過的數據不可用了)
while ([self.inputStream hasBytesAvailable]) {
//讀取相應長度的數據數據
NSInteger len = [self.inputStream read:d maxLength:1024];
//如果讀取到數據,便將數據快拼接
if (len > 0 && !self.inputStream.streamError) {
[data appendBytes:(void *)d length:len];
} else {
break;
}
}
CFFTP API
對用FTP協議通信的封裝,能下載、上傳文件和目錄到FTP服務器。CFFTP建立的連接可以是同步或者異步,此次不做詳解。
CFHTTP API
是HTTP協議的抽象,主要對象是CFHTTPMessageRef(類似于我們通常的NSURLRequest)我們需要像構建NSURLRequest那樣來構建CFHTTPMessageRef,同樣包含一下幾個元素
-
必須元素
請求方法 (類型為CFStringRef):POST、GET、DELETE等..
請求的URL地址 (類型為CFURLRef):https://www.baidu.com
請求的HTTP版本(類型為CFStringRef):通常使用kCFHTTPVersion1_1
kCFAllocatorDefault:用于創建消息引用的指定默認的系統內存分配器。
-
可選參數
- body體(類型為CFDataRef)
CFHTTPMessageSetBody(CFHTTPMessageRef message, CFDataRef bodyData) CF_AVAILABLE(10_1, 2_0);
- 消息頭部,如User-Agent等;
CFHTTPMessageSetHeaderFieldValue(CFHTTPMessageRef message, CFStringRef headerField, CFStringRef __nullable value) CF_AVAILABLE(10_1, 2_0);
CFNetwork請求過程
1:構造并創建CFHTTPMessageRef對象
//構造的方式上一步已講
CFHTTPMessageCreateRequest(CFAllocatorRef __nullable alloc, CFStringRef requestMethod, CFURLRef url, CFStringRef httpVersion) CF_AVAILABLE(10_1, 2_0);
2:使用CFHTTPMessageRef對象創建輸入流
//第一個參數傳默認
CFReadStreamCreateForHTTPRequest(CFAllocatorRef __nullable alloc, CFHTTPMessageRef request) CF_DEPRECATED(10_2, 10_11, 2_0, 9_0, "Use NSURLSession API for http requests");
3:適配SNI環境(一個 IP 地址上可以為不同域名分配使用不同的 SSL 證書;這同時意味著,共享 IP 的虛擬主機也可實現 SSL/TLS 連接。)
因為配置sni環境的所有配置都是基于輸入流來操作,所以我們構建完成輸入流之后來處理sni,像這樣
[self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
//請求的URL的Host
NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
[self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
4:打開輸入流
打開輸入流分為兩步
- 設置代理:[self.inputStream setDelegate:weakSelf]
- 加入當前的runloop:
[_inputStream removeFromRunLoop:self.runloop forMode:[self runloopMode]];
- 調用Open方法
5:收到代理數據回調
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
其中分為幾個狀態
typedef NS_OPTIONS(NSUInteger, NSStreamEvent) {
NSStreamEventNone = 0,
NSStreamEventOpenCompleted = 1UL << 0,
NSStreamEventHasBytesAvailable = 1UL << 1,
NSStreamEventHasSpaceAvailable = 1UL << 2,
NSStreamEventErrorOccurred = 1UL << 3,
NSStreamEventEndEncountered = 1UL << 4
};
通常我們會關心NSStreamEventOpenCompleted、NSStreamEventHasBytesAvailable、NSStreamEventErrorOccurred、
由于數據是以流的形式回來,我們需要在在NSStreamEventHasBytesAvailable下取出數據然后做數據拼接,拼接好完整的數據才可使用,像這樣
case NSStreamEventHasBytesAvailable:
{
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;
循環結束后我們的resultData就是完整的返回數據了。