【iOS】搭建本地http服務(wù)菱肖,并實現(xiàn)簡單的GET與POST請求

最近的一個項目中,需要向 safari 前端頁面?zhèn)鬏敂?shù)據(jù)亡鼠,研究了一番之后發(fā)現(xiàn)只有搭建本地http服務(wù)才能完美解決這一需求柜去。查詢一番資料之后,我決定采用CocoaHttpServer這個現(xiàn)成的輪子拆宛。CocoaHttpServer是由deusty designs開源的一個項目嗓奢,支持異步socket,ipv4和ipv6浑厚,http Authentication和TLS加密股耽,小巧玲瓏,而且使用方法也非常簡單钳幅。

開啟http服務(wù)

首先物蝙,我們需要開啟http服務(wù),代碼如下

    // Configure our logging framework.
    [DDLog addLogger:[DDTTYLogger sharedInstance]];
    
    // Initalize our http server
    httpServer = [[HTTPServer alloc] init];
    
    // Tell the server to broadcast its presence via Bonjour.
    [httpServer setType:@"_http._tcp."];
    
    // Normally there's no need to run our server on any specific port.
    [httpServer setPort:12345];
    
    // We're going to extend the base HTTPConnection class with our MyHTTPConnection class.
    [httpServer setConnectionClass:[YDHTTPConnection class]];
    
    // Serve files from our embedded Web folder
    NSString *webPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Web"];
    DDLogInfo(@"Setting document root: %@", webPath);
    [httpServer setDocumentRoot:webPath];
    
    NSError *error = nil;
    if(![httpServer start:&error])
    {
        DDLogError(@"Error starting HTTP Server: %@", error);
    }

[httpServer setPort:12345]用來設(shè)置端口號敢艰,此處可設(shè)置成80端口诬乞,如果是80端口,訪問手機服務(wù)器的時候可以不用寫端口號了。[httpServer setDocumentRoot:webPath]用來設(shè)置服務(wù)器根路徑震嫉。這里要注意我們設(shè)置根路徑的文件夾在拖進工程時應(yīng)選擇create folder references方式森瘪,這樣才能在外部瀏覽器通過路徑訪問到文件夾內(nèi)部的文件。

設(shè)置GET與POST路徑

GET與POST路徑的配置是在一個繼承自HTTPConnection的類中完成的票堵,即上一步[httpServer setConnectionClass:[YDHTTPConnection class]]中的YDHTTPConnection類扼睬。我們要在該類中重寫以下方法。

#pragma mark - get & post

- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
    HTTPLogTrace();
    
    // Add support for POST
    if ([method isEqualToString:@"POST"])
    {
        if ([path isEqualToString:@"/calculate"])
        {
            // Let's be extra cautious, and make sure the upload isn't 5 gigs
            return YES;
        }
    }
    
    return [super supportsMethod:method atPath:path];
}

- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
    HTTPLogTrace();
    
    // Inform HTTP server that we expect a body to accompany a POST request
    if([method isEqualToString:@"POST"]) return YES;
    
    return [super expectsRequestBodyFromMethod:method atPath:path];
}

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    HTTPLogTrace();
    
    //獲取idfa
    if ([path isEqualToString:@"/getIdfa"])
    {
        HTTPLogVerbose(@"%@[%p]: postContentLength: %qu", THIS_FILE, self, requestContentLength);
        NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
        NSData *responseData = [idfa dataUsingEncoding:NSUTF8StringEncoding];
        return [[HTTPDataResponse alloc] initWithData:responseData];
    }
    //加減乘除計算
    if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/calculate"])
    {
        HTTPLogVerbose(@"%@[%p]: postContentLength: %qu", THIS_FILE, self, requestContentLength);
        NSData *requestData = [request body];
        NSDictionary *params = [self getRequestParam:requestData];
        NSInteger firstNum = [params[@"firstNum"] integerValue];
        NSInteger secondNum = [params[@"secondNum"] integerValue];
        NSDictionary *responsDic = @{@"add":@(firstNum + secondNum),
                                     @"sub":@(firstNum - secondNum),
                                     @"mul":@(firstNum * secondNum),
                                     @"div":@(firstNum / secondNum)};
        NSData *responseData = [NSJSONSerialization dataWithJSONObject:responsDic options:0 error:nil];
        return [[HTTPDataResponse alloc] initWithData:responseData];
    }
    
    return [super httpResponseForMethod:method URI:path];
}

