Browse Source

Removed dependency on CFSocket to be 100% GCD based

Pierre-Olivier Latour 12 years ago
parent
commit
a557080a07

+ 3 - 4
CGDWebServer/GCDWebServer.h

@@ -36,8 +36,7 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
   NSMutableArray* _handlers;
   
   NSUInteger _port;
-  NSRunLoop* _runLoop;
-  CFSocketRef _socket;
+  dispatch_source_t _source;
   CFNetServiceRef _service;
 }
 @property(nonatomic, readonly, getter=isRunning) BOOL running;
@@ -45,8 +44,8 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
 - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
 - (void)removeAllHandlers;
 
-- (BOOL)start;  // Default is main runloop, 8080 port and computer name
-- (BOOL)startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name;  // Pass nil name to disable Bonjour or empty string to use computer name
+- (BOOL)start;  // Default is 8080 port and computer name
+- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name;  // Pass nil name to disable Bonjour or empty string to use computer name
 - (void)stop;
 @end
 

+ 92 - 68
CGDWebServer/GCDWebServer.m

@@ -34,6 +34,8 @@
 
 #import "GCDWebServerPrivate.h"
 
+#define kMaxPendingConnections 16
+
 static BOOL _run;
 
 NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension) {
@@ -134,7 +136,7 @@ static void _SignalHandler(int signal) {
 }
 
 - (void)dealloc {
-  if (_runLoop) {
+  if (_source) {
     [self stop];
   }
   
@@ -144,19 +146,19 @@ static void _SignalHandler(int signal) {
 }
 
 - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
-  DCHECK(_runLoop == nil);
+  DCHECK(_source == NULL);
   GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
   [_handlers insertObject:handler atIndex:0];
   [handler release];
 }
 
 - (void)removeAllHandlers {
-  DCHECK(_runLoop == nil);
+  DCHECK(_source == NULL);
   [_handlers removeAllObjects];
 }
 
 - (BOOL)start {
-  return [self startWithRunloop:[NSRunLoop mainRunLoop] port:8080 bonjourName:@""];
+  return [self startWithPort:8080 bonjourName:@""];
 }
 
 static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
@@ -169,29 +171,13 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
   }
 }
 
