iosobjective-csocketstcpnsstream

TCP Sockets not working on iPhone 7 running iOS 10.3.2


I am creating an app that communicates with an external service via a TCP Socket (NSStream).

I am running into an issue on an iPhone 7 running iOS 10.3.2 whereby the NSStream is congested and messages can't send fast enough for my app to react. There is no issue with this on an iPhone 6s running iOS 10.3.2 or an iPhone 6 running iOS 9. I have tried this on two iPhone 7's running iOS 10.3.2 and both have the same issue.

So in essence, I'm sending multiple request messages to my external device every second.

For example: If I send 3 messages to the external service every second, only one of the messages sends the response. I have written a callback method that only fires when I get an ACK back from the external device. I have used NSLogs and I have identified that the requests are never actually sent over the socket, leading me to believe this is an iOS issue (Stream is blocked from sending additional messages while waiting for a response maybe?)

Here is my code for my TCPSocketManager class, where the socket connection is managed (I'm sending the requests on a background thread, then once the response comes back, I send the callback out on the main thread):

@interface TCPSocketManager ()
@property (weak, nonatomic)NSMutableArray *jsonObject;
@property (weak, nonatomic)NSMutableArray *dataQueue;
@property (nonatomic)bool sentNotif;
@end

static NSString *hostIP;
static int hostPORT;
@implementation TCPSocketManager {
    BOOL flag_canSendDirectly;
}

-(instancetype)initWithSocketHost:(NSString *)host withPort:(int)port{
    hostIP = host;
    hostPORT = port;

    _completionDict = [NSMutableDictionary new];
    _dataQueue = [NSMutableArray new];

    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(host), port, &readStream, &writeStream);

    _inputStream = (__bridge NSInputStream *)readStream;
    _outputStream = (__bridge NSOutputStream *)writeStream;

    [self openStreams];
    return self;
}

