HTTP is one of the simpler protocols to implement for communication between computers. On the iPhone, since there are no APIs for data synchronization or file sharing, embedding an HTTP server is one of the best ways to transfer data from your iPhone application to a computer. In this post I'll show you how to write your own simple but extensible HTTP server. The server classes will also work on Mac OS X (Cocoa un-Touched).
Introduction
In this post I will present the following sample application:
The application is very simple: you can edit text and save it to a file (it is always saved to the same file).
While the application is running, it also runs an HTTP server on port 8080. If the path "/" is requested, it will return the content of the saved file. All other requests return a 501 error.
To transfer the text file from the iPhone application, just enter the IP address of the phone followed by ":8080" in any web browser.
HTTPServer and HTTPResponseHandler classes
The approach I use for an HTTP server involves two classes: the server (which listens for connections and reads data up to the end of the HTTP header) and the response handler (which sends the response and may choose to read from the connection past the header).
The key design choice for me was simplicity of each new response implementation: the server and response classes are designed so that a new response implementation need only implement three methods:
canHandleRequest:method:url:headerFields:
— to decide if the implementation can handle a specific requeststartResponse
— to begin writing (or completely write) the responseload
— all subclasses should implement the standard+[NSObject load]
method to register themselves with the base class
It is just a tiny HTTP server but the approach should allow you to quickly integrate HTTP communications into any application.
Opening a socket for listening
Most server communications, HTTP included, begin by creating a socket for listening.
Sockets in Cocoa can be created and configured entirely using the BSD sockets code but it's often marginally easier to use the CoreFoundationCFSocket
API where possible. Unfortunately, that only makes it marginally easier — we still have a large block of boilerplate code to throw down just to open a socket.
From the -[HTTPServer start]
method:
socket = CFSocketCreate(kCFAllocatorDefault,
PF_INET
,
SOCK_STREAM
,
IPPROTO_TCP
,
0
,
NULL
,
NULL
);
if
(!socket)
{
[
self
errorWithName
:
@"Unable to create socket."
];
return
;
}
int
reuse = true;
int
fileDescriptor = CFSocketGetNative(socket);
if
(setsockopt(fileDescriptor,
SOL_SOCKET
,
SO_REUSEADDR
,
(
void
*)&reuse, sizeof(
int
)) !=
0
)
{
[
self
errorWithName
:
@"Unable to set socket options."
];
return
;
}
struct
sockaddr_in address;
memset(&address,
0
, sizeof(address));
address
.sin_len
= sizeof(address);
address
.sin_family
=
AF_INET
;
address
.sin_addr
.s_addr
= htonl(INADDR_ANY);
address
.sin_port
= htons(HTTP_SERVER_PORT);
CFDataRef
addressData =
CFDataCreate(
NULL
, (
const
UInt8
*)&address, sizeof(address));
[(
id
)addressData
autorelease
];
if
(CFSocketSetAddress(socket, addressData) != kCFSocketSuccess)
{
[
self
errorWithName
:
@"Unable to bind socket to address."
];
return
;
}
|
This is a large block of code but it's really only doing one thing: opening a socket to listen for TCP connections on the port specified byHTTP_SERVER_PORT
(which is 8080 for this application).
There is some additional work because I like to specify SO_REUSEADDR
. This lets us reclaim the port if it is open but idle (a common occurrence if we restart the program immediately after a crash or killing the application).
Receiving incoming connections
After the socket is setup, Cocoa handles a little more of the work so things get easier.
We can receive each incoming connection by constructing an NSFileHandle
from thefileDescriptor
above and listening for connection notifications
From the -[HTTPServer start:]
method (immediately below the previous code):
listeningHandle = [[
NSFileHandle
alloc
]
initWithFileDescriptor
:fileDescriptor
closeOnDealloc
:
YES
];
[[
NSNotificationCenter
defaultCenter
]
addObserver
:
self
selector
:
@selector
(receiveIncomingConnectionNotification:)
name
:NSFileHandleConnectionAcceptedNotification
object
:nil
];
[listeningHandle
acceptConnectionInBackgroundAndNotify
];
|
When receiveIncomingConnectionNotification:
is invoked, each new incoming connection will get its ownNSFileHandle
. If you're keeping track, that was:
- 1 file handle (
listeningHandle
) manually created from the socketfileDesriptor
to listen on the socket for new connections. - 1 file handle automatically created for each new connection received through
listeningHandle
. We'll continue to listen to these new handles (the keys in theincomingRequests
dictionary) to record the data for each connection.
So, now that we've received a new, automatically created file handle, we create aCFHTTPMessageRef
(which will store the incoming data) we receive over the file handle. We store these as the objects inincomingRequests
dictionary to allow easy access to the CFHTTPMessageRef
for each file handle
The CFHTTPMessageRef
is both storage and the parser for the incoming data. We can invokeCFHTTPMessageIsHeaderComplete()
every time we add data to check when the HTTP headers are complete and we can spawn a response handler.
The response handler is spawned in the -[HTTPServer receiveIncomingDataNotification:]
method:
if
(CFHTTPMessageIsHeaderComplete(incomingRequest))
{
HTTPResponseHandler
*handler =
[HTTPResponseHandler
handlerForRequest
:incomingRequest
fileHandle
:incomingFileHandle
server
:
self
];
[responseHandlers
addObject
:handler];
[
self
stopReceivingForFileHandle
:incomingFileHandle
close
:
NO
];
[handler
startResponse
];
return
;
}
|
The server stops listening to the file handle for the connection at this point but it doesn't close it, since the file handle is passed to theHTTPResponseHandler
so that the HTTP response can be sent back over the same file handle.
Flexible response handling
Exactly which subclass the +[HTTPResponseHandler handlerForRequest:fileHandle:server:]
method chooses to return determines the entire content of the response. It does this by iterating over a priority ordered array of the registered handlers and asking each one if it wants to handle the request.
+ (Class)handlerClassForRequest:(CFHTTPMessageRef)aRequest
method
:(
NSString
*)requestMethod
url
:(
NSURL
*)requestURL
headerFields
:(
NSDictionary
*)requestHeaderFields
{
for
(
Class
handlerClass
in
registeredHandlers)
{
if
([handlerClass
canHandleRequest
:aRequest
method
:requestMethod
url
:requestURL
headerFields
:requestHeaderFields])
{
return
handlerClass;
}
}
return
nil
;
}
|
For this to work, all HTTPResponseHandler
s need to be registered with the base class. The easiest way to do this is to add an implementation of the+[NSObject load]
method to every subclass:
+ (
void
)load
{
[
HTTPResponseHandler
registerHandler
:
self
];
}
|
In the sample application, the only response handler other than the default is theAppTextFileResponse
. This class chooses to handle the response when therequestURL
is equal to "/".
From the AppTextFileResponse
class:
+ (
BOOL
)canHandleRequest:(CFHTTPMessageRef)aRequest
method
:(
NSString
*)requestMethod
url
:(
NSURL
*)requestURL
headerFields
:(
NSDictionary
*)requestHeaderFields
{
if
([requestURL
.path
isEqualToString
:
@"/"
])
{
return
YES
;
}
return
NO
;
}
|
AppTextFileResponse
then handles the entire response synchronously (before returning from thestartResponse
method) by writing the text file saved by the application as the response body.
- (
void
)startResponse
{
NSData
*fileData =
[
NSData
dataWithContentsOfFile
:[
AppTextFileResponse
pathForFile
]];
CFHTTPMessageRef
response =
CFHTTPMessageCreateResponse(
kCFAllocatorDefault,
2
0
0
,
NULL
, kCFHTTPVersion
1
_
1
);
CFHTTPMessageSetHeaderFieldValue(
response, (CFStringRef)
@"Content-Type"
, (CFStringRef)
@"text/plain"
);
CFHTTPMessageSetHeaderFieldValue(
response, (CFStringRef)
@"Connection"
, (CFStringRef)
@"close"
);
CFHTTPMessageSetHeaderFieldValue(
response,
(CFStringRef)
@"Content-Length"
,
(CFStringRef)[
NSString
stringWithFormat
:
@"%ld"
, [fileData
length
]]);
CFDataRef
headerData = CFHTTPMessageCopySerializedMessage(response);
@try
{
[fileHandle
writeData
:(
NSData
*)headerData];
[fileHandle
writeData
:fileData];
}
@catch
(
NSException
*exception)
{
// Ignore the exception, it normally just means the client
// closed the connection from the other end.
}
@finally
{
CFRelease(headerData);
[server
closeHandler
:
self
];
}
}
|
The [server closeHandler:self];
invocation tells the server to remove thisHTTPResponseHandler
from the set of active handlers. The server will invokeendReponse
when it removes this handler (which is where we close the connection — since this handler does not supportkeep-alive
).
Work not implemented
The biggest task not handled in this implementation is parsing the HTTP request body.
The reason for this is that a general HTTP body solution is very complicated. The body's size may be specified by theContent-Length
header but it need not be — so knowing where the body ends can be difficult. The body may also be encoded in one of about a dozen differentTransfer-Encoding
s, including chunk
,quoted-printable
,base64
,gzip
— each of which require different processing.
However, I have never needed to implement a generic solution. It is normally easiest to determine what is needed for your specific needs and handle the HTTP body in accordance with those needs. You can handle the request body by overriding-[HTTPRequestHandler receiveIncomingDataNotification:]
. The default implementation ignores all data it receives after the HTTP request headers.
Data handling note: the first time the-[HTTPRequestHandler receiveIncomingDataNotification:]
method is called, the initial bytes of the HTTP body will already have been read from thefileHandle
and appended to therequest
instance variable. If you need to read the body, either continue reading into therequest
object, or remember to include this initial data.
Another task not handled are Keep-Alive connections. These also need to be handled in-[HTTPRequestHandler receiveIncomingDataNotification:]
and I've left a big comment on the method about what would be involved. The reality is that it's probably easier to set theConnection
header field to close
for every response to tell the client that you're not going to handle Keep-Alive (see thestartResponse
code sample above for an example).
The HTTPReseponseHandler
priority does not take the requested Content-Type
into account. If this is an important consideration for you, you might want to change how+[HTTPResponseHandler handlerClassForRequest:method:url:headerFields:]
selects its handler.
Finally, this server does not handle SSL/TLS. It is intended for local network transfers where the network itself is relatively secure. If you are transferring over the open internet and want a secure connection, there's a lot to change and manage at the socket level. You could try it yourself but if security is really important, you probably shouldn't risk writing your own server — if you can arrange it, use a mature, TLS-enabled HTTP server and only handle the client-side in your application. Client-side security in Cocoa is very easy — it is automatic and transparent in CFReadStream
andNSURLConnection
.
Conclusion
Download the sample app TextTransfer.zip (45kB) which includes theHTTPServer
andHTTPResponseHandler
classes.
Just because mainstream HTTP servers are large, complex pieces of software, doesn't mean an HTTP server is necessarily large and complex — the core implementation here is only two classes, yet is flexible and configurable.
Of course, the end result is not intended for use as a complete web server solution, however it should work well as a small communications portal into your custom iPhone or Mac applications.