Skip to content

Commit 6d2e213

Browse files
authored
Merge pull request #7 from dustturtle/copilot/add-server-socket-support
add server socket support, Fix ObjC build error and add server socket API demo coverage
2 parents 77948f5 + 89dc739 commit 6d2e213

5 files changed

Lines changed: 425 additions & 14 deletions

File tree

.github/workflows/demo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
4444
- name: Run ObjC Demo (all demos)
4545
run: |
46-
printf 'a\n\n\n\n\n\nq\n' | ./ObjCDemo
46+
printf 'a\n\n\n\n\n\n\nq\n' | ./ObjCDemo
4747
4848
swift-tests:
4949
name: Swift Tests (macOS)

ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m

Lines changed: 251 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ @interface GCDAsyncSocket ()
3838

3939
#if NW_FRAMEWORK_AVAILABLE
4040
@property (nonatomic, assign) nw_connection_t connection;
41+
@property (nonatomic, assign, nullable) nw_listener_t listener;
4142
#endif
4243

4344
@property (nonatomic, strong) dispatch_queue_t socketQueue;
4445
@property (nonatomic, strong) NWStreamBuffer *buffer;
4546
@property (nonatomic, strong) NSMutableArray<NWReadRequest *> *readQueue;
4647
@property (nonatomic, assign) BOOL isReadingContinuously;
48+
@property (atomic, assign) BOOL isListening;
4749

