前言
本篇文章主要讨论如何创建、开启读写流并检查读写流上的错误。此外,还会介绍如何从读流中读出数据,如何向写流中写入数据,如何在读写的过程中防止发生阻塞,如何通过代理服务器来引导流。
CFStream可以用来读写文件或者与socket一起工作。除了创建流的过程之外,它们其他的行为都很相似。
ReadStream
要使用读流,首先需要创建读流对象,代码如下:
1
| CFReadStreamRef myReadStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL);
|
在这段代码中,kCFAllocatorDefault
参数指定了当前为流分配内存的默认系统分配器,fileURL
参数指明了读入文件的文件名,例如file:///Users/joeuser/Downloads/MyApp.sit。
创建成功后就可以打开它。打开流会导致流持有任何它需要的系统资源,如文件描述子用来打开文件。打开流的代码如下:
1 2 3 4 5 6 7 8 9 10 11
| if (!CFReadStreamOpen(myReadStream)) { CFStreamError myErr = CFReadStreamGetError(myReadStream); if (myErr.domain == kCFStreamErrorDomainPOSIX) { } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) { OSStatus macError = (OSStatus)myErr.error; } }
|
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,都会在字节流可以读取之前处于阻塞状态,都会在不阻塞的情况下继续读取直到最后。以下为读取代码:
1 2 3 4 5 6 7 8 9 10 11
| CFIndex numBytesRead; do { UInt8 buf[myReadBufferSize]; 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
方法释放stream对象的引用。你还可以通过将其设置为NULL来使其引用失效。
1 2 3
| CFReadStreamClose(myReadStream); CFRelease(myReadStream); myReadStream = NULL;
|
WriteStream
写入流与读出流原理很相似。主要的区别在于CFWriteStreamWrite
不保证会接收你传入的所有字节,而是会返回它接收的字节数目。注意看下面的代码,如果写入的字节数与需要写入的总字节数不一致,缓冲区会自动调节去容纳它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| CFWriteStreamRef myWriteStream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, fileURL); if (!CFWriteStreamOpen(myWriteStream)) { CFStreamError myErr = CFWriteStreamGetError(myWriteStream); if (myErr.domain == kCFStreamErrorDomainPOSIX) { } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) { OSStatus macError = (OSStatus)myErr.error; } } 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) { bufLen = bufLen - bytesWritten; memmove(buf, buf + bytesWritten, bufLen); CFStreamError error = CFWriteStreamGetError(myWriteStream); reportError(error); } } CFWriteStreamClose(myWriteStream); CFRelease(myWriteStream); myWriteStream = NULL;
|
防止阻塞
当使用流进行通信,特别是基于socket的流时,数据传输可能会耗费很长时间。如果你同步实现了流服务那整个app将会在数据传输上强制等待。因此,异常推荐使用替代方法来阻止阻塞。
当对CFStream对象进行读写操作时有两种方式来防止阻塞:
- runloop——注册接收流相关事件并将stream加入到runloop中。当流相关事件发生时,回调函数会被调用。
- 调查——对于读流,在从流中读取之前检查是否有字节可读,对于写流,在写入流之前检查流是否可被写入。
runloop
使用runloop是比较推荐的方式。runloop在主线程中运行,它会一直等待事件的发生,然后会调用与事件关联的方法。在网络传输过程中,当注册的事件发生时,回调函数会被runloop调用执行。这意味着我们不需要去检查套接字流,也就不会拖慢线程的运行效率。
我们以代码来进一步说明,首先创建套接字读流:
1
| CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host, port, &myReadStream, NULL);
|
其中host参数代表读流指向的主机,port参数代表主机使用的端口号,CFStreamCreatePairWithSocketToCFHost
方法返回新的读流对象的引用并将其存储在myReadStream地址中。最后一个参数NULL代表调用者不想创建写流。如果你想创建写流,就传入对象的地址如&myWriteStream。
在打开套接字读流之前,需要创建上下文,这在注册接收流相关事件的时候会用到:
1
| CFStreamClientContext myContext = {0, myPtr, myRetain, myRelease, myCopyDesc};
|
第一个参数0代表版本号,info参数即myPtr是指向你想要传给回调函数的数据的指针,通常,myPrt指向的结构体中包含的都是与流相关的信息。retain参数是指向持有info参数的方法的指针,如果你像上面代码中一样设置了它,CFStream会调用myRetain(myPtr)
方法持有info指针。类似地,release参数是指向释放info参数方法的指针,当流与上下文断开连接,CFStream会调用myRelease(myPtr)
方法。最后一个参数copyDescription是关于流描述信息的方法。例如,如果你用流上下文调用CFCopyDesc(myReadStream)
方法,CFStream回你调用myCopyDesc(myPtr)
。此外,上下文也允许将retain、release和copyDescription参数置为NULL。如果你将retain、release参数置为NULL,系统会在流对象被销毁前一直让info参数指向的内存存活。如果你将copyDescription参数置为NULL,系统会在必要的时候给出info指针指向的内存中的内容的基本描述。
创建完上下文后,调用CFReadStreamSetClient
方法来注册接收流相关事件。CFReadStreamSetClient
方法要求你指定回调方法和你想要接收的事件。下面的代码中,回调方法想要接收kCFStreamEventHasBytesAvailable, kCFStreamEventErrorOccurred和 kCFStreamEventEndEncountered事件。最后调用CFReadStreamScheduleWithRunLoop
方法将stream添加到runloop中去。
1 2 3 4
| CFOptionFlags registeredEvents = kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (CFReadStreamSetClient(myReadStream, registeredEvents, myCallBack, &myContext)){ CFReadStreamScheduleWithRunLoop(myReadStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); }
|
成功将stream添加到runloop中后,就可以打开stream了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (!CFReadStreamOpen(myReadStream)) { CFStreamError myErr = CFReadStreamGetError(myReadStream); if (myErr.error != 0) { if (myErr.domain == kCFStreamErrorDomainPOSIX) { strerror(myErr.error); } else if (myErr.domain == kCFStreamErrorDomainMacOSStatus) { OSStatus macError = (OSStatus)myErr.error; } } else CFRunLoopRun(); }
|
现在,等待你的回调方法被执行。在回调方法中,检查事件码并做相应的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void myCallBack (CFReadStreamRef stream, CFStreamEventType event, void *myPtr) { switch(event) { case kCFStreamEventHasBytesAvailable: UInt8 buf[BUFSIZE]; CFIndex bytesRead = CFReadStreamRead(stream, buf, BUFSIZE); if (bytesRead > 0) { handleBytes(buf, bytesRead); } 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
方法将stream对象从指定的runloop中移除。最后,调用CFReadStreamClose
方法关闭流,调用CFRelease
释放流对象的引用。
调查
总的来说,调查网络流是不推荐的。然而,在某些特殊的环境下,这种方式会比较有效。首先你要检查流是否准备好进行读写操作,然后在流上执行读写操作。当向写流中写入时,你可以
调用CFWriteStreamCanAcceptBytes
方法来决定流是否开始接收数据。如果返回true,就可以确保在之后的CFWriteStreamWrite
方法调用在不阻塞的情况下可以立即发送数据。类似地,对于读流,在调用CFReadStreamRead
方法之前,先调用CFReadStreamHasBytesAvailable
方法。具体的代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 { } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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)) { bufLen = bufLen - bytesWritten; memmove(buf, buf + bytesWritten, bufLen); CFStreamError error = CFWriteStreamGetError(myWriteStream); reportError(error); } } else { } }
|
防火墙
在stream中应用防火墙的方式有两种。对于大多数流而言,你可以调用SCDynamicStoreCopyProxies
方法获取代理设置,然后通过设置kCFStreamHTTPProxy
(或kCFStreamFTPProxy
)属性将设置应用到stream中。SCDynamicStoreCopyProxies
方法是SystemConfiguration.framework中的一部分,因此在使用该方法的时候你需要在项目中导入<SystemConfiguration/SystemConfiguration.h>
。然后在你使用完之后去释放代理字典的引用,整个过程的代码如下:
1 2
| CFDictionaryRef proxyDict = SCDynamicStoreCopyProxies(NULL); CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, proxyDict);
|
然而,如果你需要在多个流中使用代理设置,这就会变得复杂起来。在这种情况下获得用户机器的防火墙设置需要五个步骤:
1.创建单一持久化的指向动态存储会话SCDynamicStoreRef
的句柄。
2.将句柄添加到runloop中来接收代理变化的通知。
3.用SCDynamicStoreCopyProxies
方法获取最新的代理设置。
4.当代理设置发生改变时更新代理拷贝中的内容。
5.使用结束后清空SCDynamicStoreRef
。
我们接着以代码来详细说明。首先,调用SCDynamicStoreCreate
方法并传入分配子、描述行为的名字、回调方法和动态存储上下文等参数来创建创建动态存储会话的句柄。这会在初始化应用程序时执行。
接着,我们需要将其添加到runloop中。我们要调用SCDynamicStoreKeyCreateProxies
和SCDynamicStoreSetNotificationKeys
方法将设置句柄来管理代理的变化,然后,再调用SCDynamicStoreCreateRunLoopSource
和CFRunLoopAddSource
方法将句柄添加到runloop中去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| SCDynamicStoreContext context = {0, self, NULL, NULL, NULL}; systemDynamicStore = SCDynamicStoreCreate(NULL, CFSTR("SampleApp"), proxyHasChanged, &context); CFStringRef proxiesKey = SCDynamicStoreKeyCreateProxies(NULL); CFArrayRef keyArray = CFArrayCreate(NULL, (const void **)(&proxiesKey), 1, &kCFTypeArrayCallBacks); SCDynamicStoreSetNotificationKeys(systemDynamicStore, keyArray, NULL); CFRelease(keyArray); CFRelease(proxiesKey); CFRunLoopSourceRef storeRLSource = SCDynamicStoreCreateRunLoopSource(NULL, systemDynamicStore, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), storeRLSource, kCFRunLoopCommonModes); CFRelease(storeRLSource);
|
一旦句柄被添加到runloop中,就可以通过调用SCDynamicStoreCopyProxies
方法来预加载当前代理设置和代理字典。
1
| gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore);
|
由于将句柄添加到runloop中,因而每次代理改变时回调方法会被调用。释放当前的代理字典并用新的代理设置来重新加载它。
1 2 3 4
| void proxyHasChanged() { CFRelease(gProxyDict); gProxyDict = SCDynamicStoreCopyProxies(systemDynamicStore); }
|
当所有的代理信息都是最新的时,应用这些代理信息。当创建了读写流后,通过调用CFReadStreamSetProperty
和CFWriteStreamSetProperty
方法可以设置kCFStreamPropertyHTTPProxy
代理。以下以读流为例:
1
| CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, gProxyDict);
|
当我们使用完代理设置后,确保释放字典和句柄,并将句柄从runloop中移除。
1 2 3 4 5 6 7 8 9 10
| if (gProxyDict) { CFRelease(gProxyDict); } CFRunLoopSourceRef rls = SCDynamicStoreCreateRunLoopSource(NULL, systemDynamicStore, 0); CFRunLoopSourceInvalidate(rls); CFRelease(rls); CFRelease(systemDynamicStore);
|
结语
以上就是对读写流的操作的详细解释,如果发现文中有问题或错误,欢迎指出。