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.2
或 iPhone 6 running iOS 9
上有 no issue
。我已经在 two iPhone 7's running iOS 10.3.2
和 both 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
但它对我没有帮助,因为它破坏了消息请求和响应。它会在同一条消息中发回多个响应,从而在解析消息时增加更多的复杂性。
我发现您的代码中有几个问题,您应该尝试修复。
您可以并发访问 _dataQueue
数组:您从另一个线程(使用全局队列)写入它,但在主线程上读取。
由于您在主线程上使用流,因此您应该避免像在处理 NSStreamEventHasBytesAvailable
事件时那样使用循环。只需将一些数据读入缓冲区并将其存储在 NSMutableString
中。
数据可能不完整,因此您应该手动检查您的缓冲区中是否有完整的行,如果是,就处理它。当您使用 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 后,我的套接字消息似乎响应更快,并且我不再遇到数据包合并。
我正在创建一个 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.2
或 iPhone 6 running iOS 9
上有 no issue
。我已经在 two iPhone 7's running iOS 10.3.2
和 both 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
但它对我没有帮助,因为它破坏了消息请求和响应。它会在同一条消息中发回多个响应,从而在解析消息时增加更多的复杂性。
我发现您的代码中有几个问题,您应该尝试修复。
您可以并发访问
_dataQueue
数组:您从另一个线程(使用全局队列)写入它,但在主线程上读取。由于您在主线程上使用流,因此您应该避免像在处理
NSStreamEventHasBytesAvailable
事件时那样使用循环。只需将一些数据读入缓冲区并将其存储在NSMutableString
中。数据可能不完整,因此您应该手动检查您的缓冲区中是否有完整的行,如果是,就处理它。当您使用
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 后,我的套接字消息似乎响应更快,并且我不再遇到数据包合并。