一个stream是编程世界中的基本抽象:比特串从一点到另一点之间串行地传输。Cocoa提供了三个类来表示streams并且方便你在程序中使用:NSStream,NSInputStream,NSOutputStream。用这些类实例,你可以从文件或者程序内存中读取或者写数据。你也可以使用这些对象在基于socket的连接中和远程主机交换数据。你也可以继承这些stream类来得到专门的stream行为。
这篇文档的组织
这篇文档包含下面文章
- Cocoa Streams 对Cocoa Stream类作一个综述,描述了结构,功能和一般使用。
- Reading From Input Streams 解释了如何创造和预备一个(不是socket)输入流对象。它也描绘了如何处理不同NSInputStream对象产生的stream事件。
- Writing To Ouput Streams 解释了如何创造和预备一个(不是socket)输出流对象。它也描绘了如何处理不同NSOutputStream对象产生的stream事件。
- Polling Versus Run-Loop Scheduling 讨论了这两个技术在读写流时避免阻塞的优点。它也阐释了如何使用stream类的API轮询stream数据。
- Handling Stream Errors 描述了如何应对stream处理中发生的错误。
Setting Up Socket Streams 解释了如何建立stream对象来通过socket和远程主机通讯。
看看别的
如果你在实现基于socket的网络流,你会发现下面的这些外部资源非常有用
OpenSSL-http://www.openssl.org/
- Apache SSL-http://www.apache-ssl.org/
- SOCKS - http://tools.ietf.org/html/rfc1928
Cocoa流
流为程序和不同媒介之间以设备独立的方式交换数据提供了便捷。一个流是一个连续的比特序列,在通信路径上串行传输。流是单向的因此,从程序的角度来看,一个流可以是输入(读)流或者是输出(写)流。除了基于文件的流,流都是none-seekable-一旦提供了数据或者消耗了数据,不能从stream中再次获取。
Cocoa包含了三个流相关的类:NSStream,NSInputStream和NSOutputStream。NSStream是一个抽象类,定义了所有流对象的基本接口和特征。NSInputStream和NSOutputStream都NSStream的子类,实现了默认的输入流和输出流的行为。你可以生成NSOutputStream实例将流数据导向内存或者写入到一个文件或者C缓冲器。你可以生成一个NSInputStream从NSData对象或者一个文件读取数据。你也可以在一个基于socket的网络连接的两个终点生成NSInputStream和NSOutputStream,你可以使用流对象而不需要将所有的流数据加载到内存中。下图阐述了就着源或者目的而言输入流和输出流对象的类型。
因为它们处理基本的计算抽象(流),所以NSStream和它的子类是用来完成低层的编程任务。如何有更高层的Cocoa API适合完成特定的任务(举个例子,NSURL或者NSFileHandle),使用更高层的API。
流对象有相关的特征。大多数特征和网络安全还有配置有关,即secure-socket(SSL)levels和SOCKS代理信息。另外两个重要特征是NSStreamDataWrittenToMemoryStreamKey,这个允许输出流获取写到内存的数据,还有NSStreamFileCurrentOffsetKey,允许你操纵基于文件的流的读写位置。
一个流对象也有一个关联代理。如果代理没有明确设定,这个流对象自己成为代理(对于定制子类来说是一个很好的传统)。一个流对象为每个流关联的事件调用唯一的代理方法stream:handleEvent:。在这些事件中,表明输入流中有可读的字节和输出流中有空间可以接收字节的事件尤为重要。对于这两个事件,代理发送恰当的消息给流-read:maxLength:或者read:maxLength,取决于流的类型-从流中得到字节还是将字节投放到流中。
NSStream是构筑于Core Foundation的CFStream之上的。这个亲密关系意味着NSStream的具体子类,NSInputStream和NSOutputStream,和它们的Core Foundation对应CFReadStream和CFWriteStream是toll-free桥接的。虽然Cocoa和Core Foundation流API之间存在很强的相似性,它们的实现不是一致的。Cocoa流类为异步行为使用代理模型(run-loop调度),而Core Foundation使用client回调。Core Foundation流类型设置client(Core Foundation中叫做context)和NSStream设置代理大相径庭。设置代理的呼叫和设置context的呼叫不要混淆。除此之外,你可以在你的代码中自由的呼叫这两个APIs。
从输入流中读取
在Cocoa中,从NSInputStream实例中读取包括了几个步骤
- 从一个数据源创建和初试化一个NSInputStream实例。
- 在run loop中调度这个流对象并且打开这个流。
- 应对流对象报告给它代理的事件。
- 当没有可读数据的时候,处理掉这个流对象。
注意 在这个文档中的例子显示了在run loops上调度流对象并且设置代理处理流事件的策略。你也可以使用轮询来代替run loo调度。然而,run-loop调度和代理的配搭是最受人尊崇的方法,有几个原因,后面的章节会涉及到。
准备流对象
开始使用NSInputStream对象,你必须要有这个流的数据源。这个数据源可以是一个文件,一个NSData对象,或者一个网络socket。
注意 从网络socket初始化输入流对象的过程和从其他两个数据源初始化流对象的过程不同,在这个文章中没有涉及。学习为网络连接初始化一个NSInputStream实例,请看Setting Up Socket Streams。
NSInputStream的初试器和factory方法允许你创造和初始化来自文件或者NSData的实例。下面阐述了从文件生成的NSInputStream实例。
- (void)setUpStreamForFile:(NSString *)path {
// iStream is NSInputStream instance variable
iStream = [[NSInputStream alloc] initWithFileAtPath:path];
[iStream setDelegate:self];
[iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[iStream open];
}
如这个例子所示,当你生成了这个对象后,你应该设置代理,当这个对象调度在run loop并且具有流相关事件需要报告时,这个代理从NSInputStream对象接收stream:handleEvent:消息,例如流中有可以读的字节。
在你打开流开始数据流动之前,发送一个scheduleInRunLoop:ForMode消息给流对象,让流对象在run loo中接收流事件。通过这个,你帮助了代理在没有可读数据的时候不会阻塞程序。如果流动发生在另一个线程,确保调度流对象在那个线程的run loop。你永远不要企图从一个没有拥有这个流对象run loop的线程访问这个流。最后,发送NSInputStream实例的open消息来开始数据的流动。
应对流事件
当向流对象发送open后,你可以发现它的状态,它是否有可读字节,或者任何错误的本质。
返回的状态是一个NSStreamStatus常量,只是这个流打开,阅读,在流的最后等等。返回的错误是一个NSError对象,将发生的错误信息封装了。
更重要的时,一旦流对象被开打,它持续地发送stream:handleEvent:消息给它的代理,直到流的末尾。这些消息包含了一个NSStreamEvent常量参数,指示事件的类型。对于NSInputStream对象,最重要的事件类型时NSStreamEventOpenCompleted,NSStreamEventHasBytesAvailable和NSStreamEventEndEncountered。代理通常对NSStreamEventHasBytesAvailable事件感兴趣。下面阐述了一个应对这种类型事件的好方法。
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if(!_data) {
_data = [[NSMutableData data] retain];
}
uint8_t buf[1024];
unsigned int len = 0;
len = [(NSInputStream *)stream read:buf maxLength:1024];
if(len) {
[_data appendBytes:(const void *)buf length:len];
// bytesRead is an instance variable of type NSNumber.
[bytesRead setIntValue:[bytesRead intValue]+len];
} else {
NSLog(@"no buffer!");
}
break;
}
在这个stream:handleEvent:实现中,这个代理使用了switch语句鉴定NSStreamEvent常量。如果是NSStreamEventHasBytesAvailable,代理如果需要的化创造一个NSMutableData对象来保存接收的字节。然后它声明了一个缓冲器,调用流对象的read:maxLength:方法,这个方法将指定数量的字节装到缓冲器中。如果这个读取操作顺利地从流对象中获取数据,代理将这些字节附着在这个NSMutableData对象后。
对于一次读取多少字节没有硬性指导。虽然在一个事件中可以读取流中所有的数据,这取决于流的长度(它里面的字节数量)和kernel的行为,包括设备和socket的特征。最好的方法是使用合理的缓冲器大小,例如512字节,1024字节,或者一页大小。
当这个NSInputStream对象经历错误时,它停止流动,使用NSStreamEventErrorOccurred通知代理。
处理流对象
当一个NSInputStream对象来到了流的末尾,它发送给代理NSStreamEventEndEncounterred事件。这个代理应该处理这个流对象,按照预备对象的方法作个镜反操作,换句话说,他应该首先关闭这个流对象,从run loop中移除,最后release它。下面的代码给出了你应该怎么做。
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
switch(eventCode) {
case NSStreamEventEndEncountered:
{
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[stream release];
stream = nil; // stream is ivar, so reinit it
break;
}
// continued ...
}
}
写到输出流
使用NSOutputStream实例写输出有几个有求
- 创建和初始化一个NSOutputStream实例。设置代理
- 在run loop中调度这个流对象,并且打开这个流。
- 应对流对象报告给代理的事件
- 如果流对象向内存写数据,请求NSStreamDataWrittenToMemoryStreamKey特征来获取数据。
- 当没有可写数据时,处理这个流对象。
预备流对象
为了使用NSOutputStream对象,你必须指明向流对象写入数据的归属地。输出流对象的目的可以是一个文件,一个C缓冲器,应用内存,或者一个网络socket。
下面的代码向你展示了如何生成一个NSOutputStream实例,会将数据写到内存中。
- (void)createOutputStream {
NSLog(@"Creating and opening NSOutputStream...");
// oStream is an instance variable
oStream = [[NSOutputStream alloc] initToMemory];
[oStream setDelegate:self];
[oStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[oStream open];
}
你可以从代码中看到,当你生成了这个对象后,你应该设置它的代理。
在你开始打开流来开始数据流动之前,发送一个scheduleInRunLoop:forMode:消息到流对象调度这个流对象在一个run loop中接收流事件。通过这样,你帮助了代理在流无法接收字节的时候避免阻塞程序。如果流动发生在另一个线程,确保调度这个流对象在那个线程的run loop。你永远不要在一个没有拥有这个流对象所调度的run-loop的线程上访问这个流。最后,向NSOutputStream实例发送一个open消息来开始数据流动到输出容器上。
应对流事件
当一个流打开后,你可以发现它的状态,流是否有空间可以写数据,和发生错误的本质。
返回的状态是一个NSStreamStatus常量,指示这个流是否打开,是否在写入,是否到了流的末尾等等。返回的错误是一个NSError对象,封装了错误的信息。
更重要的是,当一个流被打开后,它持续地发送stream:handleEvent:消息给它的代理(只要代理一直往流里塞数据)直到到了流的末尾。这些消息包含了一个参数,是NSStreamEvent类型的,指示事件的类型。对于NSOutputStream对象,最常见的事件类型是NSStreamEventOpenCompleted,NSStreamEventHasSpaceAvailable,NSStreamEndEncountered。这个代理通常最感兴趣的是NSStreamEventHasSpaceAvailable事件。下面的代码向你显示了如何应对这种类型事件的方法。
Listing 2 Handling a space-available event
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
switch(eventCode) {
case NSStreamEventHasSpaceAvailable:
{
uint8_t *readBytes = (uint8_t *)[_data mutableBytes];
readBytes += byteIndex; // instance variable to move pointer
int data_len = [_data length];
unsigned int len = ((data_len - byteIndex >= 1024) ?
1024 : (data_len-byteIndex));
uint8_t buf[len];
(void)memcpy(buf, readBytes, len);
len = [stream write:(const uint8_t *)buf maxLength:len];
byteIndex += len;
break;
}
// continued ...
}
}
在这个stream:handleEvent实现中,代理使用了switch语句来鉴定NSStreamEvent常量。如果常量是NSStreamEventHasSpaceAvailable,这个代理从一个NSMutableData对象获取字节,然后为了当前的写操作移动这个指针。它接着决定写操作的字节容量(1024或者剩余的字节),声明那个大小的缓冲器,将数据复制到这个缓冲器。接着代理调用输出流对象的write:maxLength:方法将缓冲器的数据灌到输出流。最后它为下一次的操作,提高readBytes的指针。
如果代理收到NSStreamEventHasSpaceAvailable事件但是并没有写到流里,它不会从run-loop中再接收space-available事件直到这个NSOutputStream对象接收更多的字节。当这个发生时,run loop重启space-available事件。如果在你的实现中这场景可能发生,当你的代理在接收NSStreamEventHasSpaceAvailable事件后不写到流里时,你可以对标记你的代理。以后,当你的程序有更多的字节要写时,它可以校验这个代理,如果设置了标记,程序直接往输出流实例里添加数据。
对于每次写多少字节数据没有硬性指导。虽然在一次事件中将所有的数据写到流里是可能的,这取决于外部的因素,流入kernel的习惯那位IE,设备和socket的特征。最好的方法是使用合理的缓冲器,例如512字节,1024字节,或者一页大小。
当NSOutputStream对象在写到流里的过程中遇到错误时,它停止了流动并且通知代理NSStreamEventErrorOccurred。这个代理应该处理这个错误。
处理流对象
当一个NSOutputStream对象结束了向输出流写数据,它发送一个NSStreamEventEndEncountered事件在stream:handleEvent消息给它的代理。在这个点上,代理应该处理这个流对象,按照预备这个流对象的过程作个镜反处理。换句话说,它首先关闭这个流对象,从run loop中移除这个对象 最后将其释放。更进一步的是,如果NSOutputStream独享的目的地是应用程序(你使用initToMemory或者factory方法outputStreamToMemory),你应该想要从内存中获取数据。下面的代码向你展示了你可能要作的事情。
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
switch(eventCode) {
case NSStreamEventEndEncountered:
{
NSData *newData = [oStream propertyForKey:
NSStreamDataWrittenToMemoryStreamKey];
if (!newData) {
NSLog(@"No data written to memory!");
} else {
[self processData:newData];
}
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[stream release];
oStream = nil; // oStream is instance variable
break;
}
// continued ...
}
}
你获取写到内存的流对象是通过向NSOutputStream对象发送propertyForKey:消息,指明一个NSStreamDataWrittenToMemoryStreamKey,这个流对象返回数据在一个NSData对象中。
轮询VS Run-Loop调度
流处理的一个潜在问题就是阻塞。一个向流中写或者从流中读的线程可能需要无休止地等待直到流中有空间灌字节或者流中有数据可以读。事实上,线程受了流的支配,这会给应用带来麻烦。阻塞在socket流中更是一个问题,因为他们依赖远程主机的响应。
用Cocoa流,你有两种方法应对流事件
- Run-loop调度 你在一个run loop中调度流,只有当阻塞不可能发生的时候,代理才接收报告流相关事件的消息。对于读写操作,相关的NSStreamEvent常量是NSStreamEventHasBytesAvailable和NSStreamEventHasSpaceAvailable。
轮询 你持续地询问流对象是否有可读字节或者是否有可写空间。这个相关的方法是hasBytesAvailable和hasSpaceAvailable。
Run-loop相比轮询总是更受偏爱,这也是为什么之前的代码例子中无一例外地使用了run loops。使用轮询,程序被锁定在很紧的循环中,等待流事件,这些事件可能马上到来也可能要很久才到来。使用run-loop调度,你的程序可以离开做些别的事,知道当有流事件要处理时它会被通知到。run loop不需要你管理状态,比轮询更有效率。轮询对CPU消耗很大。
也有些情形,轮询是可选项。举个例子,如果你在移植古老代码,你可能选择使用轮询,因为它更适合古老代码中线程模型。下面的代码阐述了一个方法,使用轮询将代码写到输出流中。
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
NSLog(@"stream:handleEvent: is invoked...");
switch(eventCode) {
case NSStreamEventErrorOccurred:
{
NSError *theError = [stream streamError];
NSAlert *theAlert = [[NSAlert alloc] init];
[theAlert setMessageText:@"Error reading stream!"];
[theAlert setInformativeText:[NSString stringWithFormat:@"Error %i: %@",
[theError code], [theError localizedDescription]]];
[theAlert addButtonWithTitle:@"OK"];
[theAlert beginSheetModalForWindow:[NSApp mainWindow]
modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[stream close];
[stream release];
break;
}
// continued ....
}
}
建立Socket流
你可以使用CFStream API建立一个socket连接,使用产生的流对象,向远程主机发送或者接收数据。你也可以为了安全配置这个连接。
基本过程
NSStream类不支持iOS中连接到远程主机。CFStream支持。然后,一旦你使用CFStream API生成了流,你可以利用NSStream和CFStream toll-free桥接的优点将你的CFStream投掷为NSStream。只要呼叫CFStreamCreatePairWithSocketToHost函数,提供一个主机名字和一个端口号,接收一个CFReadStreamRef和CFWriteStreamRef。你可以投掷这些对象到一个NSInputStream和一个NSOutputStream。
下面的代码阐释了CFStreamCreatePairWithSocketToHost使用。这个例子显示了CFReadStreamRef对象和CFWriteStreamRef对象的生成。如果你想要只接受一个,只要指定NULL作为不想要对象的参数值。
- (IBAction)searchForSite:(id)sender
{
NSString *urlStr = [sender stringValue];
if (![urlStr isEqualToString:@""]) {
NSURL *website = [NSURL URLWithString:urlStr];
if (!website) {
NSLog(@"%@ is not a valid URL");
return;
}
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)[website host], 80, &readStream, &writeStream);
NSInputStream *inputStream = (__bridge_transfer NSInputStream *)readStream;
NSOutputStream *outputStream = (__bridge_transfer NSOutputStream *)writeStream;
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
/* Store a reference to the input and output streams so that
they don't go away.... */
...
}
}
如果你传递的是无效参数,请求的CFReadStream和CFWriteStream中的其中之一或者两个都是NULL。一旦你将这些对象投掷为NSStreams,设置代理,在run loop中调度,打开这些流。这个代理会开始收到流有关的消息。
保护和配置这个连接
在你打开这个流对象之前,你可能想要为这个和远程主机的连接设置安全和其他的特色(例如,一个HTTPS服务端)。NSStream定义了特征,这些特征影响TCP/IPsocket连接的安全性
- Secure Socket Layer(SSL)。一个安全协议使用数字证书为TCP/IP提供数据加密,服务端认证,消息加密,客户端认证。
- SOCKS代理服务。一个服务端位于客户应用和真正的服务端之间。它拦截通往真正服务端的请求,如果它不能从缓存中满足这个请求,它就将这个请求发送到真正的服务端。SOCKS代理服务提高了网络的性能,也可以用来过滤请求。
对于SSL安全,NSStream定义了不同的安全等级特征(例如,NSStreamSocketSecurityLevelSSLv2)。你通过发送setProperty:forKey:来设置这些特征。
NSStreamSocketSecurityLevelKey为key名。例如
[inputStream setProperty:NSStreamSocketSecurityLevelTLSv1 forKey:NSStreamSocketSecurityLevelKey];
你必须要在打开这个流之前设定这个特征。一旦打开,它会经历一个握手协议发现连接的另一边使用的SSL安全等级。如果这个安全等级和指定的特征不兼容,这个流对象产生一个错误事件。如果你请求了一个协商的安全等级(NSStreamSocketSecurityLevelNegotiatedSSL),这个安全等级会变成两边都可以实现的安全等级的最高级。然而,如果你设置了一个SSL安全等级,而远程主机根本没有security,错误发生了。
为了给一个连接配置SOCKS代理服务,你需要构造一个字典,以NSStreamSOCKSProxyNameKey的形式(例如,NSStreamSOCKSProxyHostKey)。然后使用setPropery:forKey:设置这个字典作为NSStreamSOCKSProxyConfigurationKey的值。
发动一个HTTP请求
如果你打开了一个HTTP服务端连接(一个网站),那么你可能需要通过发送一个HTTP请求来发起一个交易。发起这个请求的一个好时机是当NSOutputStream对象接收到携带有NSStreamEventHasSapceAvailable事件的stream:handleEvent:消息。下面的代码演示了代理生成一个HTTP GET请求,然后写到输出流,接着立即关闭这个流对象。
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
NSLog(@"stream:handleEvent: is invoked...");
switch(eventCode) {
case NSStreamEventHasSpaceAvailable:
{
if (stream == oStream) {
NSString * str = [NSString stringWithFormat:
@"GET / HTTP/1.0\r\n\r\n"];
const uint8_t * rawstring =
(const uint8_t *)[str UTF8String];
[oStream write:rawstring maxLength:strlen(rawstring)];
[oStream close];
}
break;
}
// continued ...
}
}
对于更多的信息
要了解更多使用流来进行网络通信,请阅读Networking Overview