iosiphonesocketscfsocket

How to ensure server-side CFSocket listens after application resigns and returns to active back again?


I have an iOS application that follows roughly the following steps:

  1. Opens a listening socket.
  2. Accepts a single client connection.
  3. Performs data exchanges to/from client.
  4. When it receives a "resign active" event, it closes and releases all resources associated to the client and server sockets (i.e. invalidates and releases all run loop sources, read/write streams and sockets themselves).
  5. Upon resuming active, it brings back up the listening socket to continue communications (the client will keep trying to reconnect until it is able to, after the iOS app resigned active in step #4).

Whenever a connection does take place between client and server, what I am seeing after step #5 is that the application resumes without being able to reopening the server socket for listening. In other words, even though everything is released in step #5, the application is not able to rebind and listen at the socket address. What's worse, no errors can be detected in the CFSocket API calls while trying to setup the listening socket back again.

If, on the other hand the iOS application resigns active and resumes back again without previously receiving any connection, the client is then able to connect exactly once, until the application resigns and resumes again, in which case the same behaviour above can then be observed.

An example minimal application that illustrates this issue can be found in the following repository:

https://github.com/dpereira/cfsocket_reopen_bug

The most relevant source is:

#import "AppDelegate.h"
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

static void _handleConnect(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    NSLog(@"Connected ...");
    close(*(CFSocketNativeHandle*)data);
    NSLog(@"Closed ...");
}

@interface AppDelegate ()

@end

@implementation AppDelegate {
    CFRunLoopSourceRef _source;
    CFSocketRef _serverSocket;
    CFRunLoopRef _socketRunLoop;
}

- (void)applicationWillResignActive:(UIApplication *)application {
    
    CFRunLoopRemoveSource(self->_socketRunLoop, self->_source, kCFRunLoopCommonModes);
    CFRunLoopSourceInvalidate(self->_source);
    CFRelease(self->_source);
    self->_source = nil;
    
    CFSocketInvalidate(self->_serverSocket);
    CFRelease(self->_serverSocket);
    self->_serverSocket = nil;
    
    CFRunLoopStop(self->_socketRunLoop);
    
    NSLog(@"RELASED SUCCESSFULLY!");
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    CFSocketContext ctx = {0, (__bridge void*)self, NULL, NULL, NULL};
    self->_serverSocket = CFSocketCreate(kCFAllocatorDefault,
                                        PF_INET,
                                        SOCK_STREAM,
                                        IPPROTO_TCP,
                                        kCFSocketAcceptCallBack, _handleConnect, &ctx);
    
    NSLog(@"Socket created %u", self->_serverSocket != NULL);
    
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_len = sizeof(sin);
    sin.sin_family = AF_INET;
    sin.sin_port = htons(30000);
    sin.sin_addr.s_addr= INADDR_ANY;
    
    CFDataRef sincfd = CFDataCreate(kCFAllocatorDefault,
                                    (UInt8 *)&sin,
                                    sizeof(sin));
    CFSocketSetAddress(self->_serverSocket, sincfd);
    CFRelease(sincfd);
    

    self->_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault,
                                               self->_serverSocket,
                                               0);
    
    NSLog(@"Created source %u", self->_source != NULL);
    
    self->_socketRunLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(self->_socketRunLoop,
                       self->_source,
                       kCFRunLoopCommonModes);
    
    NSLog(@"Registered into run loop");
    NSLog(@"Socket is %s", CFSocketIsValid(self->_serverSocket) ? "valid" : "invalid");
    NSLog(@"Source is %s", CFRunLoopSourceIsValid(self->_source) ? "valid" : "invalid");
}

@end

The full-blown app resides in: https://github.com/dpereira/conflux

Is there something wrong in the setup/teardown of the sockets (and related resources)?


Solution

  • The issue here is that the listening socket was going into TIME_WAIT and could not be bound to again while in that state.

    Even though no errors are returned by the CFSocket API, if the same situation happens when using POSIX sockets an error takes place while trying to re-bind the socket.

    The solution was to simply set the SO_REUSEADDR option for the socket just prior to re-binding the socket for listening.