4850
// SSE / streaming text mode
4951
@property (nonatomic, strong, nullable) NWSSEParser *sseParser;
@@ -222,7 +224,11 @@ - (uint16_t)connectedPort {
222224
- (uint16_t)localPort {
223225
__block uint16_t port = 0;
224226
[self performSyncOnSocketQueue:^{
225-
port = _isConnected ? _localPort : 0;
227+
if (_isListening) {
228+
port = _localPort;
229+
} else {
230+
port = _isConnected ? _localPort : 0;
231+
}
226232
}];
227233
return port;
228234
}
@@ -237,6 +243,9 @@ - (NSString *)localHost {
237243

238244
- (void)dealloc {
239245
#if NW_FRAMEWORK_AVAILABLE
246+
if (_listener) {
247+
nw_listener_cancel(_listener);
248+
}
240249
if (_connection) {
241250
nw_connection_cancel(_connection);
242251
}
@@ -270,13 +279,230 @@ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)e
270279
}
271280

272281
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr {
273-
(void)port;
282+
return [self acceptOnInterface:nil port:port error:errPtr];
283+
}
284+
285+
- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr {
286+
#if NW_FRAMEWORK_AVAILABLE
287+
if (self.isListening) {
288+
if (errPtr) {
289+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
290+
code:GCDAsyncSocketErrorAlreadyConnected
291+
userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}];
292+
}
293+
return NO;
294+
}
295+
296+
nw_parameters_t parameters = nw_parameters_create_secure_tcp(
297+
NW_PARAMETERS_DISABLE_PROTOCOL,
298+
NW_PARAMETERS_DEFAULT_CONFIGURATION
299+
);
300+
301+
if (interface.length > 0) {
302+
// Bind to a specific interface/address
303+
NSString *portStr = [NSString stringWithFormat:@"%u", port];
304+
nw_endpoint_t localEndpoint = nw_endpoint_create_host(interface.UTF8String, portStr.UTF8String);
305+
nw_parameters_set_local_endpoint(parameters, localEndpoint);
306+
}
307+
308+
nw_listener_t listener = nw_listener_create_with_port([NSString stringWithFormat:@"%u", port].UTF8String, parameters);
309+
if (!listener) {
310+
if (errPtr) {
311+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
312+
code:GCDAsyncSocketErrorConnectionFailed
313+
userInfo:@{NSLocalizedDescriptionKey: @"Failed to create listener."}];
314+
}
315+
return NO;
316+
}
317+
318+
self.listener = listener;
319+
320+
__weak typeof(self) weakSelf = self;
321+
322+
nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t _Nullable error) {
323+
__strong typeof(weakSelf) strongSelf = weakSelf;
324+
if (!strongSelf) return;
325+
326+
switch (state) {
327+
case nw_listener_state_ready: {
328+
strongSelf.isListening = YES;
329+
uint16_t assignedPort = nw_listener_get_port(listener);
330+
strongSelf.localPort = assignedPort;
331+
break;
332+
}
333+
case nw_listener_state_failed: {
334+
strongSelf.isListening = NO;
335+
NSError *nsError = [strongSelf socketErrorWithCode:GCDAsyncSocketErrorConnectionFailed
336+
description:@"Listener failed."
337+
reason:@"NW listener entered failed state"
338+
nwError:error];
339+
[strongSelf disconnectInternalWithError:nsError];
340+
break;
341+
}
342+
case nw_listener_state_cancelled: {
343+
strongSelf.isListening = NO;
344+
break;
345+
}
346+
default:
347+
break;
348+
}
349+
});
350+
351+
nw_listener_set_new_connection_handler(listener, ^(nw_connection_t newConnection) {
352+
__strong typeof(weakSelf) strongSelf = weakSelf;
353+
if (!strongSelf) return;
354+
355+
// Create a new GCDAsyncSocket for the accepted connection.
356+
// The new socket inherits the listener's delegate – this matches
357+
// GCDAsyncSocket from CocoaAsyncSocket. The user may reassign the
358+
// delegate on newSocket inside socket:didAcceptNewSocket: if needed.
359+
GCDAsyncSocket *newSocket = [[GCDAsyncSocket alloc] initWithDelegate:strongSelf.delegate
360+
delegateQueue:strongSelf.delegateQueue
361+
socketQueue:nil];
362+
newSocket.connection = newConnection;
363+
364+
// State change handler for the accepted connection
365+
nw_connection_set_state_changed_handler(newConnection, ^(nw_connection_state_t state, nw_error_t _Nullable error) {
366+
[newSocket handleStateChange:state error:error];
367+
});
368+
369+
nw_connection_set_queue(newConnection, newSocket.socketQueue);
370+
nw_connection_start(newConnection);
371+
372+
dispatch_async(strongSelf.delegateQueue, ^{
373+
id delegate = strongSelf.delegate;
374+
if ([delegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) {
375+
[delegate socket:strongSelf didAcceptNewSocket:newSocket];
376+
}
377+
});
378+
});
379+
380+
nw_listener_set_queue(listener, self.socketQueue);
381+
nw_listener_start(listener);
382+
383+
return YES;
384+
#else
385+
if (errPtr) {
386+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
387+
code:GCDAsyncSocketErrorConnectionFailed
388+
userInfo:@{NSLocalizedDescriptionKey: @"Network.framework is not available on this platform."}];
389+
}
390+
return NO;
391+
#endif
392+
}
393+
394+
- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr {
395+
#if NW_FRAMEWORK_AVAILABLE
396+
if (self.isListening) {
397+
if (errPtr) {
398+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
399+
code:GCDAsyncSocketErrorAlreadyConnected
400+
userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}];
401+
}
402+
return NO;
403+
}
404+
405+
if (!url.isFileURL) {
406+
if (errPtr) {
407+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
408+
code:GCDAsyncSocketErrorInvalidParameter
409+
userInfo:@{NSLocalizedDescriptionKey: @"URL must be a file URL for Unix Domain Socket."}];
410+
}
411+
return NO;
412+
}
413+
414+
nw_parameters_t parameters = nw_parameters_create_secure_tcp(
415+
NW_PARAMETERS_DISABLE_PROTOCOL,
416+
NW_PARAMETERS_DEFAULT_CONFIGURATION
417+
);
418+
419+
// Remove existing socket file if present
420+
NSString *path = url.path;
421+
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
422+
423+
// Construct a unix:// URL for the endpoint (Network.framework expects this scheme)
424+
NSString *unixURLString = [NSString stringWithFormat:@"unix://%@", path];
425+
nw_endpoint_t localEndpoint = nw_endpoint_create_url(unixURLString.UTF8String);
426+
nw_parameters_set_local_endpoint(parameters, localEndpoint);
427+
428+
nw_listener_t listener = nw_listener_create(parameters);
429+
if (!listener) {
430+
if (errPtr) {
431+
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
432+
code:GCDAsyncSocketErrorConnectionFailed
433+
userInfo:@{NSLocalizedDescriptionKey: @"Failed to create Unix Domain Socket listener."}];
434+
}
435+
return NO;
436+
}
437+
438+
self.listener = listener;
439+
440+
__weak typeof(self) weakSelf = self;
441+
442+
nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t _Nullable error) {
443+
__strong typeof(weakSelf) strongSelf = weakSelf;
444+
if (!strongSelf) return;
445+
446+
switch (state) {
447+
case nw_listener_state_ready: {
448+
strongSelf.isListening = YES;
449+
break;
450+
}
451+
case nw_listener_state_failed: {
452+
strongSelf.isListening = NO;
453+
NSError *nsError = [strongSelf socketErrorWithCode:GCDAsyncSocketErrorConnectionFailed
454+
description:@"Unix Domain Socket listener failed."
455+
reason:@"NW listener entered failed state"
456+
nwError:error];
457+
[strongSelf disconnectInternalWithError:nsError];
458+
break;
459+
}
460+
case nw_listener_state_cancelled: {
461+
strongSelf.isListening = NO;
462+
break;
463+
}
464+
default:
465+
break;
466+
}
467+
});
468+
469+
nw_listener_set_new_connection_handler(listener, ^(nw_connection_t newConnection) {
470+
__strong typeof(weakSelf) strongSelf = weakSelf;
471+
if (!strongSelf) return;
472+
473+
// See acceptOnInterface:port:error: for rationale on delegate sharing.
474+
GCDAsyncSocket *newSocket = [[GCDAsyncSocket alloc] initWithDelegate:strongSelf.delegate
475+
delegateQueue:strongSelf.delegateQueue
476+
socketQueue:nil];
477+
newSocket.connection = newConnection;
478+
479+
nw_connection_set_state_changed_handler(newConnection, ^(nw_connection_state_t state, nw_error_t _Nullable error) {
480+
[newSocket handleStateChange:state error:error];
481+
});
482+
483+
nw_connection_set_queue(newConnection, newSocket.socketQueue);
484+
nw_connection_start(newConnection);
485+
486+
dispatch_async(strongSelf.delegateQueue, ^{
487+
id delegate = strongSelf.delegate;
488+
if ([delegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) {
489+
[delegate socket:strongSelf didAcceptNewSocket:newSocket];
490+
}
491+
});
492+
});
493+
494+
nw_listener_set_queue(listener, self.socketQueue);
495+
nw_listener_start(listener);
496+
497+
return YES;
498+
#else
274499
if (errPtr) {
275500
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
276-
code:GCDAsyncSocketErrorInvalidParameter
277-
userInfo:@{NSLocalizedDescriptionKey: @"acceptOnPort:error: is not supported in NWAsyncSocketObjC."}];
501+
code:GCDAsyncSocketErrorConnectionFailed
502+
userInfo:@{NSLocalizedDescriptionKey: @"Network.framework is not available on this platform."}];
278503
}
279504
return NO;
505+
#endif
280506
}
281507

282508
- (BOOL)connectToHost:(NSString *)host
@@ -362,7 +588,18 @@ - (BOOL)connectToHost:(NSString *)host
362588
- (void)disconnect {
363589
__weak typeof(self) weakSelf = self;
364590
dispatch_async(self.socketQueue, ^{
365-
[weakSelf disconnectInternalWithError:nil];
591+
__strong typeof(weakSelf) strongSelf = weakSelf;
592+
if (!strongSelf) return;
593+
#if NW_FRAMEWORK_AVAILABLE
594+
// Stop listener if in server mode
595+
if (strongSelf.listener) {
596+
nw_listener_cancel(strongSelf.listener);
597+
strongSelf.listener = nil;
598+
strongSelf.isListening = NO;
599+
strongSelf.localPort = 0;
600+
}
601+
#endif
602+
[strongSelf disconnectInternalWithError:nil];
366603
});
367604
}
368605

@@ -513,10 +750,8 @@ - (void)handleStateChange:(nw_connection_state_t)state error:(nw_error_t _Nullab
513750
if (localHostStr) {
514751
self.localHost = [NSString stringWithUTF8String:localHostStr];
515752
}
516-
const char *localPortStr = nw_endpoint_get_port(localEndpoint);
517-
if (localPortStr) {
518-
self.localPort = (uint16_t)strtoul(localPortStr, NULL, 10);
519-
}
753+
uint16_t localPortValue = nw_endpoint_get_port(localEndpoint);
754+
self.localPort = localPortValue;
520755
}
521756
}
522757
#endif
@@ -720,9 +955,9 @@ - (void)disconnectWithError:(NSError *)error {
720955
}
721956

