处理读操作流
我们从创建一个读操作流开始。列表 2-1 为一个文件创建了读操作流。
列表 2-1 为文件创建读操作流
CFReadStreamRef myReadStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL);
在这行代码中,kCFAllocatorDefault
参数指定了当前缺省的系统定位符,它用于定位操作流的内存地址,fileURL
指定了创建的读操作流所对应的文件名称,比如 file:///Users/joeuser/Downloads/MyApp.sit
。
一旦操作流被创建,它就可以被打开。打开一个操作流会导致这个流占用它所需要的任何系统资源,比如用于打开文件的文件描述符。列表2-2 中的示例代码说明了如何打开读操作流。
列表 2-2 打开一个读操作流
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
表示操作流的打开过程已经开始。如果想要检查打开过程的状态,可以调用函数 CFReadStreamGetStatus
和CFWriteStreamGetStatus
,如果返回 kCFStreamStatusOpening
说明打开过程仍然在进行中,如果返回 kCFStreamStatusOpen
说明打开过程已经完成,而返回 kCFStreamStatusErrorOccurred
说明打开过程已经完成,但是失败了。大部分情况下,打开过程是否完成并不重要,因为 CFStream 中负责读写操作的函数在操作流打开以前会被阻塞。
想要从读操作流中读取数据的话,需要调用函数 CFReadStreamRead
,它类似于 UNIX 的 read()
系统调用。二者的相同之处包括:都需要缓冲区和缓冲区大小作为参数,都会返回读取的字节数,如果到了文件末尾会返回 0 ,如果遇到错误就会返回 -1。另外,二者都会在至少一个字节可以被读取之前被阻塞,并且如果没有遇到阻塞的情况下都会继续读取。列表 2-3 是从读操作流中获取数据的示例程序。
列表 2-3 从读操作流中获取数据 (阻塞)
CFIndex numBytesRead;
do {
UInt8 buf[kReadBufSize];
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 );
当所有数据都被读取之后,你应该调用 CFReadStreamClose
函数关闭操作流,这样可以释放与它相关的系统资源。接着通过调用函数CFRelease
释放操作流的引用对象。你也可以通过把引用对象设置为 NULL 使它无效。请参考列表
2-4 中的示例说明。
列表 2-4 释放一个读操作流
CFReadStreamClose(myReadStream);
CFRelease(myReadStream);
myReadStream = NULL;
处理写操作流
处理写操作流的方式与处理读操作流的方式非常类似。其中一个主要的区别就是,函数 CFWriteStreamWrite
不会保证接受你所传递给它的所有数据。相反,CFWriteStreamWrite
会返回它所接受的字节数。你会留意到在列表 2-5 中,如果写入的字节数与需要写入的总字节数不相同的话,缓冲区会根据这个情况进行调整。
列表 2-5 创建、打开、写入和释放一个写操作流
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”;
UInt32 bufLen = strlen(buf);
while (!done) {
CFTypeRef 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);
}
}
CFWriteStreamClose(myWriteStream);
CFRelease(myWriteStream);
myWriteStream = NULL;
处理操作流的时候防止阻塞
当利用流进行通讯的时候,常常会发生数据传输耗费大量时间,尤其是进行基于 socket 的流操作时。如果流操作是同步方式,那么整个应用都不得不停下来等待数据传输完成。因此,强烈建议您的代码中利用其它方法防止阻塞。
在读写 CFStream 对象的时候,有两种方法防止阻塞:
-
轮询 — 在进行读操作的时候,在从流中读取数据以前检查是否有字节可读。在进行写操作的时候,在写入流之前检查数据是否可以无阻塞的写入。
-
利用一个循环 — 注册接收一个与流相关的事件,并把流放入一个循环当中。当与流相关的事件发生时,你的回调函数(由注册函数指定) 就会被调用。
下面几节将一一介绍这些方法。
本节内容:
利用轮询防止阻塞
轮询的时候,需要确定读写操作流的状态是否就绪。在写入一个写操作流的时候,这是通过调用函数 CFWriteStreamCanAcceptBytes
来实现的。如果返回 TRUE
,那么你就可以利用 CFWriteStreamWrite
函数,因为它可以马上进行写入操作而不会被阻塞。类似的,对于一个读操作流,在调用 CFReadStreamRead
函数之前,可以调用函数 CFReadStreamHasBytesAvailable
。通过这种流轮询方式,你可以避免为了等待操作流就绪而阻塞整个线程。
列表 2-6 是针对读操作流的一个轮询范例。
列表 2-6 轮询一个读操作流
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...
}
}
列表 2-7 是一个写操作流的轮询范例。
列表 2-7 轮询一个写操作流
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...
}
}
利用循环防止阻塞
线程的循环会监控某些事件的发生。当这些事件发生的时候,循环会调用某个特定的函数。循环会一直监测它的输入源的事件。在进行网络传输的时候,当你注册的事件发生时,你的回调函数会被循环所执行。这使你不必轮询你的 socket 操作流,这种轮询会降低线程执行效率。如果对循环不是很熟悉,可以阅读循环中的内容。
本例以创建一个 socket 读操作流开始:
CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host, port,
&myReadStream, NULL);
其中 CFHost 引用对象 host
确定了读操作流指向的远程主机,port
参数确定了这个主机所使用的端口号码。CFStreamCreatePairWithSocketToCFHost
函数返回了新的读操作流引用 myReadStream
。最后一个参数 NULL
确定调用者不想创建一个写操作流。如果你想要创建一个写操作流,那么最后一个参数应该是,比如 &myWriteStream
这样的形式。
在打开 socket 读操作流之前,需要创建一个上下文对象,这个对象在你注册接收流相关的事件时会用到:
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
,那么如果需要的话,系统将会提供一个 info
指针指向的内存的基本描述信息。
在客户端上下文设置好之后,可以调用函数 CFReadStreamSetClient
登记接收与操作流相关的事件。CFReadStreamSetClient
要求你指定一个回调函数,以及想要接收的事件。下面列表 2-8 的范例中,回调函数需要接收 kCFStreamEventHasBytesAvailable
,kCFStreamEventErrorOccurred
以及 kCFStreamEventEndEncountered
事件。然后利用函数CFReadStreamScheduleWithRunLoop
在循环中调度这个操作流。列表 2-8 中的范例说明了如何进行这个操作。
列表 2-8 在循环中调度一个操作流
CFOptionsFlags registeredEvents = kCFStreamEventHasBytesAvailable |
kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
if (CFReadStreamSetClient(myReadStream, registeredEvents, myCallBack, &myContext)
{
CFReadStreamScheduleWithRunLoop(myReadStream, CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
}
当已经在循环中调度了操作流之后,你就可以象列表 2-9 中那样打开操作流了。
列表 2-9 打开一个非阻塞的读操作流
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();
}
现在,只需要等待回调函数被执行到了。在你的回调函数中,要检查事件代码并采取相应的措施。参见列表 2-10。
列表 2-10 网络事件回调函数
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);
CFReadStreamClose(stream);
CFRelease(stream);
break;
case kCFStreamEventEndEncountered:
reportCompletion();
CFReadStreamClose(stream);
CFRelease(stream);
break;
}
}
当回调函数接收到 kCFStreamEventHasBytesAvailable
事件代码时,它会调用 CFReadStreamRead
读取数据。
当回调函数接收到 kCFStreamEventErrorOccurred
事件代码时,它会调用 CFReadStreamGetError
来获取错误信息以及它自己的错误处理函数 (reportError
) 来处理这个错误.
当回调函数接收到 kCFStreamEventEndEncountered
事件代码时,它会调用它自己的函数(reportCompletion
) 处理数据结尾,然后调用函数 CFReadStreamClose
关闭操作流和 CFRelease
函数释放操作流引用。调用 CFReadStreamClose
会导致操作流被取消调度,因此回调函数不需要从循环中删除操作流。
利用防火墙
有两种方式设置操作流的防火墙。对大多数流来说,可以通过 SCDynamicStoreCopyProxies
函数获取代理设置,然后通过设置kCFStreamHTTPProxy
(或者 kCFStreamFTPProxy
) 参数将结果设置到操作流上。SCDynamicStoreCopyProxies
函数是系统配置框架的一部分,因此需要在项目中包含 <SystemConfiguration/SystemConfiguration.h>
,这样才能使用该函数。接着在使用之后把代理词典引用释放调。这个过程看起来类似列表 2-11 中的例子。
列表 2-11 通过代理服务器浏览一个操作流
CFDictionaryRef proxyDict = SCDynamicStoreCopyProxies(NULL);
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, proxyDict);
不过,如果你需要在多个操作流中使用代理设置的话,就会稍微复杂一点。这种情况下,获取用户机器的防火墙设置需要5个步骤:
-
创建一个指向动态存储会话的单独持久性句柄,比如
SCDynamicStoreRef
。 -
把指向动态存储会话的句柄放入循环当中,使它能够得到代理变化的通知。
-
利用
SCDynamicStoreCopyProxies
获取最新的代理设置。 -
在得到变动通知后更新代理的拷贝。
-
在使用完毕后清除
SCDynamicStoreRef
。
可以使用函数 SCDynamicStoreCreate
来创建一个指向动态存储会话的句柄,并向它传递一个定位器、一个描述过程的名称、一个回调函数和一个动态存储上下文 SCDynamicStoreContext
。在你的应用进行初始化的时候就会执行这个步骤。具体代码类似列表 2-12。
列表 2-12 创建一个指向动态存储会话的句柄
SCDynamicStoreContext context = {0, self, NULL, NULL, NULL};
systemDynamicStore = SCDynamicStoreCreate(NULL,
CFSTR("SampleApp"),
proxyHasChanged,
&context);
在创建了指向动态存储的引用之后,你需要把它加入循环当中。首先,对这个动态存储引用进行设置,使它能够监控代理的任何变化。这个可以通过函数 SCDynamicStoreKeyCreateProxies
和 SCDynamicStoreSetNotificationKeys
来实现。接着,你可以利用函数SCDynamicStoreCreateRunLoopSource
和 CFRunLoopAddSource
把动态存储引用加入到循环当中。你的代码应该类似列表 2-13。
列表 2-13 把一个动态存储引用加入循环
// 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
加载代理词典的当前代理设置,如何做请参考列表2-14 。
列表 2-14 加载代理词典
gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore);
一旦把动态存储引用加入了循环当中,每当代理发生变化,你的回调函数就会被调用。当前的代理词典会被释放掉
,并利用新的代理设置重新载入这个代理词典。列表 2-15 中是一个回调数的范例。
列表 2-15 代理回调函数
void proxyChanged() {
CFRelease(gProxyDict);
gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore);
}
由于所有的代理信息都是最新的,这样就可以使用这些代理。在创建了您的读写操作流之后, 调用函数 CFReadStreamSetProperty
或者CFWriteStreamSetProperty
设置 kCFStreamPropertyHTTPProxy
代理。如果你的操作流是一个读操作流,名称为 readStream
,那么你的函数调用将如列表 2-16 所示。
列表 2-16 把代理信息加入一个操作流
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, gProxyDict);
在使用完毕代理设置信息之后,请确保释放了词典和动态存储引用,并将动态存储引用从循环中清除。请参考列表 2-17。
列表 2-17 清理代理信息
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(systemDynmaicStore);