【iOS】搭建本地http服务,并实现简单的GET与POST请求

最近的一个项目中,需要向 safari 前端页面传输数据,研究了一番之后发现只有搭建本地http服务才能完美解决这一需求。查询一番资料之后,我决定采用CocoaHttpServer这个现成的轮子。CocoaHttpServer是由deusty designs开源的一个项目,支持异步socket,ipv4和ipv6,http Authentication和TLS加密,小巧玲珑,而且使用方法也非常简单。

开启http服务

首先,我们需要开启http服务,代码如下

    // 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]用来设置端口号,此处可设置成80端口,如果是80端口,访问手机服务器的时候可以不用写端口号了。[httpServer setDocumentRoot:webPath]用来设置服务器根路径。这里要注意我们设置根路径的文件夹在拖进工程时应选择create folder references方式,这样才能在外部浏览器通过路径访问到文件夹内部的文件。

设置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 - 私有方法

//获取上行参数
- (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方法中则用来配置不同方法及路径对应的处理方式及返回数据。

外部访问

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

当我们的服务启动后,假如要在外部访问上图中的index.html文件,只需通过http://localhost:12345/index.html这样的路径即可。当然,也可以通过http://127.0.0.1:12345/index.html或者将127.0.0.1替换成设备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访问本地服务时,还会存在跨域问题。这个问题我们需要通过在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占用硬件资源管理的很严格,更不要说应用在后台运行时的资源占用了。正常情况下,使用应用时,APP从硬盘加载到内存后,便开始正常工作。当用户按下home键,APP便被挂起到后台。当内存不够用时,系统会自动把之前挂起状态下的APP从内存中清除。如果要使程序在后台常驻,则需要申请后台权限。

因此,我们要想保持本地服务在后台运行,便必须要保证APP拥有后台运行的权限,并需要根据APP的具体类型(如:音乐播放、定位、VOIP等)在 Capabilities 中添加相应的 Background Modes 键值对,如下图所示

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

同时需要在代理方法中添加下述代码。当然,如果你的APP不存在和Background Modes 相符合的功能的话,这么做可能会导致 AppStore 审核不通过。

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

配置https

利用 CocoaHttpServer 也可以搭建出https服务。只需要在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证书,因此访问文件时会出现弹框提示不安全的问题,而GET与POST接口也出现了访问失败的情况。目前我想到的解决方案是将一个域名和127.0.0.1进行绑定,并使用该域名的SSL证书替换自签名证书。至于可行性,还没有做过实验,如果各位读者有更好的想法,欢迎一起讨论。

本文相关demo下载欢迎到我的github:Github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260