Writing To Output Streams

Using an NSOutputStream instance to write to an output stream requires several steps:

  1. Create and initialize an instance of NSOutputStream with a repository for the written data. Also set a delegate.

  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. If the stream object has written data to memory, obtain the data by requesting the NSStreamDataWrittenToMemoryStreamKey property.

  5. When there is no more data to write, 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 NSOutputStream object you must specify a destination for the data written to the stream. The destination for an output-stream object can be a file, a C buffer, application memory, or a network socket.

The initializers and factory methods for NSOutputStream allow you to create and initialize the instance with a file, a buffer, or memory. Listing 1 shows the creation of an NSOutputStream instance that will write data to application memory.

Listing 1  Creating and initializing an NSOutputStream object for 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];
}

As the code in Listing 1 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 NSOutputStream object when that object has stream-related events to report, such as when the stream has space for bytes.

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 the stream is unable to accept more bytes. 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 NSOutputStream instance an open message to start the streaming of data to the output container.

Handling Stream Events

After a stream object is sent open, you can find out about its status, whether it has space for writing data, and the nature of any error with the following messages:

The returned status is an NSStreamStatus constant indicating that the stream is opening, writing, 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 (as long as the delegate continues to put bytes on the stream) until it encounters the end of the stream. These messages include a parameter with an NSStreamEvent constant that indicates the type of event. For NSOutputStream objects, the most common types of events are NSStreamEventOpenCompleted, NSStreamEventHasSpaceAvailable, and NSStreamEventEndEncountered. The delegate is typically most interested in NSStreamEventHasSpaceAvailable events. Listing 2 illustrates one approach you could take to handle this type of event.

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

In this implementation of stream:handleEvent: the delegate uses a switch statement to identify the passed-in NSStreamEvent constant. If the constant is NSStreamEventHasSpaceAvailable, the delegate gets the bytes held by a NSMutableData object (_data) and advances the pointer for the current write operation. It next determines the byte capacity of the impending write operation (1024 or the remaining bytes to write), declares a buffer of that size, and copies that amount of data to the buffer. Next the delegate invokes the output-stream object’s write:maxLength: method to put the buffer’s contents onto the output stream. Finally it advances the index used to advance the readBytes pointer for the next operation.

If the delegate receives an NSStreamEventHasSpaceAvailable event and does not write anything to the stream, it does not receive further space-available events from the run loop until the NSOutputStream object receives more bytes. When this happens, the run loop is restarted for space-available events. If this scenario is likely in your implementation, you can have the delegate set a flag when it doesn’t write to the stream upon receiving an NSStreamEventHasSpaceAvailable event. Later, when your program has more bytes to write, it can check this flag and, if set, write to the output-stream instance directly.

There is no firm guideline on how many bytes to write at one time. Although it may be possible to write all the data to the stream in one event, this depends on external factors, such as the behavior of the kernel and 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 NSOutputStream object experiences errors writing to 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 NSOutputStream object concludes writing data to an output stream, it sends the delegate a NSStreamEventEndEncountered event in a stream:handleEvent: message. At this point the delegate should dispose of the stream 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. Furthermore, if the destination for the NSOutputStream object is application memory (that is, you created the instance using initToMemory or the factory method outputStreamToMemory), you might now want to retrieve the data held in memory. Listing 3 illustrates how you might do all of these things.

Listing 3  Closing and releasing the NSInputStream 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 ...
    }
}

You get the stream data written to memory by sending the NSOutputStream object a propertyForKey: message, specifying a key of NSStreamDataWrittenToMemoryStreamKey The stream object returns the data in an NSData object.