美文网首页Vender
CFNetwork框架详细解析(四) —— CFNetwork编

CFNetwork框架详细解析(四) —— CFNetwork编

作者: 刀客传奇 | 来源:发表于2018-06-09 10:46 被阅读86次

    版本记录

    版本号 时间
    V1.0 2018.06.09

    前言

    CFNetwork框架访问网络服务并处理网络配置的变化。 建立在网络协议抽象的基础上,可以简化诸如使用BSD套接字,管理HTTP和FTP服务器以及管理Bonjour服务等任务。接下来几篇我们就一起看一下这个框架。感兴趣的可以看上面几篇文章。
    1. CFNetwork框架详细解析(一) —— 基本概览
    2. CFNetwork框架详细解析(二) —— CFNetwork编程指导之简介(一)
    3. CFNetwork框架详细解析(三) —— CFNetwork编程指导之CFNetwork概念(二)

    Working with Streams - 处理流

    本章讨论如何创建,打开并检查读取和写入流上的错误。 它还介绍了如何从读取流中读取,如何写入写入流,如何防止在读取流或写入流时发生阻塞,以及如何通过代理服务器导航流。


    Working with Read Streams - 处理读入流

    Core Foundation流可用于读取或写入文件或使用网络套接字。 除了创建这些流的过程之外,它们的行为相似。

    1. Creating a Read Stream - 创建读入流

    首先创建一个读取流。 Listing 2-1为一个文件创建一个读取流。

    Listing 2-1  Creating a read stream from a file
    
    CFReadStreamRef myReadStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL);
    

    在此列表中,kCFAllocatorDefault参数指定使用当前默认系统分配器为流分配内存,fileURL参数指定要为其创建此读取流的文件的名称,例如file:///Users/joeuser/Downloads/MyApp.sit

    同样,您可以通过调用CFStreamCreatePairWithSocketToCFHost(在Using a Run Loop to Prevent Blocking中介绍)或CFStreamCreatePairWithSocketToNetService(在NSNetServices and CFNetServices Programming Guide中介绍)来创建基于网络服务的一对流。

    现在你已经创建了流,你可以打开它。 打开流会导致流保留所需的任何系统资源,例如打开文件所需的文件描述符。 Listing 2-2是打开读取流的示例。

    Listing 2-2  Opening a read stream
    
    if (!CFReadStreamOpen(myReadStream)) {
        CFStreamError myErr = CFReadStreamGetError(myReadStream);
        // An error has occurred.
            if (myErr.domain == kCFStreamErrorDomainPOSIX) {
            // Interpret myErr.error as a UNIX errno.
            } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) {
            // Interpret myErr.error as a MacOS error code.
                OSStatus macError = (OSStatus)myErr.error;
            // Check other error domains.
        }
    }
    

    CFReadStreamOpen函数返回TRUE表示成功,如果打开由于某种原因失败,则返回FALSE。如果CFReadStreamOpen返回FALSE,则该示例调用CFReadStreamGetError函数,该函数返回由两个值组成的CFStreamError类型的结构:域代码和错误代码。域代码指示应如何解释错误代码。例如,如果域代码是kCFStreamErrorDomainPOSIX,则错误代码是一个UNIX errno值。其他错误域是kCFStreamErrorDomainMacOSStatus,它指示错误代码是MacErrors.h中定义的OSStatus值,kCFStreamErrorDomainHTTP指示错误代码是由CFStreamErrorHTTP枚举定义的值之一。

    打开一个流可能是一个漫长的过程,所以CFReadStreamOpenCFWriteStreamOpen函数通过返回TRUE来避免阻塞,以指示打开流的过程已经开始。要检查open的状态,调用函数CFReadStreamGetStatusCFWriteStreamGetStatus,如果打开仍在进行中,则返回kCFStreamStatusOpening;如果打开已完成,则返回kCFStreamStatusOpen;如果打开已完成但失败,则返回kCFStreamStatusErrorOccurred。在大多数情况下,打开是否完成并不重要,因为读取和写入的CFStream函数将阻塞,直到流打开。

    2. Reading from a Read Stream - 从读取流中读取

    要从读取流中读取数据,请调用函数CFReadStreamRead,该函数与UNIX read()系统调用类似。 都采用缓冲区和缓冲区长度参数。 两者都返回读取的字节数,如果在流或文件结束时返回0,如果发生错误则返回-1。 两者都会阻塞,直到至少有一个字节可以被读取,并且只要不阻塞就可以继续读取。 Listing 2-3是读取流的示例。

    Listing 2-3  Reading from a read stream (blocking)
    
    CFIndex numBytesRead;
    do {
        UInt8 buf[myReadBufferSize]; // define myReadBufferSize as desired
        numBytesRead = CFReadStreamRead(myReadStream, buf, sizeof(buf));
        if( numBytesRead > 0 ) {
            handleBytes(buf, numBytesRead);
        } else if( numBytesRead < 0 ) {
            CFStreamError error = CFReadStreamGetError(myReadStream);
            reportError(error);
        }
    } while(numBytesRead > 0);
    

    3. Tearing Down a Read Stream - 关闭读取流

    当所有数据都被读取后,您应该调用CFReadStreamClose函数来关闭流,从而释放与之关联的系统资源。 然后通过调用函数CFRelease来释放流引用。 您可能还想通过将引用设置为NULL来使引用无效。 有关示例,请参阅Listing 2-4。

    Listing 2-4  Releasing a read stream
    
    CFReadStreamClose(myReadStream);
    CFRelease(myReadStream);
    myReadStream = NULL;
    

    Working with Write Streams - 处理写入流

    使用写入流类似于使用读取流。 一个主要区别是函数CFWriteStreamWrite不保证接受所有传递它的字节。 相反,CFWriteStreamWrite返回它接受的字节数。 在代码Listing 2-5所示的示例代码中,您会注意到,如果写入的字节数与要写入的总字节数不相同,则会调整缓冲区以适应此情况。

    Listing 2-5  Creating, opening, writing to, and releasing a write stream
    
    CFWriteStreamRef myWriteStream =
            CFWriteStreamCreateWithFile(kCFAllocatorDefault, fileURL);
    if (!CFWriteStreamOpen(myWriteStream)) {
        CFStreamError myErr = CFWriteStreamGetError(myWriteStream);
        // An error has occurred.
        if (myErr.domain == kCFStreamErrorDomainPOSIX) {
        // Interpret myErr.error as a UNIX errno.
        } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) {
            // Interpret myErr.error as a MacOS error code.
            OSStatus macError = (OSStatus)myErr.error;
            // Check other error domains.
        }
    }
    UInt8 buf[] = “Hello, world”;
    CFIndex bufLen = (CFIndex)strlen(buf);
     
    while (!done) {
        CFIndex bytesWritten = CFWriteStreamWrite(myWriteStream, buf, (CFIndex)bufLen);
        if (bytesWritten < 0) {
            CFStreamError error = CFWriteStreamGetError(myWriteStream);
            reportError(error);
        } else if (bytesWritten == 0) {
            if (CFWriteStreamGetStatus(myWriteStream) == kCFStreamStatusAtEnd) {
                done = TRUE;
            }
        } else if (bytesWritten != bufLen) {
            // Determine how much has been written and adjust the buffer
            bufLen = bufLen - bytesWritten;
            memmove(buf, buf + bytesWritten, bufLen);
     
            // Figure out what went wrong with the write stream
            CFStreamError error = CFWriteStreamGetError(myWriteStream);
            reportError(error);
     
        }
    }
    CFWriteStreamClose(myWriteStream);
    CFRelease(myWriteStream);
    myWriteStream = NULL;
    

    Preventing Blocking When Working with Streams - 使用流时防止阻塞

    在使用流进行通信时,数据传输可能需要很长时间才会出现,尤其是在基于套接字的流中。 如果你正在同步实现你的流,你的整个应用程序将被迫等待数据传输。 因此,强烈建议您的代码使用备用方法来防止阻塞。

    在读取或写入CFStream对象时,有两种方法可以防止阻塞:

    • 使用运行循环 - 注册以接收与流相关的事件并在运行循环中调度流。 当发生与流相关的事件时,会调用您的回调函数(由注册调用指定)。
    • 轮询 - 对于读取流,在从流中读取数据之前查明是否有要读取的字节。 对于写入流,请在写入流之前确定是否可以在不阻塞的情况下写入流。

    以下各节将介绍每种方法。

    1. Using a Run Loop to Prevent Blocking - 使用运行循环来防止阻塞

    使用流的首选方式是使用运行循环。 运行循环在主程序线程上执行。 它等待事件发生,然后调用与给定事件相关的任何函数。

    在网络传输的情况下,当您注册的事件发生时,您的回调函数由运行循环执行。 这允许你不必轮询你的套接字流,轮询会减慢线程。

    要了解有关运行循环的更多信息,请阅读Threading Programming Guide

    此示例从创建套接字读取流开始:

    CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host, port,
                                       &myReadStream, NULL);
    

    其中CFHost对象引用host指定要用来读取流的远程主机,而port参数指定主机使用的端口号。 CFStreamCreatePairWithSocketToCFHost函数返回myReadStream中的新读取流引用。 最后一个参数NULL表示调用者不想创建写入流。 如果你想创建一个写入蒸汽,最后一个参数是例如&myWriteStream

    在打开套接字读取流之前,请创建一个在注册以接收与流相关的事件时将使用的上下文:

    CFStreamClientContext myContext = {0, myPtr, myRetain, myRelease, myCopyDesc};
    

    第一个参数是0来指定版本号。info参数myPtr是一个指向要传递给回调函数的数据的指针。通常,myPtr是指向您定义的结构的指针,其中包含与流相关的信息。 retain参数是指向保留info参数的函数的指针。因此,如果将其设置为函数myRetain,如上面的代码所示,CFStream将调用myRetain(myPtr)来保留info指针。同样,release参数myRelease是指向释放info参数的函数的指针。当流与上下文分离时,CFStream会调用myRelease(myPtr)。最后,copyDescription是一个函数的参数,用于提供流的描述。例如,如果您要使用上面显示的流客户端上下文来调用CFCopyDesc(myReadStream),则CFStream会调用myCopyDesc(myPtr)

    客户端上下文还允许您选择将retainreleasecopyDescription参数设置为NULL。如果将retain和release参数设置为NULL,那么系统会希望您保持info指针指向的内存处于活动状态,直到流本身被销毁。如果您将copyDescription参数设置为NULL,则系统将根据请求提供信息指针指向的内存内容的基本描述。

    设置客户端上下文后,调用函数CFReadStreamSetClient注册以接收与流相关的事件。 CFReadStreamSetClient要求您指定回调函数和您想要接收的事件。Listing 2-6中的以下示例指定回调函数想要接收kCFStreamEventHasBytesAvailablekCFStreamEventErrorOccurredkCFStreamEventEndEncountered事件。然后使用CFReadStreamScheduleWithRunLoop函数将流安排在运行循环中。有关如何执行此操作的示例,请参见Listing 2-6

    Listing 2-6  Scheduling a stream on a run loop
    
    CFOptionFlags registeredEvents = kCFStreamEventHasBytesAvailable |
            kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
    if (CFReadStreamSetClient(myReadStream, registeredEvents, myCallBack, &myContext))
    {
        CFReadStreamScheduleWithRunLoop(myReadStream, CFRunLoopGetCurrent(),
                                        kCFRunLoopCommonModes);
    }
    

    通过运行循环中的流调度,您可以打开流,如Listing 2-7所示。

    Listing 2-7  Opening a nonblocking read stream
    
    if (!CFReadStreamOpen(myReadStream)) {
        CFStreamError myErr = CFReadStreamGetError(myReadStream);
        if (myErr.error != 0) {
        // An error has occurred.
            if (myErr.domain == kCFStreamErrorDomainPOSIX) {
            // Interpret myErr.error as a UNIX errno.
                strerror(myErr.error);
            } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) {
                OSStatus macError = (OSStatus)myErr.error;
                }
            // Check other domains.
        } else
            // start the run loop
            CFRunLoopRun();
    }
    

    现在,等待你的回调函数被执行。 在您的回调函数中,检查事件代码并采取适当的措施。 参见代码Listing 2-8

    Listing 2-8  Network events callback function
    
    void myCallBack (CFReadStreamRef stream, CFStreamEventType event, void *myPtr) {
        switch(event) {
            case kCFStreamEventHasBytesAvailable:
                // It is safe to call CFReadStreamRead; it won’t block because bytes
                // are available.
                UInt8 buf[BUFSIZE];
                CFIndex bytesRead = CFReadStreamRead(stream, buf, BUFSIZE);
                if (bytesRead > 0) {
                    handleBytes(buf, bytesRead);
                }
                // It is safe to ignore a value of bytesRead that is less than or
                // equal to zero because these cases will generate other events.
                break;
            case kCFStreamEventErrorOccurred:
                CFStreamError error = CFReadStreamGetError(stream);
                reportError(error);
                CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
                                                  kCFRunLoopCommonModes);
                CFReadStreamClose(stream);
                CFRelease(stream);
                break;
            case kCFStreamEventEndEncountered:
                reportCompletion();
                CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
                                                  kCFRunLoopCommonModes);
                CFReadStreamClose(stream);
                CFRelease(stream);
                break;
        }
    }
    

    当回调函数收到kCFStreamEventHasBytesAvailable事件代码时,它会调用CFReadStreamRead读取数据。

    当回调函数收到kCFStreamEventErrorOccurred事件代码时,它会调用CFReadStreamGetError来获取错误和它自己的错误函数(reportError)来处理错误。

    当回调函数接收到kCFStreamEventEndEncountered事件代码时,它会调用自己的函数(reportCompletion)来处理数据结束,然后调用CFReadStreamUnscheduleFromRunLoop函数从指定的运行循环中删除流。 然后运行CFReadStreamClose函数关闭流和CFRelease以释放流引用。

    2. Polling a Network Stream - 轮询网络流

    一般来说,轮询网络流是不可取的。 但是,在某些罕见情况下,这样做可能很有用。 要轮询流,首先检查流是否准备好读取或写入,然后对流执行读取或写入操作。

    写入写入流时,可以通过调用CFWriteStreamCanAcceptBytes来确定流是否准备好接受数据。 如果它返回TRUE,那么你可以确定随后调用CFWriteStreamWrite函数将立即发送数据而不会阻塞。

    同样,对于读取流,在调用CFReadStreamRead之前,调用函数CFReadStreamHasBytesAvailable

    Listing 2-9是读取流的轮询示例。

    Listing 2-9  Polling a read stream
    
    while (!done) {
        if (CFReadStreamHasBytesAvailable(myReadStream)) {
            UInt8 buf[BUFSIZE];
            CFIndex bytesRead = CFReadStreamRead(myReadStream, buf, BUFSIZE);
            if (bytesRead < 0) {
                CFStreamError error = CFReadStreamGetError(myReadStream);
                reportError(error);
            } else if (bytesRead == 0) {
                if (CFReadStreamGetStatus(myReadStream) == kCFStreamStatusAtEnd) {
                    done = TRUE;
                }
            } else {
                handleBytes(buf, bytesRead);
            }
        } else {
            // ...do something else while you wait...
        }
    }
    

    Listing 2-10是写入流的轮询示例。

    Listing 2-10  Polling a write stream
    
    UInt8 buf[] = “Hello, world”;
    UInt32 bufLen = strlen(buf);
     
    while (!done) {
        if (CFWriteStreamCanAcceptBytes(myWriteStream)) {
            int bytesWritten = CFWriteStreamWrite(myWriteStream, buf, strlen(buf));
            if (bytesWritten < 0) {
                CFStreamError error = CFWriteStreamGetError(myWriteStream);
                reportError(error);
            } else if (bytesWritten == 0) {
                if (CFWriteStreamGetStatus(myWriteStream) == kCFStreamStatusAtEnd)
                {
                    done = TRUE;
                }
            } else if (bytesWritten != strlen(buf)) {
                // Determine how much has been written and adjust the buffer
                bufLen = bufLen - bytesWritten;
                memmove(buf, buf + bytesWritten, bufLen);
     
                // Figure out what went wrong with the write stream
                CFStreamError error = CFWriteStreamGetError(myWriteStream);
                reportError(error);
            }
        } else {
            // ...do something else while you wait...
        }
    }
    

    Navigating Firewalls - 浏览防火墙

    将防火墙设置应用于流有两种方法。 对于大多数流,您可以使用SCDynamicStoreCopyProxies函数检索代理设置,然后通过设置kCFStreamHTTPProxy(或kCFStreamFTPProxy)属性将结果应用于流。 SCDynamicStoreCopyProxies函数是System Configuration框架的一部分,因此您需要在项目中包含<SystemConfiguration / SystemConfiguration.h>以使用该函数。 然后,只需在完成后发布代理字典参考。 该过程将如Listing 2-11所示

    Listing 2-11  Navigating a stream through a proxy server
    
    CFDictionaryRef proxyDict = SCDynamicStoreCopyProxies(NULL);
    CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, proxyDict);
    

    但是,如果您需要将代理设置经常用于多个流,则会变得更加复杂。 在这种情况下,检索用户机器的防火墙设置需要五个步骤:

    • 为动态存储会话创建一个持久句柄SCDynamicStoreRef
    • 将句柄放入动态存储会话中,以便通知代理更改。
    • 使用SCDynamicStoreCopyProxies检索最新的代理设置。
    • 当被告知更改时更新您的代理副本。
    • 清理SCDynamicStoreRef时,请仔细阅读。

    要创建动态存储会话的句柄,请使用函数SCDynamicStoreCreate并传递一个分配器,一个名称来描述您的进程,一个回调函数和一个动态存储上下文SCDynamicStoreContext。 这在初始化应用程序时运行。 代码与Listing 2-12中的类似。

    Listing 2-12  Creating a handle to a dynamic store session
    
    SCDynamicStoreContext context = {0, self, NULL, NULL, NULL};
    systemDynamicStore = SCDynamicStoreCreate(NULL,
                                              CFSTR("SampleApp"),
                                              proxyHasChanged,
                                              &context);
    

    创建对动态存储的引用后,需要将其添加到运行循环中。 首先,获取动态存储引用并设置它以监视代理的任何更改。 这是通过函数SCDynamicStoreKeyCreateProxiesSCDynamicStoreSetNotificationKeys完成的。 然后,您可以使用函数SCDynamicStoreCreateRunLoopSourceCFRunLoopAddSource将动态存储引用添加到运行循环中。 你的代码应该如Listing 2-13所示。

    Listing 2-13  Adding a dynamic store reference to the run loop
    
    // Set up the store to monitor any changes to the proxies
    CFStringRef proxiesKey = SCDynamicStoreKeyCreateProxies(NULL);
    CFArrayRef keyArray = CFArrayCreate(NULL,
                                        (const void **)(&proxiesKey),
                                        1,
                                        &kCFTypeArrayCallBacks);
    SCDynamicStoreSetNotificationKeys(systemDynamicStore, keyArray, NULL);
    CFRelease(keyArray);
    CFRelease(proxiesKey);
     
    // Add the dynamic store to the run loop
    CFRunLoopSourceRef storeRLSource =
        SCDynamicStoreCreateRunLoopSource(NULL, systemDynamicStore, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), storeRLSource, kCFRunLoopCommonModes);
    CFRelease(storeRLSource);
    

    一旦将动态存储引用添加到运行循环中,可以使用它通过调用SCDynamicStoreCopyProxies将代理字典预加载当前代理设置。 有关如何执行此操作,请参见Listing 2-14。

    Listing 2-14  Loading the proxy dictionary
    
    gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore);
    

    由于将动态存储引用添加到运行循环中,每次更改代理时,都会运行回调函数。 释放当前的代理字典并使用新的代理设置重新加载它。 示例回调函数看起来像Listing 2-15中的那个。

    Listing 2-15  Proxy callback function
    
    void proxyHasChanged() {
        CFRelease(gProxyDict);
        gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore);
    }
    

    由于所有代理信息都是最新的,因此应用代理。 创建读取或写入流后,通过调用函数CFReadStreamSetPropertyCFWriteStreamSetProperty来设置kCFStreamPropertyHTTPProxy代理。 如果您的流是一个名为readStream的读取流,那么您的函数调用将如Listing 2-16所示

    Listing 2-16  Adding proxy information to a stream
    
    CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, gProxyDict);
    

    当您完成使用代理设置时,请确保释放字典和动态存储引用,并从运行循环中删除动态存储引用。 参见Listing 2-17

    Listing 2-17  Cleaning up proxy information
    
    if (gProxyDict) {
        CFRelease(gProxyDict);
    }
     
    // Invalidate the dynamic store's run loop source
    // to get the store out of the run loop
    CFRunLoopSourceRef rls = SCDynamicStoreCreateRunLoopSource(NULL, systemDynamicStore, 0);
    CFRunLoopSourceInvalidate(rls);
    CFRelease(rls);
    CFRelease(systemDynamicStore);
    

    后记

    本篇主要介绍了处理流的相关逻辑,感兴趣的给个赞或者关注~~~~

    相关文章

      网友评论

        本文标题:CFNetwork框架详细解析(四) —— CFNetwork编

        本文链接:https://www.haomeiwen.com/subject/oceosftx.html