[深入淺出Cocoa]iOS網絡編程之CFNetwork


[深入淺出Cocoa]iOS網絡編程之CFNetwork

羅朝輝 (http://www.cnblogs.com/kesalin/)

本文遵循“署名-非商業用途-保持一致”創作公用協議
 

一,CFNetwork 簡介

首先來回顧下。在前文《[深入淺出Cocoa]iOS網絡編程之Socket》中,提到iOS網絡編程層次模型分為三層:
  • Cocoa層:NSURL,Bonjour,Game Kit,WebKit
  • Core Foundation層:基於 C 的 CFNetwork 和 CFNetServices
  • OS層:基於 C 的 BSD socket
前文講的是最底層的 socket,本文將介紹位於 Core Foundation 中的 CFNetwork。CFNetwork 只是對 BSD socket 的進行了輕量級的封裝,但在 iOS 中使用 CFNetwork 有一個顯著的好處,那就是 CFNetwork 與系統級別的設置(如:天線設置)以及 run-loop 結合得很好。每一個線程都有自己的 run-loop,因此我們可以 CFNetwork 當中事件源加入到 run-loop 中,這樣就可以在線程的 run-loop 中處理網絡事件了。BTW,大名鼎鼎的 ASIHttpRequest 庫就是基於 CFNetwork 封裝的。
 
本文示例代碼就是這樣做的,源碼請查看:
 

二,CFNetwork API 簡介

CFNetwork 接口是基於 C 的,下面的接口用於創建一對 socket stream,一個用於讀取,一個用於寫入:
void CFStreamCreatePairWithSocketToHost(CFAllocatorRef alloc, CFStringRef host, UInt32 port, CFReadStreamRef *readStream, CFWriteStreamRef *writeStream);

該函數使用 host 以及 port,CFNetwork 會將該 host 轉換為 IP 地址,並轉換為網絡字節順序。如果我們只需要一個 socket stream,我們可以將另外一個設置為 NULL。還有另外兩個“重載”的創建 socket sream的接口:CFStreamCreatePairWithSocket 和 CFStreamCreatePairWithPeerSocketSignature,在這里就不一一介紹了。

注意:這些 socket stream 在使用之前就如原生 socket 一樣,必須顯式地調用其 open 函數:

Boolean CFReadStreamOpen(CFReadStreamRef stream);

Boolean CFWriteStreamOpen(CFWriteStreamRef stream);

但與 socket 不同的是,這兩個接口是異步的,當成功 open 之后,如果調用方設置了獲取 kCFStreamEventOpenCompleted 事件的標志的話就會其調用回調函數。

而該回調函數及其參數設置是通過如下接口進行的:

Boolean CFReadStreamSetClient(CFReadStreamRef stream, CFOptionFlags streamEvents, CFReadStreamClientCallBack clientCB, CFStreamClientContext *clientContext);

Boolean CFWriteStreamSetClient(CFWriteStreamRef stream, CFOptionFlags streamEvents, CFWriteStreamClientCallBack clientCB, CFStreamClientContext *clientContext);

該函數用於設置回調函數及相關參數。通過 streamEvents 標志來設置我們對哪些事件感興趣;clientCB 是一個回調函數,當事件標志對應的事件發生時,該回調函數就會被調用;clientContext 是用於傳遞參數到回調函數中去。

當設置好回調函數之后,我們可以將 socket stream 當做事件源調度到 run-loop 中去,這樣 run-loop 就能分發該 socket stream 的網絡事件了。

void CFReadStreamScheduleWithRunLoop(CFReadStreamRef stream, CFRunLoopRef runLoop, CFStringRef runLoopMode);

void CFWriteStreamScheduleWithRunLoop(CFWriteStreamRef stream, CFRunLoopRef runLoop, CFStringRef runLoopMode);

注意,在我們不再關心該 socket stream 的網絡事件時,記得要調用如下接口將 socket stream 從 run-loop 的事件源中移除。

void CFReadStreamUnscheduleFromRunLoop(CFReadStreamRef stream, CFRunLoopRef runLoop, CFStringRef runLoopMode);

void CFWriteStreamUnscheduleFromRunLoop(CFWriteStreamRef stream, CFRunLoopRef runLoop, CFStringRef runLoopMode);

當我們將 socket stream 的網絡事件調度到 run-loop 之后,我們就能在回調函數中相應各種事件,比如 kCFStreamEventHasBytesAvailable 讀取數據:

Boolean CFReadStreamHasBytesAvailable(CFReadStreamRef stream);

CFIndex CFReadStreamRead(CFReadStreamRef stream, UInt8 *buffer, CFIndex bufferLength);

或 kCFStreamEventCanAcceptBytes 寫入數據:

Boolean CFWriteStreamCanAcceptBytes(CFWriteStreamRef stream);

CFIndex CFWriteStreamWrite(CFWriteStreamRef stream, const UInt8 *buffer, CFIndex bufferLength);

最后,我們調用 close 方法關閉 socket stream:

void CFReadStreamClose(CFReadStreamRef stream);

void CFWriteStreamClose(CFWriteStreamRef stream);

 

三,客戶端示例代碼

與 socket 演示類似,在這里我只演示客戶端示例。同樣,我們也在一個后台線程中啟動網絡操作:

    NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", serverHost, serverPort]];
    NSThread * backgroundThread = [[NSThread alloc] initWithTarget:self
                                                          selector:@selector(loadDataFromServerWithURL:)
                                                            object:url];
    [backgroundThread start];

然后在 loadDataFromServerWithURL 中創建 socket 流,並設置其回調函數,將其加入到 run-loop 的事件源中,然后啟動之:

- (void)loadDataFromServerWithURL:(NSURL *)url
{
    NSString * host = [url host];
    NSInteger port = [[url port] integerValue];
    
    // Keep a reference to self to use for controller callbacks
    //
    CFStreamClientContext ctx = {0, (__bridge void *)(self), NULL, NULL, NULL};
    
    // Get callbacks for stream data, stream end, and any errors
    //
    CFOptionFlags registeredEvents = (kCFStreamEventHasBytesAvailable | kCFStreamEventEndEncountered | kCFStreamEventErrorOccurred);
    
    // Create a read-only socket
    //
    CFReadStreamRef readStream;
    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef)host, port, &readStream, NULL);
    
    // Schedule the stream on the run loop to enable callbacks
    //
    if (CFReadStreamSetClient(readStream, registeredEvents, socketCallback, &ctx)) {
        CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        
    }
    else {
        [self networkFailedWithErrorMessage:@"Failed to assign callback method"];
        return;
    }
    
    // Open the stream for reading
    //
    if (CFReadStreamOpen(readStream) == NO) {
        [self networkFailedWithErrorMessage:@"Failed to open read stream"];
        
        return;
    }
    
    CFErrorRef error = CFReadStreamCopyError(readStream);
    if (error != NULL) {
        if (CFErrorGetCode(error) != 0) {
            NSString * errorInfo = [NSString stringWithFormat:@"Failed to connect stream; error '%@' (code %ld)", (__bridge NSString*)CFErrorGetDomain(error), CFErrorGetCode(error)];
            [self networkFailedWithErrorMessage:errorInfo];
        }
        
        CFRelease(error);
        
        return;
    }
    
    NSLog(@"Successfully connected to %@", url);
    
    // Start processing
    //
 CFRunLoopRun();
}