- (void)prepareForBodyWithSize:(UInt64)contentLength
{
    HTTPLogTrace();
    
    // If we supported large uploads,
    // we might use this method to create/open files, allocate memory, etc.
}

- (void)processBodyData:(NSData *)postDataChunk
{
    HTTPLogTrace();
    
    // Remember: In order to support LARGE POST uploads, the data is read in chunks.
    // This prevents a 50 MB upload from being stored in RAM.
    // The size of the chunks are limited by the POST_CHUNKSIZE definition.
    // Therefore, this method may be called multiple times for the same POST request.
    
    BOOL result = [request appendData:postDataChunk];
    if (!result)
    {
        HTTPLogError(@"%@[%p]: %@ - Couldn't append bytes!", THIS_FILE, self, THIS_METHOD);
    }
}

#pragma mark - 私有方法

//獲取上行參數(shù)
- (NSDictionary *)getRequestParam:(NSData *)rawData
{
    if (!rawData) return nil;
    
    NSString *raw = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
    NSMutableDictionary *paramDic = [NSMutableDictionary dictionary];
    NSArray *array = [raw componentsSeparatedByString:@"&"];
    for (NSString *string in array) {
        NSArray *arr = [string componentsSeparatedByString:@"="];
        NSString *value = [arr.lastObject stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        [paramDic setValue:value forKey:arr.firstObject];
    }
    return [paramDic copy];
}

其中悴势,- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path用來配置需要支持的POST路徑窗宇。父類HTTPConnection中對GET方法是默認支持的,而POST方法則必須通過重寫來支持特纤。而- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path方法中則用來配置不同方法及路徑對應(yīng)的處理方式及返回數(shù)據(jù)军俊。

外部訪問

屏幕快照 2018-01-28 下午3.41.26.png

當我們的服務(wù)啟動后,假如要在外部訪問上圖中的index.html文件捧存,只需通過http://localhost:12345/index.html這樣的路徑即可粪躬。當然,也可以通過http://127.0.0.1:12345/index.html或者將127.0.0.1替換成設(shè)備ip矗蕊。而GET和POST方法我們也可以通過以下前端代碼來進行驗證短蜕。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
    <button onclick="getIdfa()">獲取idfa</button>
    <button onclick="calculate()">加減乘除</button>
    
    <script type="text/javascript">
        function getIdfa() {
            $.ajax({
                type: "get",    //請求方式
                async: true,    //是否異步
                url: "http://localhost:12345/getIdfa",
                success: function (data) {
                    alert(data);
                },
                error: function () {
                   alert("error");
                }
            });
        }
        function calculate() {
            $.ajax({
                type: "post",    //請求方式
                async: true,    //是否異步
                url: "http://localhost:12345/calculate",
                data: {"firstNum":9, "secondNum":7},
                success: function (data) {
                    alert(data);
                },
                error: function () {
                    alert("error");
                }
            });
        }
    </script>
</body>
</html>

另外氢架,在h5訪問本地服務(wù)時傻咖,還會存在跨域問題。這個問題我們需要通過在HTTPConnection類的- (NSData *)preprocessResponse:(HTTPMessage *)response- (NSData *)preprocessErrorResponse:(HTTPMessage *)response方法中加入以下代碼來解決岖研。

//允許跨域訪問
[response setHeaderField:@"Access-Control-Allow-Origin" value:@"*"];
[response setHeaderField:@"Access-Control-Allow-Headers" value:@"X-Requested-With"];
[response setHeaderField:@"Access-Control-Allow-Methods" value:@"PUT,POST,GET,DELETE,OPTIONS"];

后臺運行

我們都知道卿操,蘋果對APP占用硬件資源管理的很嚴格,更不要說應(yīng)用在后臺運行時的資源占用了孙援。正常情況下害淤,使用應(yīng)用時,APP從硬盤加載到內(nèi)存后拓售,便開始正常工作窥摄。當用戶按下home鍵,APP便被掛起到后臺础淤。當內(nèi)存不夠用時崭放,系統(tǒng)會自動把之前掛起狀態(tài)下的APP從內(nèi)存中清除。如果要使程序在后臺常駐鸽凶,則需要申請后臺權(quán)限币砂。

因此,我們要想保持本地服務(wù)在后臺運行玻侥,便必須要保證APP擁有后臺運行的權(quán)限决摧,并需要根據(jù)APP的具體類型(如:音樂播放、定位、VOIP等)在 Capabilities 中添加相應(yīng)的 Background Modes 鍵值對掌桩,如下圖所示

屏幕快照 2018-06-26 下午4.54.54.png

同時需要在代理方法中添加下述代碼边锁。當然,如果你的APP不存在和Background Modes 相符合的功能的話拘鞋,這么做可能會導(dǎo)致 AppStore 審核不通過砚蓬。

- (void)applicationDidEnterBackground:(UIApplication *)application {
    _bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:_bgTask];
        _bgTask = UIBackgroundTaskInvalid;
    }];
}

