在iPhone上實現簡單Http服務
原文地址:http://cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html
http 是計算機之間通訊協議的比較簡單的一種。在iPhone上,由于沒有同步數據和文件共享的APIs,實現iPhone應用程序與PC之間的數據傳輸的最佳方式就是在程序中嵌入一個http服務器。在這篇帖子理,我將演示如何寫一個簡單但可以擴展的http服務器。該服務器類也可在Mac下運行。
介紹
示例程序運行效果如下:
程序很簡單:你可以編輯和保存一個文本文件(總是保存在同一個文件)。當程序還在運行的時候,它會在8080端口上運行一個http服務。如果你請求”/” 路徑,它會返回文本文件的內容。其他請求會導致501錯誤。要想將文本文件從iPhone程序傳輸到PC,只需在瀏覽器中輸入iPhone的ip地址并加上端口號8080。
HTTPServer類和 HTTPResponseHandler 類
該http服務器涉及兩個類:服務器(負責監聽連接請求并讀取數據,直到http頭結束),響應處理(發送響應并從http頭以后進行數據讀取)。
設計server類和response類的目的是簡化其他response類的實現,只需要實現這3個方法:
- canHandleRequest:method:url:headerFields: 指定該response是否會對某個請求進行處理。
- startResponse : 開始進行響應。
- load: — 所有子類都應該實現 +[NSObject load] 方法,并將自己向基類進行注冊。
這就是一個最基本的http服務器,但它可以讓你很快在程序中集成http通訊的功能。
建立Socket監聽
包括http在內的大部分服務器通訊,都要從建立socket監聽開始。Cocoa的Sockets可以完全采用BSDsockets代碼實現,但使用 CoreFoundation 的CFSocket API要容易一些。不幸的是,雖然已經“盡可能大地”簡化——但為了打開一個socket,你仍然不得不寫大量模式化的代碼。
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; }
這么多的代碼只是在做一件事情:打開socket,監聽來自HTTP_SERVER_PORT(8080端口)的TCP連接。
此外,我使用了SO_REUSEADDR。這是為了重用已經打開的端口(這是因為,如果我們在程序崩潰后立即重新打開程序,經常會導致端口被占用)。
接受請求
socket一旦建立,事情就變得簡單了。對于每個監聽到的連接通知,我們可以從fileDescriptor 構造一個NSFileHandle 以接受請求。
listeningHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileDescriptor closeOnDealloc:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveIncomingConnectionNotification:) name:NSFileHandleConnectionAcceptedNotification object:nil]; [listeningHandle acceptConnectionInBackgroundAndNotify]; |
當 receiveIncomingConnectionNotification:方法被調用時,每個新來的請求都會創建一個NSFileHandle. 繼續跟蹤下去你會發現:
- 對于socket fileDescriptor監聽到的新連接請求,從fileDescriptor“手動”創建了一個file handle(listeningHandle) 。
- 1 file handle (listeningHandle) manually created from the socket fileDesriptor to listen on the socket for new connections.
- 對于listeningHandle接收到的每個新連接,會“自動”創建一個file handle。我們會不停地監聽這些新的handles(key會記錄在inconmingRequests字典中),并記錄了每個連接的數據。
現在,我們收到了一個新的自動創建的file handle,我們創建了一個http請求消息CFHTTPMessageRef(用于儲存請求數據),并把這些對象存在incomingRequests字典以便下次訪問CFHTTPMessageRef。
CFHTTPMessageRef存放并對請求數據進行解析。 ,我們可以調用CFHTTMessageHeaderComplete()函數進行判斷,一直到http頭完成并產生一個response handler時 。
在HTTPServerreceiveIncomingDataNotification:方法中,我們生成了response handler。
if(CFHTTPMessageIsHeaderComplete(incomingRequest)) { HTTPResponseHandler *handler = [HTTPResponseHandler handlerForRequest:incomingRequest fileHandle:incomingFileHandle server:self]; [responseHandlers addObject:handler]; [self stopReceivingForFileHandle:incomingFileHandle close:NO]; [handler startResponse]; return; }
服務器停止監聽連接的同時并不關閉它,因為file handle被傳遞給HTTPResponseHandler以便HTTP響應能發至相同的file handle。
彈性的響應處理
+ [HTTPResponseHandlerhandlerForRequest:fileHandle:server:]方法到底返回哪個子類取決于 response的內容。它遍歷已注冊的handlers數組(以排序),輪詢每個handler看哪個愿意處理這個請求。
+ (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; }
因此,所有HTTPResponseHandlers都需要向基類進行注冊。最簡單的方法是在每個子類的+NSObjectload方法中進行注冊。
+ (void)load { [HTTPResponseHandler registerHandler:self]; }
在這里,僅有的response handler是AppTextFileResponse。這個類負責處理requestURL等于”/”的請求。
+ (BOOL)canHandleRequest:(CFHTTPMessageRef)aRequest method:(NSString *)requestMethod url:(NSURL *)requestURL headerFields:(NSDictionary *)requestHeaderFields { if ([requestURL.path isEqualToString:@"/"]) { return YES; } return NO; }
隨后,AppTextFileResponse在startResponse方法里進行同步響應,把程序保存的文本文件內容寫入響應消息里。
- (void)startResponse { NSData *fileData = [NSData dataWithContentsOfFile:[AppTextFileResponse pathForFile]]; CFHTTPMessageRef response = CFHTTPMessageCreateResponse( kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_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]; } }
[servercloseHandler:self]; 告訴服務端從當前handlers中移除HTTPResponseHandler。
服務端會調用endResponse移除handler(在關閉連接時——因為handler不支持keep-alive即常連接)。
有待完善之處
最大的任務解析http請求體沒有被實現。因為正常的http體解析過程十分復雜。http體的長度在content-length頭中指定,但可能不會被指定——因此無法直到http體何時結束。http體還可能是編碼的,編碼方式有各種各樣的:包括chunk, quoted-printable,base64, gzip— 而每一種的處理都完全不同。
我重來沒想過實現一種普遍的解決方案。通常要根據你的實際需要而定,你可以在HTTPRequestHandler的receiveIncomingDataNotification:方法中處理請求體。默認,我忽略了http請求頭之后的所有數據。
提示:
HTTPRequestHandlerreceiveIncomingDataNotification: 方法第一次調用時,Http體的開始字節已經從fileHandle中獲得并且添加進了request實例變量中。如果你想獲取http體,要么繼續讀入到request對象中,要么記得把開始的字節加進去。
另外一個沒有處理的是常連接keep-alive。這也是在 -[HTTPRequestHandler receiveIncomingDataNotification:]方法中處理,那里我已經做了注釋。實際上簡單的做法是設置每個response的 Connection http頭,告訴客戶端你不處理keep-alive。HttpReseponseHandler不會使用請求中的Content-Type頭。如果你想處理這個,你應該在+[HTTPResponseHandler handlerClassForRequest:method:url:headerFields:]方法中進行處理。
最后, 服務器不處理SSL/TLS。因為在本地網絡而言數據傳輸是相對安全的。如果在開放的internet中要提供一個安全鏈接,在socket這一層需要進行大量的改動。如果安全對你來說很主要,你可能不應該自己實現服務器——可能的話,采用一種成熟的TLSHTTP服務器,并只在客戶端進行處理。用 Cocoa處理客戶端的安全是很容易的——通過CFReadStream和NSURLConnection,它完全是自動的和透明的。
結語
下載: the sample app TextTransfer.zip (45kB) ,包含 HTTPServer 和 HTTPResponseHandler 類。雖然主流的HTTP 服務器都是龐大而復雜的軟件,但不意味著它必須是龐大和復雜的— 本文的實現只有兩個類,然而也可以進行配置和擴展。
當然,我們的目的不是把它當作一個復雜web服務器使用,它適用于在你的iPhone或Mac程序中作為一個輕量級的數據共享的入口。