美文网首页iOS专题
【iOS】搭建本地http服务,并实现简单的GET与POST请求

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

作者: 焚雪残阳 | 来源:发表于2018-01-28 15:30 被阅读0次

    最近的一个项目中,需要向 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地址

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/prxhoxtx.html