配置https

利用 CocoaHttpServer 也可以搭建出https服務(wù)。只需要在YDHTTPConnection中重寫以下兩個方法盆色。

#pragma mark - https

- (BOOL)isSecureServer
{
    HTTPLogTrace();

    return YES;
}

- (NSArray *)sslIdentityAndCertificates
{
    HTTPLogTrace();
    
    SecIdentityRef identityRef = NULL;
    SecCertificateRef certificateRef = NULL;
    SecTrustRef trustRef = NULL;
    NSString *thePath = [[NSBundle mainBundle] pathForResource:@"localhost" ofType:@"p12"];
    NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
    CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
    CFStringRef password = CFSTR("123456");
    const void *keys[] = { kSecImportExportPassphrase };
    const void *values[] = { password };
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);

    OSStatus securityError = errSecSuccess;
    securityError =  SecPKCS12Import(inPKCS12Data, optionsDictionary, &items);
    if (securityError == 0) {
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
        identityRef = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
        trustRef = (SecTrustRef)tempTrust;
    } else {
        NSLog(@"Failed with error code %d",(int)securityError);
        return nil;
    }

    SecIdentityCopyCertificate(identityRef, &certificateRef);
    NSArray *result = [[NSArray alloc] initWithObjects:(__bridge id)identityRef, (__bridge id)certificateRef, nil];

    return result;
}

在實驗過程中我使用的為自簽名SSL證書灰蛙,因此訪問文件時會出現(xiàn)彈框提示不安全的問題,而GET與POST接口也出現(xiàn)了訪問失敗的情況隔躲。目前我想到的解決方案是將一個域名和127.0.0.1進行綁定摩梧,并使用該域名的SSL證書替換自簽名證書。至于可行性宣旱,還沒有做過實驗仅父,如果各位讀者有更好的想法,歡迎一起討論浑吟。

本文相關(guān)demo下載歡迎到我的github:Github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笙纤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子组力,更是在濱河造成了極大的恐慌省容,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燎字,死亡現(xiàn)場離奇詭異腥椒,居然都是意外死亡,警方通過查閱死者的電腦和手機候衍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門笼蛛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蛉鹿,你說我怎么就攤上這事滨砍。” “怎么了妖异?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵惋戏,是天一觀的道長。 經(jīng)常有香客問我随闺,道長日川,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任矩乐,我火速辦了婚禮龄句,結(jié)果婚禮上回论,老公的妹妹穿的比我還像新娘。我一直安慰自己分歇,他們只是感情好傀蓉,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著职抡,像睡著了一般葬燎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缚甩,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天谱净,我揣著相機與錄音,去河邊找鬼擅威。 笑死壕探,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的郊丛。 我是一名探鬼主播李请,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼厉熟!你這毒婦竟也來了导盅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤揍瑟,失蹤者是張志新(化名)和其女友劉穎白翻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體月培,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡嘁字,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年恩急,在試婚紗的時候發(fā)現(xiàn)自己被綠了杉畜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡衷恭,死狀恐怖此叠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情随珠,我是刑警寧澤灭袁,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站窗看,受9級特大地震影響茸歧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜显沈,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一软瞎、第九天 我趴在偏房一處隱蔽的房頂上張望逢唤。 院中可真熱鬧,春花似錦涤浇、人聲如沸鳖藕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽著恩。三九已至,卻和暖如春蜻展,著一層夾襖步出監(jiān)牢的瞬間喉誊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工纵顾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留裹驰,地道東北人。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓片挂,卻偏偏與公主長得像幻林,于是被迫代替她去往敵國和親俊犯。 傳聞我的和親對象是個殘疾皇子摘能,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361