原文鏈接:NSStream
流是位數據通過通信路徑的連續傳送序列。它是單向的,從一個應用程序的角度,流可以是輸入流(讀操作流)或者輸出流(寫操作流),除了基於文件的流之外,其餘的都是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
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
使用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對象中。
=====================================================================================
原文鏈接:iOS網絡編程之NSStream
首先來回顧下。在前文《[深入淺出Cocoa]iOS網絡編程之Socket》中,提到iOS網絡編程層次模型分爲三層:
- Cocoa層:NSURL,Bonjour,Game Kit,WebKit
- Core Foundation層:基於 C 的 CFNetwork 和 CFNetServices
- OS層:基於 C 的 BSD socket
前文《iOS網絡編程之Socket》 和《iOS網絡編程之CFNetwork》 講了最底層的 socket 和Core Foundation層的 CFNetwork,本文將介紹位於 Cocoa 中的 NSStream。NSStream 其實只是用 Objective-C 對 CFNetwork 的簡單封裝,它使用名爲 NSStreamDelegate 的協議來實現 CFNetwork 中的回調函數的作用,同樣,runloop 也與 NSStream 結合的很好。NSStream 有兩個實體類:NSInputStream 和 NSOutputStream,分別對應 CFNetwork 中的 CFReadStream 和 CFWriteStream。
本文示例代碼請查看:
https://github.com/kesalin/iOSSnippet/tree/master/KSNetworkDemo
二,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 編程。