Skip to content

Instantly share code, notes, and snippets.

@enigmaticape
Created November 6, 2012 14:22
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save enigmaticape/4024992 to your computer and use it in GitHub Desktop.
Save enigmaticape/4024992 to your computer and use it in GitHub Desktop.
Minimal (ish) HTTP server in ObjC using GCD socket dispatch
#import <Foundation/Foundation.h>
@interface HTTPMessage : NSObject
@property (nonatomic, readonly) CFHTTPMessageRef request;
- ( BOOL ) isRequestComplete:(NSData *) append_data;
@end
#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
#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
#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
#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
#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
#import <Foundation/Foundation.h>
@class HTTPResponse;
@interface HttpService : NSObject
@property (nonatomic, assign) id responder;
- ( void ) startServiceOnPort:(NSUInteger) port;
- ( void ) stopService;
@end
#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
#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
#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
@enigmaticape
Copy link
Author

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment