Search code examples
objective-cxcodeparsingnsstreamnsinputstream

How to reliably retrieve NSData objects from NSInputStream in XCode


So my application works along these lines:

  1. An iPod continuously sends NSDictionaries that contain: an image encoded in JPEG and some image properties as NSStrings.
  2. The NSDictionary is encoded using NSPropertyListSerialization with the format BinaryFormat_v1_0 and sent in packets of 1024 bytes via NSStream to the central computer running an app on OSX.
  3. The OSX app receives the data packets, continuously appending to a single NSMutableData object, until it sees the first packet of the next NSData object (which in binary format I've found starts as 'bplist').
  4. The NSData is converted back to an NSDictionary to be used by the OSX app, by calling NSPropertyListSerialization.
  5. Once the NSData was successfully converted (or not),the NSData object is set back to zero to start reading the next round of packets.

A few more notes: both the NSInputStream and NSOutput streams are running on their respective device's currentRunLoop in NSDefaultRunLoopMode.

When running this process, sometimes the conversion back to NSDictionary works fine with no errors (about 1/3 of the attempts), but the other times the conversion returns this error:

Error: Failed to convert NSData to NSDict : Error Domain=NSCocoaErrorDomain Code=3840 "Unexpected character b at line 1" UserInfo={NSDebugDescription=Unexpected character b at line 1, kCFPropertyListOldStyleParsingError=Error Domain=NSCocoaErrorDomain Code=3840 "Conversion of string failed." UserInfo={NSDebugDescription=Conversion of string failed.}}

Following are the parts of the program that parse the data from the stream:

... method to handle stream events:

-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
    case NSStreamEventHasBytesAvailable: {

        uint8_t buf[1024];
        unsigned int len = (unsigned)[(NSInputStream *)aStream read:buf maxLength:1024];

        if(len) {
            [self handleEventBuffer:buf WithLength:len];
        }
...

... and the method that takes care of the data:

-(void)handleEventBuffer:(uint8_t*)buf WithLength:(unsigned int)len {
...
NSString *bufStr = [NSString stringWithFormat:@"%s",(const char*)buf];
        if ([bufStr containsString:@"bplist00"] && [self.cameraData length] > 0) {
            // Detected new file, enter in all the old data and reset for new data
            NSError *error;
            NSDictionary *tempDict = [[NSDictionary alloc] init];

            tempDict = [NSPropertyListSerialization propertyListWithData:self.cameraData
                                                             options:0
                                                              format:NULL
                                                               error:&error];

            if (error != nil) {
                // Expected good file but no good file, erase and restart
                NSLog(@"Error: Failed to convert NSData to NSDict : %@", [error description]);
                [self.cameraData setLength:0];
            } 
...
            [self.cameraData setLength:0];
            [self.cameraData appendBytes:buf length:len];

        } else {
            // Still recieving data
            [self.cameraData appendBytes:buf length:len];
        }

So, the question that I'm getting at is:

  • How can I fix my parsing method to give me reliable results that don't randomly fail to convert?
  • OR is there a better way than this to parse buffer streams for this purpose?
  • OR am I just doing something stupid or missing something obvious?

Solution

  • You appear to be relying on each write to the stream resulting in a matching read of the same size, do you know this is guaranteed by NSStream? If not then any read could contain parts of two (or more) of your encoded dictionaries, and you would get the parsing errors you see.

    Alternative approach:

    For each encoded dictionary to send:

    Write end:

    1. Send a message containing the size in bytes of the encoded dictionary that will follow.
    2. Write the encoded dictionary in chunks, the last chunk may be short
    3. Repeat

    Read end:

    1. Read the size message specifying its exact length in bytes.
    2. Read the encoded dictionary in chunks, making sure you read only the number of bytes reported by (1).
    3. Repeat.

    Provided you are using a reliable communication stream this should enable you to read each encoded dictionary reliably. It avoids you trying to figure out where the boundary between each encoded dictionary is, as that information is part of your protocol.

    HTH