-static void _SocketCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) {
-  if (type == kCFSocketAcceptCallBack) {
-    CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
-    int set = 1;
-    setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));  // Make sure this socket cannot generate SIG_PIPE
-    @autoreleasepool {
-      Class class = [[(GCDWebServer*)info class] connectionClass];
-      GCDWebServerConnection* connection = [[class alloc] initWithServer:(GCDWebServer*)info address:(NSData*)address socket:handle];
-      [connection release];  // Connection will automatically retain itself while opened
-    }
-  } else {
-    DNOT_REACHED();
-  }
-}
-
-- (BOOL)startWithRunloop:(NSRunLoop*)runloop port:(NSUInteger)port bonjourName:(NSString*)name {
-  DCHECK(runloop);
-  DCHECK(_runLoop == nil);
-  CFSocketContext context = {0, self, NULL, NULL, NULL};
-  _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, _SocketCallBack, &context);
-  if (_socket) {
+- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
+  DCHECK(_source == NULL);
+  int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
+  if (listeningSocket > 0) {
     int yes = 1;
-    setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+    setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+    setsockopt(listeningSocket, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
     
     struct sockaddr_in addr4;
     bzero(&addr4, sizeof(addr4));
@@ -199,66 +185,104 @@ static void _SocketCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDat
     addr4.sin_family = AF_INET;
     addr4.sin_port = htons(port);
     addr4.sin_addr.s_addr = htonl(INADDR_ANY);
-    if (CFSocketSetAddress(_socket, (CFDataRef)[NSData dataWithBytes:&addr4 length:sizeof(addr4)]) == kCFSocketSuccess) {
-      CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
-      CFRunLoopAddSource([runloop getCFRunLoop], source, kCFRunLoopCommonModes);
-      if (port == 0) {  // Determine the actual port we are listening on
-        CFDataRef addressData = CFSocketCopyAddress(_socket);
-        struct sockaddr_in* sockaddr = (struct sockaddr_in*)CFDataGetBytePtr(addressData);
-        DCHECK(sockaddr);
-        _port = ntohs(sockaddr->sin_port);
-        CFRelease(addressData);
-      } else {
-        _port = port;
-      }
-      CFRelease(source);
-      
-      if (name) {
-        _service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (CFStringRef)name, _port);
-        if (_service) {
-          CFNetServiceClientContext context = {0, self, NULL, NULL, NULL};
-          CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
-          CFNetServiceScheduleWithRunLoop(_service, [runloop getCFRunLoop], kCFRunLoopCommonModes);
-          CFStreamError error = {0};
-          CFNetServiceRegisterWithOptions(_service, 0, &error);
+    if (bind(listeningSocket, (void*)&addr4, sizeof(addr4)) == 0) {
+      if (listen(listeningSocket, kMaxPendingConnections) == 0) {
+        _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listeningSocket, 0, kGCDWebServerGCDQueue);
+        dispatch_source_set_cancel_handler(_source, ^{
+          
+          @autoreleasepool {
+            int result = close(listeningSocket);
+            if (result != 0) {
+              LOG_ERROR(@"Failed closing socket (%i): %s", errno, strerror(errno));
+            } else {
+              LOG_DEBUG(@"Closed listening socket");
+            }
+          }
+          
+        });
+        dispatch_source_set_event_handler(_source, ^{
+          
+          @autoreleasepool {
+            struct sockaddr addr;
+            socklen_t addrlen = sizeof(addr);
+            int socket = accept(listeningSocket, &addr, &addrlen);
+            if (socket > 0) {
+              int yes = 1;
+              setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes));  // Make sure this socket cannot generate SIG_PIPE
+              
+              NSData* data = [NSData dataWithBytes:&addr length:addrlen];
+              Class connectionClass = [[self class] connectionClass];
+              GCDWebServerConnection* connection = [[connectionClass alloc] initWithServer:self address:data socket:socket];
+              [connection release];  // Connection will automatically retain itself while opened
+            } else {
+              LOG_ERROR(@"Failed accepting socket (%i): %s", errno, strerror(errno));
+            }
+          }
+          
+        });
+        
+        if (port == 0) {  // Determine the actual port we are listening on
+          struct sockaddr addr;
+          socklen_t addrlen = sizeof(addr);
+          if (getsockname(listeningSocket, &addr, &addrlen) == 0) {
+            struct sockaddr_in* sockaddr = (struct sockaddr_in*)&addr;
+            _port = ntohs(sockaddr->sin_port);
+          } else {
+            LOG_ERROR(@"Failed retrieving socket address (%i): %s", errno, strerror(errno));
+          }
         } else {
-          LOG_ERROR(@"Failed creating CFNetService");
+          _port = port;
         }
+        
+        if (name) {
+          _service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (CFStringRef)name, _port);
+          if (_service) {
+            CFNetServiceClientContext context = {0, self, NULL, NULL, NULL};
+            CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
+            CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
+            CFStreamError error = {0};
+            CFNetServiceRegisterWithOptions(_service, 0, &error);
+          } else {
+            LOG_ERROR(@"Failed creating CFNetService");
+          }
+        }
+        
+        dispatch_resume(_source);
+        LOG_VERBOSE(@"%@ started on port %i", [self class], (int)_port);
+      } else {
+        LOG_ERROR(@"Failed listening on socket (%i): %s", errno, strerror(errno));
+        close(listeningSocket);
       }
-      
-      _runLoop = [runloop retain];
-      LOG_VERBOSE(@"%@ started on port %i", [self class], (int)_port);
     } else {
-      LOG_ERROR(@"Failed binding socket");
-      CFRelease(_socket);
-      _socket = NULL;
+      LOG_ERROR(@"Failed binding socket (%i): %s", errno, strerror(errno));
+      close(listeningSocket);
     }
   } else {
-    LOG_ERROR(@"Failed creating CFSocket");
+    LOG_ERROR(@"Failed creating socket (%i): %s", errno, strerror(errno));
   }
-  return (_runLoop != nil ? YES : NO);
+  return (_source ? YES : NO);
 }
 
 - (BOOL)isRunning {
-  return (_runLoop != nil ? YES : NO);
+  return (_source ? YES : NO);
 }
 
 - (void)stop {
-  DCHECK(_runLoop != nil);
-  if (_socket) {
+  DCHECK(_source != NULL);
+  if (_source) {
     if (_service) {
-      CFNetServiceUnscheduleFromRunLoop(_service, [_runLoop getCFRunLoop], kCFRunLoopCommonModes);
+      CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
       CFNetServiceSetClient(_service, NULL, NULL);
       CFRelease(_service);
+      _service = NULL;
     }
     
-    CFSocketInvalidate(_socket);
-    CFRelease(_socket);
-    _socket = NULL;
+    dispatch_source_cancel(_source);  // This will close the socket
+    dispatch_release(_source);
+    _source = NULL;
+    
     LOG_VERBOSE(@"%@ stopped", [self class]);
   }
-  [_runLoop release];
-  _runLoop = nil;
   _port = 0;
 }
 
@@ -283,9 +307,9 @@ static void _SocketCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDat
   _run = YES;
   void* handler = signal(SIGINT, _SignalHandler);
   if (handler != SIG_ERR) {
-    if ([self startWithRunloop:[NSRunLoop currentRunLoop] port:port bonjourName:@""]) {
+    if ([self startWithPort:port bonjourName:@""]) {
       while (_run) {
-        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
+        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
       }
       [self stop];
       success = YES;

+ 6 - 4
CGDWebServer/GCDWebServerConnection.m

@@ -27,7 +27,6 @@
 
 #import "GCDWebServerPrivate.h"
 
-#define kReadWriteQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
 #define kHeadersReadBuffer 1024
 #define kBodyWriteBufferSize (32 * 1024)
 
@@ -49,7 +48,7 @@ static dispatch_queue_t _formatterQueue = NULL;
 @implementation GCDWebServerConnection (Read)
 
 - (void)_readBufferWithLength:(NSUInteger)length completionBlock:(ReadBufferCompletionBlock)block {
-  dispatch_read(_socket, length, kReadWriteQueue, ^(dispatch_data_t buffer, int error) {
+  dispatch_read(_socket, length, kGCDWebServerGCDQueue, ^(dispatch_data_t buffer, int error) {
     
     @autoreleasepool {
       if (error == 0) {
@@ -173,7 +172,7 @@ static dispatch_queue_t _formatterQueue = NULL;
 
 - (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
   size_t size = dispatch_data_get_size(buffer);
-  dispatch_write(_socket, buffer, kReadWriteQueue, ^(dispatch_data_t data, int error) {
+  dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) {
     
     @autoreleasepool {
       if (error == 0) {
@@ -483,7 +482,10 @@ static dispatch_queue_t _formatterQueue = NULL;
 }
 
 - (void)close {
-  close(_socket);
+  int result = close(_socket);
+  if (result != 0) {
+    LOG_ERROR(@"Failed closing socket %i for connection (%i): %s", _socket, errno, strerror(errno));
+  }
   LOG_DEBUG(@"Did close connection on socket %i", _socket);
 }
 

+ 1 - 0
CGDWebServer/GCDWebServerPrivate.h

@@ -79,6 +79,7 @@ static inline void __LogMessage(long level, NSString* format, ...) {
 #endif
 
 #define kGCDWebServerDefaultMimeType @"application/octet-stream"
+#define kGCDWebServerGCDQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
 
 #ifdef __cplusplus
 extern "C" {

+ 1 - 1
README.md

@@ -83,7 +83,7 @@ You start by creating an instance of the 'GCDWebServer' class. Note that you can
 
 Then you add one or more "handlers" to the server: each handler gets a chance to handle an incoming web request and provide a response. Handlers are called in a LIFO queue, so the latest added handler overrides any previously added ones.
 
-Finally you start the server on a given port. Note that even if built on GCD, GCDWebServer still requires a runloop to be around (by default the main thread runloop is used). This is because there is no CGD API at this point to handle listening sockets, so it must be done using CFSocket which requires a runloop. However, the runloop is only used to accept the connection: immediately afterwards, the connection handling is dispatched to GCD queues.
+Finally you start the server on a given port.
 
 Understanding GCDWebServer Architecture
 =======================================