diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index b76e5c5..9e6a117 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -43,7 +43,7 @@ jobs: - name: Run ObjC Demo (all demos) run: | - printf 'a\n\n\n\n\n\nq\n' | ./ObjCDemo + printf 'a\n\n\n\n\n\n\nq\n' | ./ObjCDemo swift-tests: name: Swift Tests (macOS) diff --git a/ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m b/ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m index 214ea1a..e354dd3 100644 --- a/ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m +++ b/ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m @@ -38,12 +38,14 @@ @interface GCDAsyncSocket () #if NW_FRAMEWORK_AVAILABLE @property (nonatomic, assign) nw_connection_t connection; +@property (nonatomic, assign, nullable) nw_listener_t listener; #endif @property (nonatomic, strong) dispatch_queue_t socketQueue; @property (nonatomic, strong) NWStreamBuffer *buffer; @property (nonatomic, strong) NSMutableArray *readQueue; @property (nonatomic, assign) BOOL isReadingContinuously; +@property (atomic, assign) BOOL isListening; // SSE / streaming text mode @property (nonatomic, strong, nullable) NWSSEParser *sseParser; @@ -222,7 +224,11 @@ - (uint16_t)connectedPort { - (uint16_t)localPort { __block uint16_t port = 0; [self performSyncOnSocketQueue:^{ - port = _isConnected ? _localPort : 0; + if (_isListening) { + port = _localPort; + } else { + port = _isConnected ? _localPort : 0; + } }]; return port; } @@ -237,6 +243,9 @@ - (NSString *)localHost { - (void)dealloc { #if NW_FRAMEWORK_AVAILABLE + if (_listener) { + nw_listener_cancel(_listener); + } if (_connection) { nw_connection_cancel(_connection); } @@ -270,13 +279,230 @@ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)e } - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr { - (void)port; + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr { +#if NW_FRAMEWORK_AVAILABLE + if (self.isListening) { + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorAlreadyConnected + userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}]; + } + return NO; + } + + nw_parameters_t parameters = nw_parameters_create_secure_tcp( + NW_PARAMETERS_DISABLE_PROTOCOL, + NW_PARAMETERS_DEFAULT_CONFIGURATION + ); + + if (interface.length > 0) { + // Bind to a specific interface/address + NSString *portStr = [NSString stringWithFormat:@"%u", port]; + nw_endpoint_t localEndpoint = nw_endpoint_create_host(interface.UTF8String, portStr.UTF8String); + nw_parameters_set_local_endpoint(parameters, localEndpoint); + } + + nw_listener_t listener = nw_listener_create_with_port([NSString stringWithFormat:@"%u", port].UTF8String, parameters); + if (!listener) { + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorConnectionFailed + userInfo:@{NSLocalizedDescriptionKey: @"Failed to create listener."}]; + } + return NO; + } + + self.listener = listener; + + __weak typeof(self) weakSelf = self; + + nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + switch (state) { + case nw_listener_state_ready: { + strongSelf.isListening = YES; + uint16_t assignedPort = nw_listener_get_port(listener); + strongSelf.localPort = assignedPort; + break; + } + case nw_listener_state_failed: { + strongSelf.isListening = NO; + NSError *nsError = [strongSelf socketErrorWithCode:GCDAsyncSocketErrorConnectionFailed + description:@"Listener failed." + reason:@"NW listener entered failed state" + nwError:error]; + [strongSelf disconnectInternalWithError:nsError]; + break; + } + case nw_listener_state_cancelled: { + strongSelf.isListening = NO; + break; + } + default: + break; + } + }); + + nw_listener_set_new_connection_handler(listener, ^(nw_connection_t newConnection) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + // Create a new GCDAsyncSocket for the accepted connection. + // The new socket inherits the listener's delegate – this matches + // GCDAsyncSocket from CocoaAsyncSocket. The user may reassign the + // delegate on newSocket inside socket:didAcceptNewSocket: if needed. + GCDAsyncSocket *newSocket = [[GCDAsyncSocket alloc] initWithDelegate:strongSelf.delegate + delegateQueue:strongSelf.delegateQueue + socketQueue:nil]; + newSocket.connection = newConnection; + + // State change handler for the accepted connection + nw_connection_set_state_changed_handler(newConnection, ^(nw_connection_state_t state, nw_error_t _Nullable error) { + [newSocket handleStateChange:state error:error]; + }); + + nw_connection_set_queue(newConnection, newSocket.socketQueue); + nw_connection_start(newConnection); + + dispatch_async(strongSelf.delegateQueue, ^{ + id delegate = strongSelf.delegate; + if ([delegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) { + [delegate socket:strongSelf didAcceptNewSocket:newSocket]; + } + }); + }); + + nw_listener_set_queue(listener, self.socketQueue); + nw_listener_start(listener); + + return YES; +#else + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorConnectionFailed + userInfo:@{NSLocalizedDescriptionKey: @"Network.framework is not available on this platform."}]; + } + return NO; +#endif +} + +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr { +#if NW_FRAMEWORK_AVAILABLE + if (self.isListening) { + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorAlreadyConnected + userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}]; + } + return NO; + } + + if (!url.isFileURL) { + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorInvalidParameter + userInfo:@{NSLocalizedDescriptionKey: @"URL must be a file URL for Unix Domain Socket."}]; + } + return NO; + } + + nw_parameters_t parameters = nw_parameters_create_secure_tcp( + NW_PARAMETERS_DISABLE_PROTOCOL, + NW_PARAMETERS_DEFAULT_CONFIGURATION + ); + + // Remove existing socket file if present + NSString *path = url.path; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + + // Construct a unix:// URL for the endpoint (Network.framework expects this scheme) + NSString *unixURLString = [NSString stringWithFormat:@"unix://%@", path]; + nw_endpoint_t localEndpoint = nw_endpoint_create_url(unixURLString.UTF8String); + nw_parameters_set_local_endpoint(parameters, localEndpoint); + + nw_listener_t listener = nw_listener_create(parameters); + if (!listener) { + if (errPtr) { + *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain + code:GCDAsyncSocketErrorConnectionFailed + userInfo:@{NSLocalizedDescriptionKey: @"Failed to create Unix Domain Socket listener."}]; + } + return NO; + } + + self.listener = listener; + + __weak typeof(self) weakSelf = self; + + nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + switch (state) { + case nw_listener_state_ready: { + strongSelf.isListening = YES; + break; + } + case nw_listener_state_failed: { + strongSelf.isListening = NO; + NSError *nsError = [strongSelf socketErrorWithCode:GCDAsyncSocketErrorConnectionFailed + description:@"Unix Domain Socket listener failed." + reason:@"NW listener entered failed state" + nwError:error]; + [strongSelf disconnectInternalWithError:nsError]; + break; + } + case nw_listener_state_cancelled: { + strongSelf.isListening = NO; + break; + } + default: + break; + } + }); + + nw_listener_set_new_connection_handler(listener, ^(nw_connection_t newConnection) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + // See acceptOnInterface:port:error: for rationale on delegate sharing. + GCDAsyncSocket *newSocket = [[GCDAsyncSocket alloc] initWithDelegate:strongSelf.delegate + delegateQueue:strongSelf.delegateQueue + socketQueue:nil]; + newSocket.connection = newConnection; + + nw_connection_set_state_changed_handler(newConnection, ^(nw_connection_state_t state, nw_error_t _Nullable error) { + [newSocket handleStateChange:state error:error]; + }); + + nw_connection_set_queue(newConnection, newSocket.socketQueue); + nw_connection_start(newConnection); + + dispatch_async(strongSelf.delegateQueue, ^{ + id delegate = strongSelf.delegate; + if ([delegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) { + [delegate socket:strongSelf didAcceptNewSocket:newSocket]; + } + }); + }); + + nw_listener_set_queue(listener, self.socketQueue); + nw_listener_start(listener); + + return YES; +#else if (errPtr) { *errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain - code:GCDAsyncSocketErrorInvalidParameter - userInfo:@{NSLocalizedDescriptionKey: @"acceptOnPort:error: is not supported in NWAsyncSocketObjC."}]; + code:GCDAsyncSocketErrorConnectionFailed + userInfo:@{NSLocalizedDescriptionKey: @"Network.framework is not available on this platform."}]; } return NO; +#endif } - (BOOL)connectToHost:(NSString *)host @@ -362,7 +588,18 @@ - (BOOL)connectToHost:(NSString *)host - (void)disconnect { __weak typeof(self) weakSelf = self; dispatch_async(self.socketQueue, ^{ - [weakSelf disconnectInternalWithError:nil]; + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; +#if NW_FRAMEWORK_AVAILABLE + // Stop listener if in server mode + if (strongSelf.listener) { + nw_listener_cancel(strongSelf.listener); + strongSelf.listener = nil; + strongSelf.isListening = NO; + strongSelf.localPort = 0; + } +#endif + [strongSelf disconnectInternalWithError:nil]; }); } @@ -513,10 +750,8 @@ - (void)handleStateChange:(nw_connection_state_t)state error:(nw_error_t _Nullab if (localHostStr) { self.localHost = [NSString stringWithUTF8String:localHostStr]; } - const char *localPortStr = nw_endpoint_get_port(localEndpoint); - if (localPortStr) { - self.localPort = (uint16_t)strtoul(localPortStr, NULL, 10); - } + uint16_t localPortValue = nw_endpoint_get_port(localEndpoint); + self.localPort = localPortValue; } } #endif @@ -720,9 +955,9 @@ - (void)disconnectWithError:(NSError *)error { } - (void)disconnectInternalWithError:(NSError *)error { - if (!self.isConnected + if (!self.isConnected && !self.isListening #if NW_FRAMEWORK_AVAILABLE - && !self.connection + && !self.connection && !self.listener #endif ) { return; @@ -732,6 +967,11 @@ - (void)disconnectInternalWithError:(NSError *)error { self.isReadingContinuously = NO; #if NW_FRAMEWORK_AVAILABLE + if (self.listener) { + nw_listener_cancel(self.listener); + self.listener = nil; + self.isListening = NO; + } if (self.connection) { nw_connection_cancel(self.connection); self.connection = nil; diff --git a/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h b/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h index 1c29661..4642874 100644 --- a/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h +++ b/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h @@ -53,6 +53,9 @@ typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { /// Whether the socket is currently disconnected. @property (atomic, readonly) BOOL isDisconnected; +/// Whether the socket is currently listening for incoming connections (server mode). +@property (atomic, readonly) BOOL isListening; + /// Whether the socket is using a secure TLS transport. @property (atomic, readonly) BOOL isSecure; @@ -109,8 +112,17 @@ typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { error:(NSError **)errPtr; /// Compatibility API for CocoaAsyncSocket server mode. +/// Listen for incoming TCP connections on all interfaces on the given port. +/// Pass port 0 to let the system assign an available port (query via `localPort`). - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; +/// Listen for incoming TCP connections on a specific interface/address and port. +/// Pass @"localhost" or @"127.0.0.1" to restrict connections to the local machine. +- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/// Listen for incoming connections on a Unix Domain Socket at the given file URL. +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + // MARK: - Disconnect /// Disconnect the socket gracefully. diff --git a/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h b/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h index eab7863..116d149 100644 --- a/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h +++ b/ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h @@ -31,6 +31,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)error; @optional +/// Called when a listening socket accepts a new incoming connection. +/// The delegate **must** retain `newSocket` (e.g. add it to an array), +/// otherwise it will be deallocated and the connection will be dropped. +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + /// Called when a complete SSE event has been parsed. - (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event; diff --git a/ObjC/ObjCDemo/main.m b/ObjC/ObjCDemo/main.m index 7e8a148..d1b782c 100644 --- a/ObjC/ObjCDemo/main.m +++ b/ObjC/ObjCDemo/main.m @@ -10,6 +10,7 @@ // 3. UTF-8 Safety — Multi-byte character boundary detection // 4. NWReadRequest — Read-request queue types // 5. GCDAsyncSocket — Connection usage pattern +// 6. GCDAsyncSocket — Server socket API (accept/listen) // // Build (from repository root): // clang -framework Foundation \ @@ -26,6 +27,7 @@ // #import +#include #import "NWStreamBuffer.h" #import "NWSSEParser.h" #import "NWReadRequest.h" @@ -422,10 +424,19 @@ static void demoReadRequest(void) { // Sample delegate class for demonstration @interface DemoSocketDelegate : NSObject +@property (nonatomic, strong) NSMutableArray *acceptedSockets; @end @implementation DemoSocketDelegate +- (instancetype)init { + self = [super init]; + if (self) { + _acceptedSockets = [NSMutableArray array]; + } + return self; +} + - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { NSLog(@"Connected to %@:%u", host, port); // Send an HTTP GET request @@ -458,6 +469,11 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error { NSLog(@"Disconnected: %@", error ? error.localizedDescription : @"clean"); } +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { + NSLog(@"Accepted new connection: %@", newSocket); + [self.acceptedSockets addObject:newSocket]; +} + - (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event { NSLog(@"SSE Event: type=%@ data=%@", event.event, event.data); } @@ -538,9 +554,143 @@ static void demoGCDAsyncSocketUsage(void) { } // ============================================================================ -#pragma mark - Main Menu +#pragma mark - 6. GCDAsyncSocket Server API // ============================================================================ +static void demoServerSocket(void) { + printHeader(@"6. GCDAsyncSocket — Server Socket API"); + + DemoSocketDelegate *delegate = [[DemoSocketDelegate alloc] init]; + dispatch_queue_t queue = dispatch_queue_create("com.demo.server", DISPATCH_QUEUE_SERIAL); + + // ---- 6a. acceptOnPort: ---- + printSubHeader(@"6a. acceptOnPort: — Listen on a TCP port"); + + GCDAsyncSocket *serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:queue]; + printf("Created server socket\n"); + printf(" isListening: %s (expected: NO)\n", serverSocket.isListening ? "YES" : "NO"); + printf(" isConnected: %s (expected: NO)\n", serverSocket.isConnected ? "YES" : "NO"); + + NSError *err = nil; + BOOL ok = [serverSocket acceptOnPort:0 error:&err]; + printf(" acceptOnPort:0 → %s (port 0 lets the system choose)\n", ok ? "YES" : "NO"); + if (err) { + printf(" Error: %s\n", err.localizedDescription.UTF8String); + } + // Give the listener a moment to start + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + printf(" isListening: %s (expected: YES)\n", serverSocket.isListening ? "YES" : "NO"); + printf(" localPort: %u (system-assigned)\n", serverSocket.localPort); + + // ---- 6b. Double-accept error ---- + printSubHeader(@"6b. Double-accept error — Calling accept while already listening"); + + NSError *doubleErr = nil; + BOOL doubleOk = [serverSocket acceptOnPort:0 error:&doubleErr]; + printf(" acceptOnPort:0 again → %s (expected: NO)\n", doubleOk ? "YES" : "NO"); + if (doubleErr) { + printf(" Error: %s (expected: already listening)\n", doubleErr.localizedDescription.UTF8String); + } + printf("✅ Double-accept correctly returns error\n"); + + // ---- 6c. Client connects to server ---- + printSubHeader(@"6c. Client → Server connection — Verify didAcceptNewSocket:"); + + uint16_t serverPort = serverSocket.localPort; + GCDAsyncSocket *clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:queue]; + NSError *connectErr = nil; + BOOL connected = [clientSocket connectToHost:@"127.0.0.1" onPort:serverPort error:&connectErr]; + printf(" Client connectToHost:127.0.0.1 onPort:%u → %s\n", serverPort, connected ? "YES" : "NO"); + if (connectErr) { + printf(" Connect Error: %s\n", connectErr.localizedDescription.UTF8String); + } + + // Pump the run loop to allow the connection and accept to complete + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; + printf(" Accepted sockets count: %lu (expected: 1)\n", + (unsigned long)delegate.acceptedSockets.count); + if (delegate.acceptedSockets.count > 0) { + GCDAsyncSocket *accepted = delegate.acceptedSockets[0]; + printf(" Accepted socket isConnected: %s\n", accepted.isConnected ? "YES" : "NO"); + printf("✅ Server accepted client connection via didAcceptNewSocket:\n"); + } else { + printf("⚠️ No accepted socket yet (listener may need more time)\n"); + } + + // ---- 6d. Disconnect server ---- + printSubHeader(@"6d. Disconnect server — Verify listener stops"); + + [serverSocket disconnect]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + printf(" isListening after disconnect: %s (expected: NO)\n", + serverSocket.isListening ? "YES" : "NO"); + printf("✅ Server socket disconnected, listener stopped\n"); + + // Clean up client + [clientSocket disconnect]; + for (GCDAsyncSocket *s in delegate.acceptedSockets) { + [s disconnect]; + } + + // ---- 6e. acceptOnInterface:port: ---- + printSubHeader(@"6e. acceptOnInterface:port: — Listen on localhost only"); + + GCDAsyncSocket *localServer = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:queue]; + NSError *ifErr = nil; + BOOL ifOk = [localServer acceptOnInterface:@"127.0.0.1" port:0 error:&ifErr]; + printf(" acceptOnInterface:@\"127.0.0.1\" port:0 → %s\n", ifOk ? "YES" : "NO"); + if (ifErr) { + printf(" Error: %s\n", ifErr.localizedDescription.UTF8String); + } + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + printf(" isListening: %s (expected: YES)\n", localServer.isListening ? "YES" : "NO"); + printf("✅ acceptOnInterface works\n"); + [localServer disconnect]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + // ---- 6f. acceptOnUrl: (Unix Domain Socket) ---- + printSubHeader(@"6f. acceptOnUrl: — Listen on Unix Domain Socket"); + + GCDAsyncSocket *udsServer = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:queue]; + NSString *tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent: + [NSString stringWithFormat:@"nwasyncsocket_demo_%d.sock", getpid()]]; + NSURL *sockURL = [NSURL fileURLWithPath:tmpPath]; + NSError *udsErr = nil; + BOOL udsOk = [udsServer acceptOnUrl:sockURL error:&udsErr]; + printf(" acceptOnUrl:%s → %s\n", tmpPath.UTF8String, udsOk ? "YES" : "NO"); + if (udsErr) { + printf(" Error: %s\n", udsErr.localizedDescription.UTF8String); + } + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + printf(" isListening: %s (expected: YES)\n", udsServer.isListening ? "YES" : "NO"); + printf("✅ acceptOnUrl works\n"); + [udsServer disconnect]; + + // Clean up socket file + [[NSFileManager defaultManager] removeItemAtPath:tmpPath error:nil]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + // ---- 6g. acceptOnUrl: with non-file URL error ---- + printSubHeader(@"6g. acceptOnUrl: error — Non-file URL rejected"); + + GCDAsyncSocket *badUds = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:queue]; + NSError *badErr = nil; + NSURL *httpUrl = [NSURL URLWithString:@"http://example.com"]; + BOOL badOk = [badUds acceptOnUrl:httpUrl error:&badErr]; + printf(" acceptOnUrl:http://example.com → %s (expected: NO)\n", badOk ? "YES" : "NO"); + if (badErr) { + printf(" Error: %s (expected: must be file URL)\n", badErr.localizedDescription.UTF8String); + } + printf("✅ Non-file URL correctly rejected\n"); + + waitForUser(); +} + static void printMenu(void) { printHeader(@"NWAsyncSocket Objective-C Demo"); printf( @@ -550,6 +700,7 @@ static void printMenu(void) { " 3. UTF-8 Safety — Multi-byte character boundary detection\n" " 4. NWReadRequest — Read-request queue types\n" " 5. GCDAsyncSocket — Connection usage pattern\n" + " 6. GCDAsyncSocket — Server socket API\n" " a. Run all demos\n" " q. Quit\n\n" ); @@ -580,17 +731,20 @@ int main(int argc, const char * argv[]) { demoReadRequest(); } else if ([input isEqualToString:@"5"]) { demoGCDAsyncSocketUsage(); + } else if ([input isEqualToString:@"6"]) { + demoServerSocket(); } else if ([input isEqualToString:@"a"]) { demoStreamBuffer(); demoSSEParser(); demoUTF8Safety(); demoReadRequest(); demoGCDAsyncSocketUsage(); + demoServerSocket(); } else if ([input isEqualToString:@"q"]) { printf("\nGoodbye! 👋\n"); running = NO; } else { - printf("Invalid choice. Please enter 1-5, a, or q.\n"); + printf("Invalid choice. Please enter 1-6, a, or q.\n"); } } }