TCP 套接字无法在 iPhone 7 运行 iOS 10.3.2 上工作

TCP Sockets not working on iPhone 7 running iOS 10.3.2

我正在创建一个 communicates 通过 TCP 套接字 external service 的应用程序 (NSStream)。

我 运行 遇到一个关于 iPhone 7 running iOS 10.3.2 的问题,据此 NSStream is congested and messages can't send fast enough 我的应用程序可以做出反应。在 iPhone 6s running iOS 10.3.2iPhone 6 running iOS 9 上有 no issue。我已经在 two iPhone 7's running iOS 10.3.2both have the same issue.

上试过了

所以本质上,我每秒都会向我的外部设备发送多条请求消息。

例如: 如果我每秒向外部服务发送3条消息,只有一条消息发送响应。我写了一个回调方法,只有当我从外部设备收到 ACK 时才会触发。我使用了 NSLogs 并且我已经确定请求实际上从未通过套接字发送,这使我相信这是一个 iOS 问题(可能在等待响应时阻止流发送其他消息?)

这是我的 TCPSocketManager class 的代码,其中管理套接字连接(我在后台线程上发送请求,然后一旦响应返回,我发送在主线程上回调):

@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

这里有一个sample request和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
              }
        }];
}

因为这个问题只出现在iPhone 7台设备上。我想知道这是否是 NSStream bug? 有没有人遇到过任何类似的问题。使用 CocoaAsyncSocket 这样的库会更好吗?有什么办法可以在不使用外部库的情况下解决这个问题吗?

我之前设置过 CocoaAsyncSocket 但它对我没有帮助,因为它破坏了消息请求和响应。它会在同一条消息中发回多个响应,从而在解析消息时增加更多的复杂性。

我发现您的代码中有几个问题,您应该尝试修复。

  1. 您可以并发访问 _dataQueue 数组:您从另一个线程(使用全局队列)写入它,但在主线程上读取。

  2. 由于您在主线程上使用流,因此您应该避免像在处理 NSStreamEventHasBytesAvailable 事件时那样使用循环。只需将一些数据读入缓冲区并将其存储在 NSMutableString 中。

  3. 数据可能不完整,因此您应该手动检查您的缓冲区中是否有完整的行,如果是,就处理它。当您使用 enumerateLinesUsingBlock: 时,可能会有不完整的行,您将丢失该不完整的行。

经过大量试验和错误后,我得出结论,由于我快速发送相对较小的字节消息,TCP 数据包正在内核级别合并。我在查找这个问题时遇到了 Nagles Algorithm`。

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.

我的问题的解决方案是在 question.

上找到的

基本上,为了解决我的问题,我需要像这样禁用 Nagles 算法:

#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);
    }
}

禁用 Nagles 后,我的套接字消息似乎响应更快,并且我不再遇到数据包合并。