diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index 9df1d6a..4b23260 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -69,6 +69,19 @@ typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, */ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request); +/** + * The GCDWebServerAsynchronousProcessBlock works like the GCDWebServerProcessBlock + * except the GCDWebServerResponse can be returned to the server at a later time + * allowing for asynchronous generation of the response. + * + * The block must eventually call "completionBlock" passing a GCDWebServerResponse + * or nil on error, which will result in a 500 HTTP status code returned to the client. + * It's however recommended to return a GCDWebServerErrorResponse on error so more + * useful information can be returned to the client. + */ +typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* response); +typedef void (^GCDWebServerAsyncProcessBlock)(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock); + /** * The port used by the GCDWebServer (NSNumber / NSUInteger). * @@ -289,7 +302,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; - (instancetype)init; /** - * Adds a handler to the server to handle incoming HTTP requests. + * Adds to the server a handler that generates responses synchronously when handling incoming HTTP requests. * * Handlers are called in a LIFO queue, so if multiple handlers can potentially * respond to a given request, the latest added one wins. @@ -298,6 +311,16 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; */ - (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; +/** + * Adds to the server a handler that generates responses asynchronously when handling incoming HTTP requests. + * + * Handlers are called in a LIFO queue, so if multiple handlers can potentially + * respond to a given request, the latest added one wins. + * + * @warning Addling handlers while the server is running is not allowed. + */ +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock; + /** * Removes all handlers previously added to the server. * @@ -392,22 +415,44 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; /** * Adds a default handler to the server to handle all incoming HTTP requests - * with a given HTTP method. + * with a given HTTP method and generate responses synchronously. */ - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; +/** + * Adds a default handler to the server to handle all incoming HTTP requests + * with a given HTTP method and generate responses asynchronously. + */ +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + /** * Adds a handler to the server to handle incoming HTTP requests with a given - * HTTP method and a specific case-insensitive path. + * HTTP method and a specific case-insensitive path and generate responses + * synchronously. */ - (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; /** * Adds a handler to the server to handle incoming HTTP requests with a given - * HTTP method and a path matching a case-insensitive regular expression. + * HTTP method and a specific case-insensitive path and generate responses + * asynchronously. + */ +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a path matching a case-insensitive regular expression and + * generate responses synchronously. */ - (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; +/** + * Adds a handler to the server to handle incoming HTTP requests with a given + * HTTP method and a path matching a case-insensitive regular expression and + * generate responses asynchronously. + */ +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block; + @end @interface GCDWebServer (GETHandlers) diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index 1e9ddae..5e06936 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -114,25 +114,25 @@ static void _ExecuteMainThreadRunLoopSources() { @interface GCDWebServerHandler () { @private GCDWebServerMatchBlock _matchBlock; - GCDWebServerProcessBlock _processBlock; + GCDWebServerAsyncProcessBlock _asyncProcessBlock; } @end @implementation GCDWebServerHandler -@synthesize matchBlock=_matchBlock, processBlock=_processBlock; +@synthesize matchBlock=_matchBlock, asyncProcessBlock=_asyncProcessBlock; -- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock { +- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock { if ((self = [super init])) { _matchBlock = [matchBlock copy]; - _processBlock = [processBlock copy]; + _asyncProcessBlock = [processBlock copy]; } return self; } - (void)dealloc { ARC_RELEASE(_matchBlock); - ARC_RELEASE(_processBlock); + ARC_RELEASE(_asyncProcessBlock); ARC_DEALLOC(super); } @@ -345,9 +345,15 @@ static void _ExecuteMainThreadRunLoopSources() { return type && CFStringGetLength(type) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil; } -- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock { +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock { + [self addHandlerWithMatchBlock:matchBlock asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(processBlock(request)); + }]; +} + +- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock { DCHECK(_options == nil); - GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock]; + GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock]; [_handlers insertObject:handler atIndex:0]; ARC_RELEASE(handler); } @@ -754,6 +760,12 @@ static inline NSString* _EncodeBase64(NSString* string) { @implementation GCDWebServer (Handlers) - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addDefaultHandlerForMethod:method requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { if (![requestMethod isEqualToString:method]) { @@ -761,10 +773,16 @@ static inline NSString* _EncodeBase64(NSString* string) { } return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]); - } processBlock:block]; + } asyncProcessBlock:block]; } - (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addHandlerForMethod:method path:path requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { @@ -776,13 +794,19 @@ static inline NSString* _EncodeBase64(NSString* string) { } return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]); - } processBlock:block]; + } asyncProcessBlock:block]; } else { DNOT_REACHED(); } } - (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { + [self addHandlerForMethod:method pathRegex:regex requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { + completionBlock(block(request)); + }]; +} + +- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL]; if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { @@ -808,7 +832,7 @@ static inline NSString* _EncodeBase64(NSString* string) { [request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures]; return ARC_AUTORELEASE(request); - } processBlock:block]; + } asyncProcessBlock:block]; } else { DNOT_REACHED(); } diff --git a/GCDWebServer/Core/GCDWebServerConnection.h b/GCDWebServer/Core/GCDWebServerConnection.h index 338c8e2..68e6ac9 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.h +++ b/GCDWebServer/Core/GCDWebServerConnection.h @@ -138,13 +138,14 @@ /** * Assuming a valid HTTP request was received and -preflightRequest: returned nil, - * this method is called to process the request. + * this method is called to process the request by executing the handler's + * process block. */ -- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; +- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion; /** * Assuming a valid HTTP request was received and either -preflightRequest: - * or -processRequest:withBlock: returned a non-nil GCDWebServerResponse, + * or -processRequest:completion: returned a non-nil GCDWebServerResponse, * this method is called to override the response. * * You can either modify the current response and return it, or return a diff --git a/GCDWebServer/Core/GCDWebServerConnection.m b/GCDWebServer/Core/GCDWebServerConnection.m index a8e447b..32c23bd 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.m +++ b/GCDWebServer/Core/GCDWebServerConnection.m @@ -373,15 +373,24 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822([NSDate date])); } +- (void)_startProcessingRequest { + DCHECK(_responseMessage == NULL); + + GCDWebServerResponse* preflightResponse = [self preflightRequest:_request]; + if (preflightResponse) { + [self _finishProcessingRequest:preflightResponse]; + } else { + [self processRequest:_request completion:^(GCDWebServerResponse* processResponse) { + [self _finishProcessingRequest:processResponse]; + }]; + } +} + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html -- (void)_processRequest { +- (void)_finishProcessingRequest:(GCDWebServerResponse*)response { DCHECK(_responseMessage == NULL); BOOL hasBody = NO; - GCDWebServerResponse* response = [self preflightRequest:_request]; - if (!response) { - response = [self processRequest:_request withBlock:_handler.processBlock]; - } if (response) { response = [self overrideResponse:response forRequest:_request]; } @@ -471,7 +480,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* localError = nil; if ([_request performClose:&localError]) { - [self _processRequest]; + [self _startProcessingRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; @@ -480,7 +489,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } else { if ([_request performClose:&error]) { - [self _processRequest]; + [self _startProcessingRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; @@ -501,7 +510,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSError* localError = nil; if ([_request performClose:&localError]) { - [self _processRequest]; + [self _startProcessingRequest]; } else { LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; @@ -572,7 +581,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest]; } } else { - [self _processRequest]; + [self _startProcessingRequest]; } } else { _request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery]; @@ -772,16 +781,14 @@ static NSString* _StringFromAddressData(NSData* data) { return response; } -- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block { +- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion { LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead); - GCDWebServerResponse* response = nil; @try { - response = block(request); + _handler.asyncProcessBlock(request, completion); } @catch (NSException* exception) { LOG_EXCEPTION(exception); } - return response; } // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 diff --git a/GCDWebServer/Core/GCDWebServerPrivate.h b/GCDWebServer/Core/GCDWebServerPrivate.h index 03668a6..7fe57fc 100644 --- a/GCDWebServer/Core/GCDWebServerPrivate.h +++ b/GCDWebServer/Core/GCDWebServerPrivate.h @@ -143,8 +143,7 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F @interface GCDWebServerHandler : NSObject @property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock; -@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock; -- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock; +@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock; @end @interface GCDWebServerRequest () diff --git a/Mac/main.m b/Mac/main.m index d88310f..552d1b7 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -52,6 +52,7 @@ typedef enum { kMode_WebDAV, kMode_WebUploader, kMode_StreamingResponse, + kMode_AsyncResponse } Mode; @interface Delegate : NSObject @@ -142,7 +143,7 @@ int main(int argc, const char* argv[]) { NSString* authenticationPassword = nil; if (argc == 1) { - fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0])); + fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse | asyncResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0])); } else { for (int i = 1; i < argc; ++i) { if (argv[i][0] != '-') { @@ -164,6 +165,8 @@ int main(int argc, const char* argv[]) { mode = kMode_WebUploader; } else if (!strcmp(argv[i], "streamingResponse")) { mode = kMode_StreamingResponse; + } else if (!strcmp(argv[i], "asyncResponse")) { + mode = kMode_AsyncResponse; } } else if (!strcmp(argv[i], "-record")) { recording = YES; @@ -328,6 +331,24 @@ int main(int argc, const char* argv[]) { break; } + // Test async responses + case kMode_AsyncResponse: { + fprintf(stdout, "Running in Async Response mode"); + webServer = [[GCDWebServer alloc] init]; + [webServer addHandlerForMethod:@"GET" + path:@"/" + requestClass:[GCDWebServerRequest class] + asyncProcessBlock:^(GCDWebServerRequest *request, GCDWebServerCompletionBlock completionBlock) { + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"]; + completionBlock(response); + }); + + }]; + break; + } + } #if __has_feature(objc_arc) fprintf(stdout, " (ARC is ON)\n");