0

I have this sample code taken from the example of this iPhone chat server.

In my case I need to send larger amounts of data, 100k-200k thus I changed the buffer size to something that can accommodate what I want.

On the iOS simulator (emulating a 6plus) everything works perfectly, as soon as I try to debug on my iPhone 6plus after implementing the suggestions of @tc. I get the incoming message broken into 3-4 parts! Any ideas?

2015-02-03 22:42:34.756 Sock1[7390:3773054] Incoming message: XML part 1
2015-02-03 22:42:34.759 Sock1[7390:3773054] Incoming message: XML part 2
2015-02-03 22:42:34.774 Sock1[7390:3773054] Incoming message: XML part 3
2015-02-03 22:42:34.794 Sock1[7390:3773054] Incoming message: XML part 4

XML part 1-4 make up the entire message that should have been in one as there is no null byte character within.

Just to make things even stranger, when I add a break point on this line:

[currentMessage appendBytes:buffer length:len]; 

and go through step by step (69-70 pressing Continue) everything works fine! No errors at all! So it is something to do with the speed of parsing or receiving or something I can't figure out?

What I've made sure is that the server doesn't send any null byte characters apart from the terminating one at the end of the message.

The code I use is this:

@implementation ViewController

bool connectionError = true;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)initNetworkCommunication {
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)@"192.168.1.1", 6035, &readStream, &writeStream);

    inputStream = (__bridge NSInputStream *)readStream;
    outputStream = (__bridge NSOutputStream *)writeStream;

    [inputStream setDelegate:self];
    [outputStream setDelegate:self];

    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [inputStream open];
    [outputStream open];

}

- (void)closeAll {
    NSLog(@"Closing streams.");

    [inputStream close];
    [outputStream close];

    [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [inputStream setDelegate:nil];
    [outputStream setDelegate:nil];

    inputStream = nil;
    outputStream = nil;
}

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {

    switch (streamEvent) {

    case NSStreamEventOpenCompleted:
        NSLog(@"Stream opened");
        break;

    case NSStreamEventHasBytesAvailable:

        connectionError = false;
        if (theStream == inputStream) {

        uint8_t buffer[1024];
        long len;

        NSMutableData *currentMessage;
        currentMessage = [NSMutableData dataWithBytes:"" length:strlen("")];

        while ([inputStream hasBytesAvailable]) {
            len = [inputStream read:buffer maxLength:sizeof(buffer)];
            [currentMessage appendBytes:buffer length:len];
        }

        NSString *newMessage = [[NSString alloc] initWithData:currentMessage encoding:NSUTF8StringEncoding];
        NSLog(@"Incoming message: %@",newMessage);

        NSData *nullByte = [NSMutableData dataWithLength:1];
        len = currentMessage.length;
        NSRange searchRange = {0, len};
        for (NSRange r; (r = [currentMessage rangeOfData:nullByte options:0 range:searchRange]).length; ) {
            NSString *message = [[NSString alloc] initWithBytes:currentMessage.bytes length:r.location encoding:NSUTF8StringEncoding];
            searchRange.location = r.location+r.length;
            searchRange.length = len - searchRange.location;

            [self messageReceived:message];
        }
        [currentMessage replaceBytesInRange:(NSRange){0, searchRange.location} withBytes:NULL length:0];
        }
        break;

    case NSStreamEventErrorOccurred:
        NSLog(@"Can not connect to the host!");
        connectionError = true;

        [self closeAll];

        [self connectionLost];

        break;


    case NSStreamEventEndEncountered:
        break;

    default:
        NSLog(@"Unknown event");
    }

}

- (void) connectionLost {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert!"
                            message:@"Connection to the server lost!"
                           delegate:nil
                      cancelButtonTitle:@"OK"
                      otherButtonTitles:nil];

    [alert show];

}

- (void) messageReceived:(NSString *)message {

    [messages addObject:message];

    if (inputStream.streamStatus == NSStreamStatusClosed || inputStream.streamStatus == NSStreamStatusError || inputStream.streamStatus == NSStreamStatusNotOpen) {
    [self closeAll];
    [self initNetworkCommunication];
    }

    // do things with the message, XML parsing...    
}

- (void) initConnection {

    [self initNetworkCommunication];

    messages = [[NSMutableArray alloc] init];

}

- (IBAction)joinChat:(id)sender {

    [self initConnection];

    [self sendSocketMessage: @"iam:" message: _inputNameField.text];

}


- (void) sendSocketMessage:(NSString*) sendCommand message:(NSString*) sendMessage
{
    // do something...

    if (outputStream.streamStatus == NSStreamStatusClosed || outputStream.streamStatus == NSStreamStatusError || outputStream.streamStatus == NSStreamStatusNotOpen) {
    [self closeAll];
    [self initNetworkCommunication];
    }    

    NSString *response  = [NSString stringWithFormat: @"%@%@", sendCommand, sendMessage];
    NSData *data = [[NSData alloc] initWithData:[response dataUsingEncoding:NSASCIIStringEncoding]];
    [outputStream write:[data bytes] maxLength:[data length]];
    NSLog(@"clint sent: %@", response);

}



@end
Kostas
  • 367
  • 1
  • 3
  • 17

1 Answers1

1

The short answer is that TCP offers a stream of bytes, not a stream of variable-length messages¹. I don't know why it works in the simulator, unless 192.168.1.1 is the same machine (in which case it isn't subject to the usual flow control, though it's perhaps surprising that the kernel buffers are that big).

To work around this, you need to somehow signal where the end of a message is. There are two common ways:

  • Length-prefixes. Examples include DJB's netstrings (e.g. 5:abcde for the bytestring "abcde"), HTTP's Content-Length, HTTP 1.1's chunked encoding, and various binary protocols typically with 8-bit, 16-bit, or variable-length-encoded lengths.
  • Delimiters. Examples include newlines (traditionally CRLF, e.g. in Telnet/SMTP/HTTP/IRC), null bytes, MIME boundaries (ick).

In this case, we can make use of the fact that null bytes aren't permitted within XML. currentMessage is assumed to be a NSMutableData ivar and not nil.

while ([inputStream hasBytesAvailable]) {
  len = [inputStream read:buffer maxLength:sizeof(buffer)];
  [currentMessage appendBytes:buffer length:len];
}
NSData * nullByte = [NSMutableData dataWithLength:1];
len = currentMessage.length;
NSRange searchRange = {0, len};
for (NSRange r; (r = [currentMessage rangeOfData:nullByte options:0 range:searchRange]).length; ) {
  NSString * message = [[NSString alloc] initWithBytes:currentMessage.bytes length:r.location encoding:NSUTF8StringEncoding];
  searchRange.location = r.location+r.length;
  searchRange.length = len - searchRange.location;
  [self messageReceived:message];
}
[currentMessage replaceBytesInRange:(NSRange){0, searchRange.location} withBytes:NULL length:0];

Also note that initializing things in -viewDidLoad requires some care if the view can be unloaded (this is less of an issue if you only support iOS 6+, since views are no longer automatically unloaded).

tc.
  • 33,468
  • 5
  • 78
  • 96
  • Thanks for your input @tc. ! I'm working on a personal project only for iOS8 and your code has been very helpful! Now to see how I can recover when the connection breaks... but that's another [topic](http://stackoverflow.com/questions/28265700/recovering-a-lost-connection-using-sockets)... – Kostas Feb 02 '15 at 22:25
  • I spoke too fast @tc. I edited the question as a different type of issue arose... – Kostas Feb 03 '15 at 23:07
  • `currentMessage` needs to be an ivar, not a local variable. – tc. Feb 17 '15 at 04:48