RyanM RyanM - 6 months ago 103
Objective-C Question

Parsing data after sending between iOS devices over NSStream

I have an application set up to send data between two iOS devices using

NSStream
s 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?

Answer

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
}