Reading From Input Streams

In Cocoa, reading from an NSInputStream instance consists of several steps:

  1. Create and initialize an instance of NSInputStream from a source of data.

  2. Schedule the stream object on a run loop and open the stream.

  3. Handle the events that the stream object reports to its delegate.

  4. When there is no more data to read, dispose of the stream object.

The following discussion goes into each of these steps in more detail.

Preparing the Stream Object

To begin using an NSInputStream object you must have (after first locating, if necessary) a source of data for the stream. The source of data can be a file, an NSData object, or a network socket.

The initializers and factory methods for NSInputStream allow you to create and initialize the instance from an NSData or file. Listing 1 shows an NSInputStream instance created from a file.

Listing 1  Creating and initializing an NSInputStream object

- (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];
}

As this example shows, after you create the object you should set the delegate (more often than not to self). The delegate receives stream:handleEvent: messages from the NSInputStream object when that object is scheduled on the run loop and has stream-related events to report, such as when there are bytes on the stream to be read.

Before you open the stream to begin the streaming of data, send a scheduleInRunLoop:forMode: message to the stream object to schedule it to receive stream events on a run loop. By doing this, you are helping the delegate to avoid blocking when there is no data on the stream to read. If streaming is taking place on another thread, be sure to schedule the stream object on that thread’s run loop. You should never attempt to access a scheduled stream from a thread different than the one owning the stream’s run loop. Finally, send the NSInputStream instance an open message to start the streaming of data from the input source.

Handling Stream Events

After a stream object is sent open, you can find out about its status, whether it has bytes available to read, and the nature of any error with the following messages:

The returned status is an NSStreamStatus constant indicating that the stream is opening, reading, at the end of the stream, and so on. The returned error is an NSError object encapsulating information about any error that took place. (See the reference documentation for NSStream for descriptions of NSStreamStatus and other stream types.)

More importantly, once the stream object has been opened, it keeps sending stream:handleEvent: messages to its delegate until it encounters the end of the stream. These messages include a parameter with an NSStreamEvent constant that indicates the type of event. For NSInputStream objects, the most common types of events are NSStreamEventOpenCompleted, NSStreamEventHasBytesAvailable, and NSStreamEventEndEncountered. The delegate is typically most interested in NSStreamEventHasBytesAvailable events. Listing 2 illustrates a good approach for handling this type of event.

Listing 2  Handling a bytes-available event

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
    switch(eventCode) {
        case NSStreamEventHasBytesAvailable:
        {
            if(!_data) {
                _data = [[NSMutableData data] retain];
            }
            uint8_t buf[1024];
            NSInteger 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

In this implementation of stream:handleEvent: the delegate uses a switch statement to identify the passed-in NSStreamEvent constant. If the constant is NSStreamEventHasBytesAvailable, the delegate first lazily creates (if necessary) an NSMutableData object (_data) to hold the retrieved bytes. Then it declares a buffer of a certain size (1024 bytes, in this case) and invokes the stream object’s read:maxLength: method, which fills up the buffer with the specified number of bytes. If the read operation successfully fetched bytes from the stream, the delegate appends these bytes to the NSMutableData object.

There is no firm guideline on how many bytes to read at one time. Although it may be possible to read all the data in the stream in one event, this depends on the length of the stream (that is, the number of bytes in it) as well as the behavior of the kernel, including device and socket characteristics. The best approach is to use some reasonable buffer size, such as 512 bytes, one kilobyte (as in the example above), or a page size (four kilobytes).

When the NSInputStream object experiences errors processing the stream, it stops streaming and notifies its delegate with a NSStreamEventErrorOccurred. The delegate should handle the error in its stream:handleEvent: method as described in Handling Stream Errors.

Disposing of the Stream Object

When an NSInputStream object reaches the end of a stream, it sends the delegate a NSStreamEventEndEncountered event in a stream:handleEvent: message. The delegate should dispose of the object by doing the mirror-opposite of what it did to prepare the object. In other words, it should first close the stream object, remove it from the run loop, and finally release it. Listing 3 gives an example of how you might do this.

Listing 3  Closing and releasing the NSInputStream object

- (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 ...
    }
}