流是位数据通过通信路径的连续传送序列。它是单向的,从一个应用程序的角度,流可以是输入流(读操作流)或者输出流(写操作流),除了基于文件的流之外,其余的都是non-seekable的。一旦流数据被提供或者被使用,数据就不能够从流中获取到。
Cocoa包括三种与流有关的类:NSStream,NSInputStream,NSOutputStream. NSStream是抽象类,它定义了流对象的基本接口和属性。NSInputStream和NSOutputStream是NSStream的子类,它们实现了输入流和输出流的基本操作。你可以为存储在内存中,向文件或者C buffer写的流数据创建NSOutputStream对象;可以为从NSData对象和文件中读取的流数据创建NSInputStream对象;也可以在网络套接字的两端创建NSInputStream和NSOutputStream对象,通过流对象,你可以不用一次性将所有的流数据加载到内存中。下图是就输入流和输出流对象的源和目的地为依据对输入流和输出流的分类:
NSStream及其子类进行的是比较底层的开发,对于某些特殊的需求如果有顶层的Cocoa API更加适合的话(比如NSURL,NSFileHandle),那么就用顶层的API进行编程。
流对象有许多属性,大多数属性都和网络安全及其配置有关,也就是SSL和SOCKS代理信息。另外有两个重要的属性,一个是NSStreamDataWrittenToMemoryStreamKey,对于一个输出流它可以用来获取到写入内存中的数据。另一个是NSStreamFileCurrentOffsetKey,对于一个基于文件的流,可以用它操作读或者写的位置。
每个流对象都有一个与其相关联的delegate,如果其delegate没有显示的设置,那么这个流对象自身成为其delegate(对于自定义子类的话这是一个很有用的约定)。流对象调用它唯一的delegate方法stream:handleEvent:来处理所有与stream-related事件。对于传入参数中的events事件,它指示了什么时候输入流中有数据可供读入,什么时候输出流中有空间可供数据写入。对于这两个事件中的NSStreamEventHasBytesAvailable事件,delegate向该stream发送read:maxLength:消息从流中读取数据,对于NSStreamHasSpaceAvailable事件,delegate向该stream发送write:maxlength:向流中写入数据。
NSStream是建立在Core Foundation的CFStream层之上的。这层紧密的关系意味着NSStream的具体子类-NSInputStream和NSOutputStream与Core Foundation中的CFReadStream和CFWriteStream是一一对应的。尽管Cocoa和Core Foundation的stream APIs有很大的相似性,但是它们的实现却不尽相同,Cocoa stream类使用delegate模式来实现异步操作(比如将其布置在run loop之上),而Core Foundation使用客户端的回调。Core Foundation的stream类型设置的是client(在Core Foundation中叫做context),NSStream中设置的delegate,这是两个不同的概念,不应该把设置delegate和设置context搞混淆。
相比CFStream而言,NSStream有更强的可扩展性,你可以生成NSStream,NSInputStream,NSOutputStream的子类来自定义其属性和方法。For example, you could create an input stream that maintains statistics on the bytes it reads; or you could make a NSStream
subclass whose instances can seek through their stream, putting back bytes that have been read. NSStream
has its own set of required overrides, as do NSInputStream
and NSOutputStream
.
NSInputStream
原文:Reading From Input Streams
ios cocoa 编程,从NSInputStream中读入数据包括几个步骤:
1.从数据源创建和初始化一个NSInputStream实例
2.将输入流对象配置到一个run loop,open the stream
3. 通过流对象的delegate函数处理事件
4. 当所有数据读完,进行流对象的内存处理
一,使用流对象的准备工作
在使用NSInputStream对象之前你必须有流的数据源,数据源的类型可以是文件,NSData对象,或者一个网络套接字。
NSInputStream的初始化函数和工厂方法可以从NSData和文件创建和初始化一个NSInputStream的实例。下面的例子是从文件创建一个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];
- }
上面的例子显示,当你创建对象之后你应该设置其delegate。当把NSInputStream对象配置到一个run loop,并且有与流相关的事件(例如流中有可读数据)发生时,该对象会收到stream:handleEvent:消息。
在你open stream之前,给流对象发送一个scheduleInRunLoop:forMode:消息,来将该对象配置到一个run loop接收stream events。这样,当流中没有数据可读时可以避免delegate阻塞。如果流是发生在另一个线程,你需要确认该流对象是配置在那个线程的run loop中。你不应该尝试从一个除了包含该流对象的run loop的线程的其他线程中对流进行操作。最后,对NSInputStream对象发送open消息开始对输入数据的流操作。
二,处理Stream Events
当你对一个流对象发送open消息之后,你可以查找到它的当前状态。通过下面的消息可以知道流对象中是否有数据可读,以及任何错误的属性:
-
streamStatus
-
hasBytesAvailable
-
streamError
返回的状态是一个NSStreamStatus常量,它可以指示流对象是处于opening,reading,或者at the end of the stream等等。返回的错误是一个NSError对象,它封装了可能发生的所有错误信息。
重要的是,一旦 open 流对象,流对象会一直向其delegate发送stream:handleEvent:
消息直到到达了流对象的末尾。这些消息的参数中包含一个指示流事件类型的NSStreamEvent常量。对NSInputStream对象而言,最常用的事件类型是NSStreamEventOpenCompleted,NSStreamEventHasBytesAvailable,NSStreamEventEndEncountered。我们尤其感兴趣的应该是NSStreamEventHasBytesAvailable事件。下面的例子就是一个处理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;
- }
- // continued
- }
stream:handleEvent: 函数使用switch语句来判别NSStreamEvent常量,当这个常量是MSStreamEventHasBytesAvailable的时候,delegate函数会lazy create 一个NSMutableData对象_data来接收读取的数据。然后声明一个大小为1024的uint8_t类型数组buf,调用read:maxLength:函数从stream中读取指定大小的数据到buf中,如果读取成功,delegate将会将读取到的数据添加到NSMutableData对象_data中,并且更新总的读取到的数据bytesRead.
至于一次从stream中读取多大的数据,一般来说,使用一些常用的数据大小规格,比如说512Bytes,1kB,4kB(一个页面大小)。
三,处理stream object
当NSInputStream对象到达steam的末尾的时候,它会向stream:handleEvent:函数发送一个NSStreamEventEndEncountered事件类型常量,delegate函数应该做出与准备使用流对象相反的操作,也就是说,需要关闭流对象,从run loop中移除,最终释放流对象。如下面的代码所示:
- - (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
译自:Writing To Output Streams
使用NSOutputStream实例需要以下几个步骤:
1,使用存储写入数据的存储库创建和初始化一个NSOutputSteam实例,并且设置它的delegate。
2,将这个流对象布置在一个runloop上并且open the stream。
3,处理流对象向其delegate发送的事件消息。
4,如果流对象向内存中写入了数据,那么可以通过使用NSStreamDataWrittenToMemoryStreamKey属性获取数据。
5,当没有数据可供写入时,清理流对象。
一,使用流对象的准备工作
使用NSOutputStream对象之前你必须指定数据写入的流的目标位置,输出流对象的目标位置可以是file,C buffer, application memory,network socket。
NSOutputStream的初始化方法和工厂方法可以使用a file,a buffer, memory来创建和初始化实例,下面的代码初始化了一个NSOutputStream实例,用来向 application memory 写入数据。
- - (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];
- }
上面的代码显示,在你初始化一个NSOutputStream对象之后应该设置它的delegate(通常是self),当流对象有 有空间可供数据写入 之类的与流有关的事件消息发送时,delegate会收到从NSOutputStream对象发送来的消息。
当你在open the stream对象之前,向流对象发送scheduleInRunLoop:forMode:消息使其在一个runloop上可以接收到stream events,这样,当流对象不能接收更多数据的时候,可以使delegate避免阻塞。当streaming发生在另外一个线程时,你必须将流对象布置在那个线程的run loop上,You should never attempt to access a scheduled stream from a thread different than the one owning the stream’s run loop. 最后 open the stream 开始数据向 NSOutputStream对象传送。
二,处理 Stream Events
当你向流对象发送open消息之后,你可以通过以下消息获取到流对象的状态,比如说当前是否有空间可供数据写入以及其他错误信息的属性。
- streamStatus
- hasSpaceAvailable
- streamError
返回的状态是NSStreamStatus常量,它指示流当前的状态是opening,writing,at the end of the stream等等,返回的错误是NSError对象,它封装的是所有错误的信息。
重要的是,一旦open the stream,只要delegate持续想流对象写入数据,流对象就是一直向其delegate发送stream:handleEvent:消息,直到到达了流的末尾。这些消息中包含一个NSStreamEvent常量参数来指示事件的类型。对于一个NSOutputStream对象,最常见的事件类型是NSStreamEventOpenCompleted,NSStreamEventHasSpaceAvailable,NSStreamEventEndEncountered,delegate通常对NSStreamEventHasSpaceAvaliable事件最感兴趣。下面的代码就是处理NSStreamEventHasSpaceAvaliable事件的一种方法:
- - (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常量,当这个常量是NSStreamEventHasSpacesAvailable的时候,delegate从NSMutableData对象_data中获取数据,并且将其指针转化为适合当前操作的类型u_int8.下一步计算即将进行写操作的字节数(是1024还是所有剩余的字节数),声明一段相应大小的buffer,向该buffer写入相应大小的数据,然后delegate调用流对象write:maxLength:方法将buffer中的数据置入output stream中,最后更新byteIndex用于下一次的读取操作。
如果delegate收到NSStreamEventHasSpacesAvailable事件消息但是没有向stream里写入任何数据,它不会从runloop再接收到space-available的事件消息直到NSOutputStream对象接收到数据,这样由于space-available事件该run loop会重新启动。如果这种情况很有可能在你的程序设计中出现,在收到NSStreamEventHasSpaceAvailable消息并且没有向该stream中写入数据时可以在delegate中设置一个标志位flag,之后,当存在更多的数据需要写入时,先检查该标志位,如果该标志位被设置,那么直接向output-stream实例写入数据。
对于一次向output-stream实例中写入多少数据没有严格的限定,一般情况下使用一些合理值,如512Bytes,1kB,4kB(一个页面大小)。
当在向stream中写数据时NSOutputStream对象发生错误,它会停止streaming并且使用NSStreamEventErrorOccurred消息通知其delegate。
三,清理 Stream Object
当一个NSOutputStream对象结束向一个output stream写入数据,它通过stream:handleEvent:消息向delegate发送NSStreamEventEndEncountered事件消息,这个时候delegate应该清理 stream object,先关闭该stream object,从run loop中移除,释放该stream object。此外,如果NSOutputStream对象的目的存储库是application memory(也就是,你通过initToMemory方法或者其工厂方法outputStreamToMemory创建的该对象),现在就可以从内存中获取数据了。下面的代码实现的清理 stream object的工作:
- - (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:消息获取从流向内存中写入的数据,设定key的值为NSStreamDataWrittenToMemoryStreamKey,该stream object将数据返回到一个NSData对象中。
二 :首先来回顾下。在前文《[深入浅出Cocoa]iOS网络编程之Socket》中,提到iOS网络编程层次模型分为三层:
- Cocoa层:NSURL,Bonjour,Game Kit,WebKit
- Core Foundation层:基于 C 的 CFNetwork 和 CFNetServices
- OS层:基于 C 的 BSD socket
二,NSStream 类接口简介
NSStream 类有如下接口:
- (void)open;
- (void)close;
- (id <NSStreamDelegate>)delegate;
- (void)setDelegate:(id <NSStreamDelegate>)delegate;
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (NSStreamStatus)streamStatus;
- (NSError *)streamError;
NSStream 的一些接口与 CFNetwork 类似,如打开,关闭,获取状态和错误信息,以及和 runloop 结合等在这里就不再重复了。前面提到 NSStream 是通过 NSStreamDelegate 来实现 CFNetwork 中的回调函数,这个可选的协议只有一个接口:
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
NSStreamEvent 是一个流事件枚举:
typedef NS_OPTIONS(NSUInteger, NSStreamEvent) {
NSStreamEventNone = 0,
NSStreamEventOpenCompleted = 1UL << 0,
NSStreamEventHasBytesAvailable = 1UL << 1,
NSStreamEventHasSpaceAvailable = 1UL << 2,
NSStreamEventErrorOccurred = 1UL << 3,
NSStreamEventEndEncountered = 1UL << 4
};
这些事件枚举的含义也和 CFNetwork 中的 CFStreamEventType 类似,在此也就不再重复了。
NSInputStream 类有如下接口:
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len;
从流中读取数据到 buffer 中,buffer 的长度不应少于 len,该接口返回实际读取的数据长度(该长度最大为 len)。
- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len;
获取当前流中的数据以及大小,注意 buffer 只在下一个流操作之前有效。
- (BOOL)hasBytesAvailable;
检查流中是否还有数据。
NSOutputStream 类有如下接口:
- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;
将 buffer 中的数据写入流中,返回实际写入的字节数。
- (BOOL)hasSpaceAvailable;
检查流中是否还有可供写入的空间。
从这些接口可以看出,NSStream 真的就是 CFNetwork 上的一层简单的 Objective-C 封装。但 iOS 中的 NSStream 不支持 NShost,这是一个缺陷,苹果也意识到这问题了(http://developer.apple.com/library/ios/#qa/qa1652/_index.html),我们可以通过 NSStream 的扩展函数来实现该功能:
@implementation NSStream(StreamsToHost)
+ (void)getStreamsToHostNamed:(NSString *)hostName
port:(NSInteger)port
inputStream:(out NSInputStream **)inputStreamPtr
outputStream:(out NSOutputStream **)outputStreamPtr
{
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
assert(hostName != nil);
assert( (port > 0) && (port < 65536) );
assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) );
readStream = NULL;
writeStream = NULL;
CFStreamCreatePairWithSocketToHost(
NULL,
(__bridge CFStringRef) hostName,
port,
((inputStreamPtr != NULL) ? &readStream : NULL),
((outputStreamPtr != NULL) ? &writeStream : NULL)
);
if (inputStreamPtr != NULL) {
*inputStreamPtr = CFBridgingRelease(readStream);
}
if (outputStreamPtr != NULL) {
*outputStreamPtr = CFBridgingRelease(writeStream);
}
}
@end
三,客户端示例代码
与前面的示例类似,在这里我只演示客户端示例。同样,我们也在一个后台线程中启动网络操作:
NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", serverHost, serverPort]];
NSThread * backgroundThread = [[NSThread alloc] initWithTarget:self
selector:@selector(loadDataFromServerWithURL:)
object:url];
[backgroundThread start];
然后在 loadDataFromServerWithURL 中创建 NSInputStream,并设置其 delegate,将其加入到 run-loop 的事件源中,然后打开流,运行 runloop:
- (void)loadDataFromServerWithURL:(NSURL *)url
{
NSInputStream * readStream;
[NSStream getStreamsToHostNamed:[url host]
port:[[url port] integerValue]
inputStream:&readStream
outputStream:NULL];
[readStream setDelegate:self];
[readStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[readStream open];
[[NSRunLoop currentRunLoop] run];
}
因为我们将 KSNSStreamViewController 当作 NSInputStream 的 delegate,因此要在 KSNSStreamViewController 中实现该 delgate:
#pragma mark NSStreamDelegate
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
NSLog(@" >> NSStreamDelegate in Thread %@", [NSThread currentThread]);
switch (eventCode) {
case NSStreamEventHasBytesAvailable: {
if (_receivedData == nil) {
_receivedData = [[NSMutableData alloc] init];
}
uint8_t buf[kBufferSize];
int numBytesRead = [(NSInputStream *)stream read:buf maxLength:kBufferSize];
if (numBytesRead > 0) {
[self didReceiveData:[NSData dataWithBytes:buf length:numBytesRead]];
} else if (numBytesRead == 0) {
NSLog(@" >> End of stream reached");
} else {
NSLog(@" >> Read error occurred");
}
break;
}
case NSStreamEventErrorOccurred: {
NSError * error = [stream streamError];
NSString * errorInfo = [NSString stringWithFormat:@"Failed while reading stream; error '%@' (code %d)", error.localizedDescription, error.code];
[self cleanUpStream:stream];
[self networkFailedWithErrorMessage:errorInfo];
}
case NSStreamEventEndEncountered: {
[self cleanUpStream:stream];
[self didFinishReceivingData];
break;
}
default:
break;
}
}
当数据读取完毕或者读取失败时,调用 cleanUpStream 方法来关闭流:
- (void)cleanUpStream:(NSStream *)stream
{
[stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[stream close];
stream = nil;
}
四,结语
通过上面的示例演示,我们可以看到 NSStream 只是用 Objective-C 对 CFNetwork 的一层简单封装,但确实大大方便了我们使用 socket 进行编程,因此在大多数情况下,我们都应该优先使用 NSStream 进行 socket 编程。