722957
- (void)disconnectInternalWithError:(NSError *)error {
723-
if (!self.isConnected
958+
if (!self.isConnected && !self.isListening
724959
#if NW_FRAMEWORK_AVAILABLE
725-
&& !self.connection
960+
&& !self.connection && !self.listener
726961
#endif
727962
) {
728963
return;
@@ -732,6 +967,11 @@ - (void)disconnectInternalWithError:(NSError *)error {
732967
self.isReadingContinuously = NO;
733968

734969
#if NW_FRAMEWORK_AVAILABLE
970+
if (self.listener) {
971+
nw_listener_cancel(self.listener);
972+
self.listener = nil;
973+
self.isListening = NO;
974+
}
735975
if (self.connection) {
736976
nw_connection_cancel(self.connection);
737977
self.connection = nil;

ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ typedef NS_ENUM(NSInteger, GCDAsyncSocketError) {
5353
/// Whether the socket is currently disconnected.
5454
@property (atomic, readonly) BOOL isDisconnected;
5555

56+
/// Whether the socket is currently listening for incoming connections (server mode).
57+
@property (atomic, readonly) BOOL isListening;
58+
5659
/// Whether the socket is using a secure TLS transport.
5760
@property (atomic, readonly) BOOL isSecure;
5861

@@ -109,8 +112,17 @@ typedef NS_ENUM(NSInteger, GCDAsyncSocketError) {
109112
error:(NSError **)errPtr;
110113

111114
/// Compatibility API for CocoaAsyncSocket server mode.
115+
/// Listen for incoming TCP connections on all interfaces on the given port.
116+
/// Pass port 0 to let the system assign an available port (query via `localPort`).
112117
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr;
113118

119+
/// Listen for incoming TCP connections on a specific interface/address and port.
120+
/// Pass @"localhost" or @"127.0.0.1" to restrict connections to the local machine.
121+
- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr;
122+
123+
/// Listen for incoming connections on a Unix Domain Socket at the given file URL.
124+
- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr;
125+
114126
// MARK: - Disconnect
115127

116128
/// Disconnect the socket gracefully.

ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ NS_ASSUME_NONNULL_BEGIN
3131
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)error;
3232

3333
@optional
34+
/// Called when a listening socket accepts a new incoming connection.
35+
/// The delegate **must** retain `newSocket` (e.g. add it to an array),
36+
/// otherwise it will be deallocated and the connection will be dropped.
37+
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket;
38+
3439
/// Called when a complete SSE event has been parsed.
3540
- (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event;
3641

0 commit comments

Comments
 (0)