參考前面的接口說明,相信你不難理解上面的代碼。前面唯一沒有提到的接口就是 CFReadStreamCopyError,該接口用於獲取當前的錯誤信息,如果沒有錯誤則返回 NULL。

CFErrorRef CFReadStreamCopyError(CFReadStreamRef stream);

CFErrorRef CFWriteStreamCopyError(CFWriteStreamRef stream);

此外,我們還可以調用如下接口獲取 socket stream 的當前狀態:

CFStreamStatus CFReadStreamGetStatus(CFReadStreamRef stream);

CFStreamStatus CFWriteStreamGetStatus(CFWriteStreamRef stream);

在上面的代碼中,我們設置了當有數據可以讀取,流到達結尾處時以及錯誤發生時調用回調函數 socketCallback:

void socketCallback(CFReadStreamRef stream, CFStreamEventType event, void * myPtr)
{
    KSCFNetworkViewController * controller = (__bridge KSCFNetworkViewController *)myPtr;
    
    switch(event) {
        case kCFStreamEventHasBytesAvailable: {
            // Read bytes until there are no more
            //
            while (CFReadStreamHasBytesAvailable(stream)) {
                UInt8 buffer[kBufferSize];
                int numBytesRead = CFReadStreamRead(stream, buffer, kBufferSize);
                
                [controller didReceiveData:[NSData dataWithBytes:buffer length:numBytesRead]];
            }
            
            break;
        }
            
        case kCFStreamEventErrorOccurred: {
            CFErrorRef error = CFReadStreamCopyError(stream);
            if (error != NULL) {
                if (CFErrorGetCode(error) != 0) {
                    NSString * errorInfo = [NSString stringWithFormat:@"Failed while reading stream; error '%@' (code %ld)", (__bridge NSString*)CFErrorGetDomain(error), CFErrorGetCode(error)];
                    
                    [controller networkFailedWithErrorMessage:errorInfo];
                }
                
                CFRelease(error);
            }
            
            break;
        }
            
        case kCFStreamEventEndEncountered:
            // Finnish receiveing data
            //
            [controller didFinishReceivingData];
            
            // Clean up
            //
 CFReadStreamClose(stream);
            CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
            CFRunLoopStop(CFRunLoopGetCurrent());
            
            break;
            
        default:
            break;
    }
}

上面的代碼也很好理解,當有數據可以讀取時,讀取之,然后更新 UI;當流到達結尾處時,關閉流,執行清理工作;當錯誤發送時,報告錯誤信息。

 

四,擴展

雖然上面的代碼只演示了如何使用 CFNetwork 的 CFReadStream 來讀取數據,寫入數據使用 CFWriteStream,其工作流程也是一樣的。在這里就不再介紹了。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM