Created
November 6, 2012 14:22
-
-
Save enigmaticape/4024992 to your computer and use it in GitHub Desktop.
Minimal (ish) HTTP server in ObjC using GCD socket dispatch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@interface HTTPMessage : NSObject | |
@property (nonatomic, readonly) CFHTTPMessageRef request; | |
- ( BOOL ) isRequestComplete:(NSData *) append_data; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "HTTPMessage.h" | |
#import "HTTPRequest.h" | |
@implementation HTTPMessage { | |
} | |
@synthesize request = _request; | |
- ( id ) init { | |
if( (self = [super init]) ) { | |
_request = CFHTTPMessageCreateEmpty( NULL, TRUE ); | |
} | |
return self; | |
} | |
- ( BOOL ) isRequestComplete:(NSData *) append_data { | |
CFHTTPMessageAppendBytes( _request, | |
[append_data bytes], | |
[append_data length] ); | |
if( CFHTTPMessageIsHeaderComplete(_request) ) { | |
NSString * content_hdr | |
= [(NSString *)CFHTTPMessageCopyHeaderFieldValue( | |
_request, | |
CFSTR("Content-Length") ) | |
autorelease]; | |
NSData * body = [(NSData *)CFHTTPMessageCopyBody( _request ) | |
autorelease]; | |
int content_length = [content_hdr intValue]; | |
if( [body length] >= content_length ) { | |
return YES; | |
} | |
} | |
return NO; | |
} | |
- ( void ) dealloc { | |
CFRelease(_request); | |
[super dealloc]; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@class HTTPMessage; | |
@interface HTTPRequest : NSObject | |
@property (readonly) NSDictionary * headers; | |
@property (readonly) NSString * method; | |
@property (readonly) NSURL * url; | |
@property (readonly) NSData * body; | |
- ( NSString * ) getBodyAsText; | |
- ( id ) initWithHTTPMessage:( HTTPMessage * ) http_message; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "HTTPRequest.h" | |
#import "HTTPMessage.h" | |
@implementation HTTPRequest | |
@synthesize headers = _headers; | |
@synthesize method = _method; | |
@synthesize url = _url; | |
@synthesize body = _body; | |
- ( id ) initWithHTTPMessage:( HTTPMessage * ) http_message { | |
if( (self = [super init]) ) { | |
CFHTTPMessageRef request = [http_message request]; | |
_headers = (NSDictionary *)CFHTTPMessageCopyAllHeaderFields( request ); | |
_url = (NSURL *)CFHTTPMessageCopyRequestURL( request ); | |
_method = (NSString *)CFHTTPMessageCopyRequestMethod( request ); | |
_body = (NSData *)CFHTTPMessageCopyBody( request ); | |
} | |
return self; | |
} | |
- (NSString*) getBodyAsText { | |
return [[[NSString alloc] initWithData:_body | |
encoding:NSUTF8StringEncoding]autorelease]; | |
}// Hmm, that rather assumes UTF8, doesn't it ? | |
/* | |
De-URLEncode an HTTP POST body | |
NB that this really doesn't belong here, but is supplied for | |
the purposes of demonstration, because there are quite enough | |
classes to be going on with. | |
*/ | |
-( NSDictionary *) urlDecodePostBody { | |
NSMutableDictionary * kvPairs = [[NSMutableDictionary alloc]init]; | |
/* First, translate "+" to " ", then split by & | |
using quite a fugly bit of code, sorry. | |
*/ | |
NSArray * queryComponents | |
= [ [[self getBodyAsText] stringByReplacingOccurrencesOfString:@"+" | |
withString:@" " ] | |
componentsSeparatedByString:@"&" | |
]; | |
/* | |
We replaced '+' signs above because application/x-www-form-urlencoded | |
data (as in the POST body) encodes spaces as '+' | |
instead of %20%. Handy, eh ? | |
*/ | |
for (NSString * kvPairString in queryComponents) { | |
NSArray * keyValuePair = [kvPairString componentsSeparatedByString:@"="]; | |
if( [keyValuePair count] != 2 ) { continue; } | |
/* | |
Similarly, we avoid using the NSString methods here | |
because they don't encode or decode properly. Grr! | |
*/ | |
NSString * decoded_key | |
= [(NSString*)CFURLCreateStringByReplacingPercentEscapes( | |
NULL, | |
(CFStringRef)[keyValuePair objectAtIndex:0], | |
CFSTR("") | |
) | |
autorelease]; | |
NSString * decoded_value | |
= [(NSString*)CFURLCreateStringByReplacingPercentEscapes( | |
NULL, | |
(CFStringRef)[keyValuePair objectAtIndex:1], | |
CFSTR("") | |
) | |
autorelease]; | |
[kvPairs setValue: decoded_value forKey: decoded_key ]; | |
} | |
return [kvPairs autorelease]; | |
} | |
-( void ) dealloc { | |
[ _headers release ]; | |
[ _url release ]; | |
[ _method release ]; | |
[ _body release ]; | |
[ super dealloc ]; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@interface HTTPResponse : NSObject | |
- ( id ) initWithResponseCode:(int) response_code; | |
- ( void ) setHeaderField:(NSString*) header_field | |
toValue:(NSString*) header_value; | |
- ( void ) setAllHeaders:(NSDictionary*) header_dict; | |
- ( void ) setBodyText:(NSString *) body_text; | |
- ( void ) setBodyData:(NSData *) body_data; | |
- ( NSData * ) serialize; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "HTTPResponse.h" | |
@implementation HTTPResponse { | |
CFHTTPMessageRef _response; | |
} | |
- ( id ) initWithResponseCode:(int) response_code { | |
if( (self = [super init]) ) { | |
_response = CFHTTPMessageCreateResponse( | |
NULL, | |
response_code, | |
NULL, | |
kCFHTTPVersion1_1 | |
); | |
} | |
return self; | |
} | |
- ( id ) init { | |
if( (self = [super init]) ) { | |
_response = CFHTTPMessageCreateResponse( | |
NULL, | |
200, | |
NULL, | |
kCFHTTPVersion1_1 | |
); | |
} | |
return self; | |
} | |
- ( void ) setHeaderField:(NSString*) header_field toValue:(NSString*) header_value { | |
CFHTTPMessageSetHeaderFieldValue( | |
_response, | |
(CFStringRef)header_field, | |
(CFStringRef)header_value | |
); | |
} | |
- ( void ) setAllHeaders:(NSDictionary*) header_dict { | |
for( NSString * key in [header_dict allKeys] ) { | |
CFHTTPMessageSetHeaderFieldValue( | |
_response, | |
(CFStringRef)key, | |
(CFStringRef)[header_dict valueForKey:key] | |
); | |
} | |
} | |
- ( void ) setBodyText:(NSString*) body_text { | |
CFHTTPMessageSetHeaderFieldValue( | |
_response, | |
CFSTR("Content-Type"), | |
CFSTR("text/html") | |
); | |
CFHTTPMessageSetBody( | |
_response, | |
(CFDataRef)[body_text dataUsingEncoding:NSUTF8StringEncoding] | |
); | |
} | |
- ( void ) setBodyData:(NSData*) body_data { | |
CFHTTPMessageSetBody( _response, (CFDataRef) body_data ); | |
} | |
- ( NSData * ) serialize { | |
return [(NSData*) | |
CFHTTPMessageCopySerializedMessage( _response ) | |
autorelease]; | |
} | |
- ( void ) dealloc { | |
CFRelease( _response ); | |
[super dealloc]; | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@class HTTPResponse; | |
@interface HttpService : NSObject | |
@property (nonatomic, assign) id responder; | |
- ( void ) startServiceOnPort:(NSUInteger) port; | |
- ( void ) stopService; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "HttpService.h" | |
#import "TCPSocket.h" | |
#import "HTTPRequest.h" | |
#import "HTTPResponse.h" | |
#import "HTTPMessage.h" | |
@implementation HttpService { | |
TCPSocket * _sock; | |
} | |
@synthesize responder = _responder; | |
-( void ) doResponse:( HTTPRequest * ) request | |
onSocket:( NSFileHandle * ) socket | |
{ | |
NSLog(@"%@ %@", request.method, [request.url path]); | |
/* | |
Here, we're going to check for a matching method signature | |
along the lines of : | |
- ( HTTPResponse * ) POST:( HTTPRequest * ) request | |
(Or, GET ... or whatever other HTTP request methods | |
you want to support) | |
On the responder class. If it exists we'll call it with the | |
relevant parameters, if not, we'll send a '405, huh ?' message. | |
*/ | |
HTTPResponse * response = nil; | |
NSString * method_signature | |
= [NSString stringWithFormat:@"%@:",request.method]; | |
SEL selector = NSSelectorFromString( method_signature ); | |
if( [_responder respondsToSelector:selector] ) { | |
response = [_responder performSelector:selector | |
withObject:request]; | |
} | |
else { | |
/* | |
If we get a request we don't understand, we should at | |
least tip our hat towards the standards and send a | |
'not supported' response | |
*/ | |
response = [[[HTTPResponse alloc] initWithResponseCode:405] | |
autorelease]; | |
[response setBodyText:[NSString stringWithFormat:@"%@ method not supported", | |
request.method]]; | |
} | |
[socket writeData:[response serialize]]; | |
} | |
/* | |
- ( void ) sendResponse:( HTTPResponse * ) response | |
onSocket:( NSFileHandle * ) socket | |
{ | |
[socket writeData:[response serialize]]; | |
} | |
*/ | |
/* Now for the meat in this sandwich! | |
When we set our TCP socket to listen for incoming | |
connections, we will set a block to execute the | |
following method, passing its instance here | |
so that we can call accept ... | |
*/ | |
- ( void ) gotConnectionOnSocket:(TCPSocket*) sock { | |
NSLog(@"Connect"); | |
/* | |
... which gives us a live, connected socket that | |
we can talk to ... | |
*/ | |
TCPSocket * live = [sock accept]; | |
/* | |
... which we set up as another dispatch source ... | |
*/ | |
dispatch_source_t source = dispatch_source_create( | |
DISPATCH_SOURCE_TYPE_READ, | |
live.socket, | |
0, | |
dispatch_get_global_queue(0, 0) | |
); | |
__block HTTPMessage * http_message = [[HTTPMessage alloc]init] ; | |
__block NSFileHandle * sock_handle | |
= [[NSFileHandle alloc] initWithFileDescriptor:live.socket]; | |
/* every time our socket gets some data, run a block to | |
process it. When the request transmission is complete, | |
we dispatch another block to handle responding. | |
*/ | |
dispatch_source_set_event_handler( source, ^{ | |
if( [http_message isRequestComplete: [sock_handle availableData]] ) { | |
dispatch_source_cancel( source ); | |
dispatch_release( source ); | |
HTTPRequest * http_request | |
= [[HTTPRequest alloc] initWithHTTPMessage:http_message]; | |
[http_message release]; | |
dispatch_async( dispatch_get_global_queue( 0, 0 ), ^{ | |
[self doResponse:http_request onSocket:sock_handle]; | |
[http_request release]; | |
[sock_handle closeFile]; | |
[sock_handle release]; | |
}); | |
} // if( [http_message ... | |
} ); // dispatch_source ... | |
dispatch_resume( source ); | |
} | |
/* | |
And so now, starting our weeny web server is as easy | |
as calling this method with a port number. | |
Erm, probably should have built in a mechanism to stop. | |
*/ | |
- ( void ) startServiceOnPort:(NSUInteger) port { | |
_sock = [[TCPSocket alloc] initWithPort:port]; | |
[_sock listen:^{ [self gotConnectionOnSocket:_sock];}]; | |
} | |
- ( void ) stopService { | |
[_sock stopDispatch]; | |
} | |
@end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
#import <sys/socket.h> | |
#import <netinet/in.h> | |
@interface TCPSocket : NSObject | |
@property (readonly) NSUInteger port; | |
@property (readonly) int socket; | |
- ( NSUInteger ) listen:(dispatch_block_t) block; | |
- ( id ) initWithPort:( uint16_t ) port; | |
- ( TCPSocket * ) accept; | |
- ( void ) stopDispatch; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "TCPSocket.h" | |
@implementation TCPSocket { | |
int _sock_ref; | |
NSUInteger _port; | |
dispatch_source_t _source; | |
} | |
@synthesize port = _port; | |
@synthesize socket = _sock_ref; | |
/* | |
Use the standard low level TCP sockets API to create | |
and bind a TCP socket | |
*/ | |
- ( id ) initWithPort:( uint16_t ) port { | |
if( (self = [super init]) ) { | |
_sock_ref = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ); | |
struct sockaddr_in addr = { | |
sizeof(addr), | |
AF_INET, | |
htons(port), | |
{ INADDR_ANY }, | |
{ 0 } | |
}; | |
int yes = 1; | |
setsockopt( | |
_sock_ref, | |
SOL_SOCKET, | |
SO_REUSEADDR, | |
(void *)&yes, | |
sizeof(yes) | |
); | |
bind( _sock_ref, (void *)&addr, sizeof(addr) ); | |
_port = port; // NB that if port == 0, this will be true until listen() | |
// is called, at which point the actual port number is set. | |
} | |
return self; | |
} | |
/* | |
Call the TCP sockets 'listen' to make the socket, er, listen, | |
on a port. So far so standard. But after that we set it as a | |
GCD dispatch source ... | |
Returns the number of the port. | |
*/ | |
- ( NSUInteger ) listen:( dispatch_block_t ) block { | |
listen( _sock_ref, 2 ); | |
struct sockaddr_in addr; | |
unsigned int addrlen = sizeof( addr ); | |
getsockname( _sock_ref, (struct sockaddr*)&addr, &addrlen ); | |
_port = ntohs(addr.sin_port); | |
NSLog(@"Listening on %lu", _port); | |
/* | |
... set the listening socket to be a GCD dispatch source, | |
so that when it recieves data it will dispatch it using | |
the block we passed as a parameter. | |
*/ | |
_source = dispatch_source_create( | |
DISPATCH_SOURCE_TYPE_READ, | |
_sock_ref, | |
0, | |
dispatch_get_global_queue(0, 0) | |
); | |
dispatch_source_set_event_handler( _source, block ); | |
dispatch_resume( _source ); | |
return _port; | |
} | |
/* | |
Wrap a native socket reference in a TCPSocket class | |
*/ | |
- ( id ) initWithNativeSocket:(int) socket | |
fromTCPSocket:(TCPSocket *) tcp_sock | |
{ | |
if( (self = [super init]) ) { | |
_sock_ref = socket; | |
_port = tcp_sock.port; | |
} | |
return self; | |
} | |
/* | |
Called by client if it wishes to accept a connection | |
indicated by a dispatch from this object. | |
Returns a listening socket instance that can read and written. | |
*/ | |
- ( TCPSocket * ) accept { | |
struct sockaddr addr; | |
socklen_t addrlen = sizeof(addr); | |
int listening_sock = accept(_sock_ref, &addr, &addrlen); | |
TCPSocket * listening_tcp_sock | |
= [[TCPSocket alloc] initWithNativeSocket:listening_sock | |
fromTCPSocket:self]; | |
return [listening_tcp_sock autorelease]; | |
} | |
- ( void ) stopDispatch { | |
dispatch_source_cancel( _source ); | |
dispatch_release( _source ); | |
close( _sock_ref ); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This code is the demo code that I used to answer the question "Roughly how much Objective C do we have to write to get a minimally functional HTTP service going ?". From the Enigmatic Ape blog at http://www.enigmaticape.com/programming/a-minimalish-objective-c-http-server/