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.