TCP 套接字在运行 iOS 10.3.2 的 iPhone 7 上不起作用



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

我在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类的代码,其中套接字连接是管理的(我在后台线程上发送请求,然后一旦响应返回,我就会在主线程上发送回调):

@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

下面是 TCPSocketManager 类的sample request和定义:

-(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 算法。

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.

在这个问题上找到了我问题的解决方案。

基本上,为了解决我的问题,我需要像这样禁用 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 后,我的套接字消息似乎响应更快,我不再遇到数据包合并的情况。

最新更新