如何确保服务器端 CFSocket 在应用程序退出并 returns 重新激活后进行监听?

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

我有一个 iOS 应用程序,大致遵循以下步骤:

  1. 打开监听套接字。
  2. 接受单个客户端连接。
  3. 执行数据交换to/from客户端。
  4. 当它接收到 "resign active" 事件时,它会关闭并释放与客户端和服务器套接字关联的所有资源(即使所有 运行 循环源、read/write 流和套接字本身)。
  5. 恢复活动后,它会恢复监听套接字以继续通信(客户端将继续尝试重新连接,直到它能够,在 iOS 应用程序在步骤 #4 中退出活动后)。

每当客户端和服务器之间确实发生连接时,我在步骤 #5 后看到的是应用程序恢复,但无法重新打开服务器套接字进行侦听。换句话说,即使在步骤#5 中释放了所有内容,应用程序也无法重新绑定并侦听套接字地址。更糟糕的是,在尝试重新设置侦听套接字时,在 CFSocket API 调用中没有检测到任何错误。

另一方面,如果 iOS 应用程序退出活动状态并再次恢复,而之前没有收到任何连接,则客户端可以恰好连接一次,直到应用程序退出并再次恢复,其中然后可以观察到上述相同行为的情况。

可以在以下存储库中找到说明此问题的示例最小应用程序:

https://github.com/dpereira/cfsocket_reopen_bug

最相关的来源是:

#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

成熟的应用位于:https://github.com/dpereira/conflux

套接字(及相关资源)的 setup/teardown 是否有问题?

这里的问题是侦听套接字正在进入 TIME_WAIT 并且在该状态下无法再次绑定。

即使 CFSocket API 没有返回任何错误,如果在使用 POSIX 套接字时发生相同的情况,则在尝试重新绑定套接字时会发生错误。

解决方案是在重新绑定套接字以进行侦听之前简单地为套接字设置 SO_REUSEADDR 选项。