Kealdish's Studio.

CFNetwork学习笔记(二)

字数统计: 3.2k阅读时长: 13 min
2016/10/06 Share

前言

本篇文章主要讨论如何创建、开启读写流并检查读写流上的错误。此外,还会介绍如何从读流中读出数据,如何向写流中写入数据,如何在读写的过程中防止发生阻塞,如何通过代理服务器来引导流。

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);
// 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说明打开流的进程已经开始。我们可以通过调用CFReadStreamGetStatusCFWriteStreamGetStatus方法检查开启的状态,如果开启还在进行中,则返回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]; // 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 );

当所有的数据读取完毕后,你应该调用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);
// 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;

防止阻塞

当使用流进行通信,特别是基于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) {
// 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();
}

现在,等待你的回调方法被执行。在回调方法中,检查事件码并做相应的处理:

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:
// 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方法将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
// Polling in 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...
}
}
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
// Polling in 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...
}
}

防火墙

在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中。我们要调用SCDynamicStoreKeyCreateProxiesSCDynamicStoreSetNotificationKeys方法将设置句柄来管理代理的变化,然后,再调用SCDynamicStoreCreateRunLoopSourceCFRunLoopAddSource方法将句柄添加到runloop中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.Create a handle to a dynamic store session
SCDynamicStoreContext context = {0, self, NULL, NULL, NULL};
systemDynamicStore = SCDynamicStoreCreate(NULL, CFSTR("SampleApp"), proxyHasChanged, &context);
// 2.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);
// 3.Add the dynamic store to the run loop
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);
}

当所有的代理信息都是最新的时,应用这些代理信息。当创建了读写流后,通过调用CFReadStreamSetPropertyCFWriteStreamSetProperty方法可以设置kCFStreamPropertyHTTPProxy代理。以下以读流为例:

1
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, gProxyDict);

当我们使用完代理设置后,确保释放字典和句柄,并将句柄从runloop中移除。

1
2
3
4
5
6
7
8
9
10
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);

结语

以上就是对读写流的操作的详细解释,如果发现文中有问题或错误,欢迎指出。

CATALOG
  1. 1. 前言
  2. 2. ReadStream
  3. 3. WriteStream
  4. 4. 防止阻塞
    1. 4.0.1. runloop
    2. 4.0.2. 调查
  • 5. 防火墙
  • 6. 结语