Search code examples
objective-ctcpnsstream

Parsing data after sending between iOS devices over NSStream


I have an application set up to send data between two iOS devices using NSStreams over a TCP connection.

The data sent consists of two parts:

  1. An integer indicating the size of the message object to come
  2. The message object, some NSStrings and an NSData object encoded with NSKeyedArchiver

The issue:

  1. When the NSData object is around 1.5Mb, I get an incomprehensible archive exception when I try to decode it. When reading the 4 bytes following where the message should be, a large number is there, not the size of the next message.
  2. When the NSData object is around 80Kb, two messages are successfully decoded and then I get the incomprehensible archive exception.

It seems like the data is getting out of order at some point... though the whole purpose of TCP is to keep it in order. So, I must be the problem!

Relevant Code

Server

sendData: is passed the Message object that has been encoded with an NSKeyedArchiver. It is called for 100ish messages in a short period of time

// dataQueue is an NSMutableArray
- (void) sendData:(NSData *)data
{
  int size = data.length;
  NSData *msgSize = [NSData dataWithBytes:&size length:sizeof(int)];
  
  if (self.outputStream.hasSpaceAvailable && (self.dataQueue.count == 0)) {
    [self.dataQueue addObject:data];
    [self.outputStream write:msgSize.bytes maxLength:msgSize.length];
  }
  else {
    [self.dataQueue addObject:msgSize];
    [self.dataQueue addObject:data];
  }
}

//called by NSStreamDelegate method when space is available
- (void) hasSpaceAvailable
{
  if (self.dataQueue.count > 0) {
    NSData *tmp = [self.dataQueue objectAtIndex:0];
    [self.outputStream write:tmp.bytes maxLength:tmp.length];
    [self.dataQueue removeObjectAtIndex:0];
  }
}

Client

streamHasBytes: collects the message fragments and appends them to self.buffer. When the length of self.buffer becomes greater than the message length indicated in the first 4 bytes of self.buffer, the Message object is parsed...

//Called by NSStreamDelegate method when bytes are available
- (void) streamHasBytes:(NSInputStream *)stream
{
  NSInteger       bytesRead;
  uint8_t         buffer[32768];
  
  bytesRead= [stream read:buffer maxLength:sizeof(buffer)];
  
  if (bytesRead == -1 || bytesRead == 0) //...err
  
  @synchronized(self) { //added to test concurrency
  [self.buffer appendBytes:buffer length:bytesRead];
  }
  [self checkForMessage];
}

- (void) checkForMessage
{
  @synchronized(self) { //added to test concurrency
  int msgLength = *(const int *)self.buffer.bytes;
        
  if (self.buffer.length < msgLength) return;
  
  //remove the integer from self.buffer
  [self.buffer replaceBytesInRange:NSMakeRange(0, sizeof(int)) withBytes:NULL length:0]; 
  
  //copy the actual message from self.buffer
  NSData *msgData = [NSData dataWithBytes:self.buffer.bytes length:msgLength];
        
  //remove the message from self.buffer
  [self.buffer replaceBytesInRange:NSMakeRange(0, msgLength) withBytes:NULL length:0];
                                  
  Message *theMsg = [NSKeyedUnarchiver unarchiveObjectWithData:msgData];
  [self.delegate didReceiveMessage:theMsg];
  }
}

EDIT:

I'm now noticing that, in the case where the NSData object in the first Message is around 1.5Mb, for a total Message size of about 1.6Mb, only about 1.3Mb are received by the client... This would explain the incomprehensible archive errors. Why would all of the data not be delivered?


Solution

  • It turns out that in some cases only a fraction of the data that I assumed was sending was actually sending. NSOutputStream's write:maxLength: method returns the number of bytes that were actually written to the stream. So the hasSpaceAvailable method above can be fixed with

    NSInteger i = [self.outputStream write:tmp.bytes maxLength:tmp.length];
    if (i < tmp.length) {
        //send the difference
    }