-(void)openStreams {
    [_outputStream setDelegate:self];
    [_inputStream setDelegate:self];

    [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [_outputStream open];
    [_inputStream open];
}

-(void)closeStreams{
    [_outputStream close];
    [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [_inputStream close];
    [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void) messageReceived:(NSString *)message {
    [message enumerateLinesUsingBlock:^(NSString * _Nonnull msg, BOOL * _Nonnull stop) {
        [_messages addObject:msg];

        NSError *error;
        NSMutableArray *copyJsonObject = [NSJSONSerialization JSONObjectWithData:[msg dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];
        _jsonObject = [copyJsonObject copy];

        NSDictionary *rsp_type = [_jsonObject valueForKey:@"rsp"];                            
        NSString *typeKey = rsp_type[@"type”];
        CompleteMsgRsp response = _completionDict[typeKey];
        //assign the response to the block

         if (response){
             dispatch_async(dispatch_get_main_queue(), ^{
                  response(rsp_type);
             });
         }

         [_completionDict removeObjectForKey:typeKey]
   }];
}

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
    switch (streamEvent) {
        case NSStreamEventOpenCompleted:
            break;

        case NSStreamEventHasBytesAvailable:
            if (theStream == _inputStream){
                uint8_t buffer[1024];
                NSInteger len;

                while ([_inputStream hasBytesAvailable])
                {
                    len = [_inputStream read:buffer maxLength:sizeof(buffer)];
                    if (len > 0)
                    {
                        NSString *output = [[NSString alloc] initWithBytes:buffer length:len encoding:NSASCIIStringEncoding];
                        if (nil != output)
                        {
                            [self messageReceived:output];
               //Do Something with the message
                        }
                    }
                }
            }
            break;

        case NSStreamEventHasSpaceAvailable:{
            //send data over stream now that we know the stream is ready to send/ receive
            [self _sendData];
            break;
        }
        case NSStreamEventErrorOccurred:
            [self initWithSocketHost:hostIP withPort:hostPORT];
            break;

        case NSStreamEventEndEncountered:
            [theStream close];
            [theStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

            break;
        default:
            DLog(@"Unknown Stream Event");
    }

}

- (void)sendData:(NSData *)data {
    //insert the request to the head of a queue
    [_dataQueue insertObject:data atIndex:0];

    //if able to send directly, send it. This flag is set in _sendData if the array is empty
    //Message is sent when the stream has space available.
    if (flag_canSendDirectly) [self _sendData];
}

-(void)_sendData {
    flag_canSendDirectly = NO;

    //get the last object of the array.
    NSData *data = [_dataQueue lastObject];

    //if data is empty, set the send direct flag
    if (data == nil){
        flag_canSendDirectly = YES;
        return;
    }
    //send request out over stream, store the amount of bytes written to stream
    NSInteger bytesWritten = [_outputStream write:[data bytes] maxLength:[data length]];

    //if bytes written is more than 0, we know something was output over the stream
    if (bytesWritten >0) {
        //remove the request from the queue.
        [self.dataQueue removeLastObject];
    }
}

- (void)sendRequest:(NSString*)request withCompletion:(void (^)(NSDictionary *rsp_dict))finishBlock{
    self.uuid = [[NSUUID UUID] UUIDString];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
            //Convert the request string to NSData.
            NSData *data = [[NSData alloc] initWithData:[request dataUsingEncoding:NSASCIIStringEncoding]];

            //method to send the data over stream with a queue
            [self sendData:data];

            //Completion Handler for Messages
            NSString *typeKey = reqType;
            [_completionDict setObject:[finishBlock copy] forKey:typeKey];
    });
}
@end

Here is a sample request and the definition of the TCPSocketManager class:

-(void)connectToSocket{
        _socketMan = [[TCPSocketManager alloc] initWithSocketHost:@"192.168.1.10" withPort:50505];
}

-(void)sendSomeRequest:(NSString *)request {
        [_socketMan sendRequest:request withCompletion:^(NSDictionary *rsp_dict) {
              NSString *result =[rsp_dict objectForKey:@"result"];
              if ([result length] < 3 && [result isEqualToString:@"OK"]){
                  //Successful request with a response
              }else{
                  //Request has failed with no/ bad response
              }
        }];
}

Since this problem only appears on iPhone 7 devices. I'm wondering if this is an NSStream bug? Has anyone come across any similar issues. Would I be better off using a library such as CocoaAsyncSocket? Is there any way to fix the issue without using an external library?

I have set up CocoaAsyncSocket before and it didn't help me as it was breaking messages requests and responses. It would send multiple responses back in the same message, adding more complexity when parsing the messages.


Solution

  • After a lot of trial and error, I came to the conclusion that TCP Packets were being coalesced on the kernel level due to me rapidly sending relatively small byte message. I came across Nagles Algorithm` when looking up this issue.

    Nagle's algorithm works by combining a number of small outgoing messages, and sending them all at once. Specifically, as long as there is a sent packet for which the sender has received no acknowledgment, the sender should keep buffering its output until it has a full packet's worth of output, thus allowing output to be sent all at once.

    The solution to my problem was found on this question.

    Basically, In order to fix my problem, I needed to disable Nagles algorithm like so:

    #import <arpa/inet.h>       // for IPPROTO_TCP
    #include <netinet/tcp.h>    // for TCP_NODELAY
    
    - (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
        switch (streamEvent) {
          case NSStreamEventOpenCompleted:
            [self disableNaglesAlgorithmForStream:theStream];
            break;
        ...
    }
    
    
    //from tar500's answer in the linked question.
    -(void)disableNaglesAlgorithmForStream:(NSStream *)stream {
        CFDataRef socketData;
    
        // Get socket data
        if ([stream isKindOfClass:[NSOutputStream class]]) {
            socketData = CFWriteStreamCopyProperty((__bridge CFWriteStreamRef)((NSOutputStream *)stream), kCFStreamPropertySocketNativeHandle);
        } else if ([stream isKindOfClass:[NSInputStream class]]) {
            socketData = CFReadStreamCopyProperty((__bridge CFReadStreamRef)((NSInputStream *)stream), kCFStreamPropertySocketNativeHandle);
        }
    
        // get a handle to the native socket
        CFSocketNativeHandle rawsock;
    
        CFDataGetBytes(socketData, CFRangeMake(0, sizeof(CFSocketNativeHandle)), (UInt8 *)&rawsock);
        CFRelease(socketData);
    
        // Disable Nagle's algorythm
    
        // Debug info
        BOOL isInput = [stream isKindOfClass:[NSInputStream class]];
        NSString * streamType = isInput ? @"INPUT" : @"OUTPUT";
    
        int err;
        static const int kOne = 1;
        err = setsockopt(rawsock, IPPROTO_TCP, TCP_NODELAY, &kOne, sizeof(kOne));
        if (err < 0) {
            err = errno;
            NSLog(@"Could Not Disable Nagle for %@ stream", streamType);
        } else {
            NSLog(@"Nagle Is Disabled for %@ stream", streamType);
        }
    }
    

    After disabling Nagles, my socket messages appeared to be more responsive and I'm not experiencing the packet coalescing any more.