版本记录
版本号 | 时间 |
---|---|
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
枚举定义的值之一。
打开一个流可能是一个漫长的过程,所以CFReadStreamOpen
和CFWriteStreamOpen
函数通过返回TRUE来避免阻塞,以指示打开流的过程已经开始。要检查open的状态,调用函数CFReadStreamGetStatus
和CFWriteStreamGetStatus
,如果打开仍在进行中,则返回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)
。
客户端上下文还允许您选择将retain
,release
和copyDescription
参数设置为NULL
。如果将retain和release参数设置为NULL,那么系统会希望您保持info指针指向的内存处于活动状态,直到流本身被销毁。如果您将copyDescription
参数设置为NULL,则系统将根据请求提供信息指针指向的内存内容的基本描述。
设置客户端上下文后,调用函数CFReadStreamSetClient
注册以接收与流相关的事件。 CFReadStreamSetClient
要求您指定回调函数和您想要接收的事件。Listing 2-6
中的以下示例指定回调函数想要接收kCFStreamEventHasBytesAvailable
,kCFStreamEventErrorOccurred
和kCFStreamEventEndEncountered
事件。然后使用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);
创建对动态存储的引用后,需要将其添加到运行循环中。 首先,获取动态存储引用并设置它以监视代理的任何更改。 这是通过函数SCDynamicStoreKeyCreateProxies
和SCDynamicStoreSetNotificationKeys
完成的。 然后,您可以使用函数SCDynamicStoreCreateRunLoopSource
和CFRunLoopAddSource
将动态存储引用添加到运行循环中。 你的代码应该如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);
}
由于所有代理信息都是最新的,因此应用代理。 创建读取或写入流后,通过调用函数CFReadStreamSetProperty
或CFWriteStreamSetProperty
来设置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);
后记
本篇主要介绍了处理流的相关逻辑,感兴趣的给个赞或者关注~~~~
网友评论