在iPhone上實現簡單Http服務

fmms 12年前發布 | 38K 次閱讀 iPhone iOS開發 移動開發

原文地址:http://cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html

http 是計算機之間通訊協議的比較簡單的一種。在iPhone上,由于沒有同步數據和文件共享的APIs,實現iPhone應用程序與PC之間的數據傳輸的最佳方式就是在程序中嵌入一個http服務器。在這篇帖子理,我將演示如何寫一個簡單但可以擴展的http服務器。該服務器類也可在Mac下運行。

介紹

示例程序運行效果如下:

在iPhone上實現簡單Http服務

程序很簡單:你可以編輯和保存一個文本文件(總是保存在同一個文件)。當程序還在運行的時候,它會在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程序中作為一個輕量級的數據共享的入口。

譯文出處:http://blog.csdn.net/kmyhy/article/details/7031329

 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!