iOS 基于 CocoaHTTPServer 搭建手机内部服务器,实现 http 及 https 访问、传输数据

2,942 阅读6分钟

废话开篇:先说一下小程序开发过程中,一般会利用工具进程真机测试,那么,是不是有一点好奇,就是如何用小程序开发工具进行打包,然后给手机在局域网下进行传包?那么,这里简单实现 iOS 手机内部搭建本地服务器,实现数据传输功能。即,有了沟通桥梁,就将小程序的压缩包解压并保存在沙盒路径下,在用手机小程序 SDK 进行预览。这里也只是推测了一下实现过程,官方到底是不是这样做的就不得而知了。但是,这里先实现一下 iOS 手机内部实现搭建服务器,实现 http 及 https 的访问、传输数据。

先看效果图:

屏幕录制2021-11-17 上午8.21.24.gif

步骤一、先 pod 一下 CocoaHTTPServer 这个第三方

pod 'CocoaHTTPServer', '~> 2.3'

步骤二、实现 http 请求方式

创建两个类

1、KDSHttpServer 继承自 NSObject,用来进行服务的设置、开启、停止等操作

@interface KDSHttpServer : NSObject

@end
#import "KDSHttpServer.h"
#import <CocoaHTTPServer/HTTPServer.h>
#import "KDSHTTPConnection.h"
#include <ifaddrs.h>
#include <arpa/inet.h>


@interface KDSHttpServer()

//本地服务对象
@property (nonatomic,strong) HTTPServer * server;
//本地服务端口
@property (nonatomic,strong) NSString * port;


@end


@implementation KDSHttpServer


- (instancetype)init
{
    self = [super init];
    if (self) {
        //获取当前ip
        [self getIPAddress];
        self.server = [[HTTPServer alloc] init];
        //设置固定端口号
        [self.server setPort:12345];
        //设置请求类型
        [self.server setType:@"_https.tcp"];
        //KDSHTTPConnection 这个类后面说明(请求管理类
        //设置请求管理类(设置接口地址请求类型(POST,GET),设置服务 http / https)
        [self.server setConnectionClass:[KDSHTTPConnection class]];
        //开启服务
        [self startServer];
    }
    return self;
}

//开启服务
- (void)startServer
{

    NSError *error;
    if([self.server start:&error]){
        NSLog(@"Started HTTP Server on port %hu", [self.server listeningPort]);
        self.port = [NSString stringWithFormat:@"%d",[self.server listeningPort]];
        //保存端口号,在调用的时候使用
        NSUserDefaults *accountDefaults = [NSUserDefaults standardUserDefaults];
        [accountDefaults setObject:self.port forKey:@"webPort"];
        [accountDefaults synchronize];
    } else {
        NSLog(@"Error starting HTTP Server: %@", error);
    }
}

//获取本地IP地址
- (NSString *)getIPAddress
{
    NSString *address = @"error";
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = 0;
    
    //retrieve the current interfaces - returns 0 on success
    success = getifaddrs(&interfaces);
    if (success == 0) {
        //Loop through linked list of interfaces
        temp_addr = interfaces;
        while (temp_addr != NULL) {
            if (temp_addr->ifa_addr->sa_family == AF_INET) {
                //Check if interface is en0 which is the wifi connection on the iPhone
                if ([[NSString stringWithUTF8String: temp_addr->ifa_name] isEqualToString:@"en0"]) {
                    //Get NSString from C String
                    address =[NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *) temp_addr->ifa_addr)->sin_addr)];
                }
            }
            temp_addr = temp_addr->ifa_next;
        }
    }
    //Free memory
    freeifaddrs(interfaces);
    NSLog(@"addrees----%@",address);
    return address;

}

//销毁时停止服务
- (void)dealloc
{
    [self.server stop];

}

2、KDSHTTPConnection 继承自 HTTPConnection 类,自定义接口请求类型、服务器 http / https 协议类型。

#import <Foundation/Foundation.h>
#import <CocoaHTTPServer/MultipartFormDataParser.h>
#import <CocoaHTTPServer/HTTPConnection.h>
#import <CocoaHTTPServer/HTTPDataResponse.h>
#import <CocoaHTTPServer/HTTPMessage.h>

NS_ASSUME_NONNULL_BEGIN

@interface KDSHTTPConnection : HTTPConnection<MultipartFormDataParserDelegate>

@end

NS_ASSUME_NONNULL_END

遵循的 MultipartFormDataParserDelegate 代理协议可约束各种请求方式。

//接口路径支持的请求方式(POST、GET...)
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path;

//针对不同请求方式是否允许 body 携带参数,(默认 POST、PUT 是允许)
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path;

//接受请求响应,解析数据(这里可以对 header、body等参数进行获取及解析)并向客户端返回数据
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path;

//数据接收回调,reques 成员变量拼接body里面的数据,最终调用上面数据解析步骤(这里必须实现)
- (void)processBodyData:(NSData *)postDataChunk;

//是否支持 https (返回YES即为 https)
- (BOOL)isSecureServer;

//返回一个保存证书的集合,数组的第一个集合必须是 SecIdentityRef(这个就是 公钥证书部分,需要客户端进行验证的部分),后面是各级 cer 证书。
- (NSArray *)sslIdentityAndCertificates;

下面先进行 http 的建立,.m 代码如下:

//设置接口请求类型
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
    // Add support for POST
    if ([method isEqualToString:@"POST"])
    {
        if ([path isEqualToString:@"/doPost"])
        {

            // Let's be extra cautious, and make sure the upload isn't 5 gigs
            return YES;
        }
    }
    
    if ([method isEqualToString:@"GET"])
    {
        if ([path isEqualToString:@"/doGet"])
        {
            // Let's be extra cautious, and make sure the upload isn't 5 gigs
            return YES;
        }
    }
    return [super supportsMethod:method atPath:path];

}

//是否允许 body 携带参数
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
    // 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

{
    if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/doPost"])
    {
        [self parseRequest];
        NSDictionary *responsDic = @{@"code":@"200",@"msg":@"操作成功"};
        NSData *responseData = [NSJSONSerialization dataWithJSONObject:responsDic options:0 error:nil];

        return [[HTTPDataResponse alloc] initWithData:responseData];

    }

   
    if ([method isEqualToString:@"GET"])
    {
        NSLog(@"GET param = %@",path);

        //[self getRequestParam:[request messageData] method:method];

        NSDictionary *responsDic = @{@"code":@"200",@"msg":@"操作成功"};

        NSData *responseData = [NSJSONSerialization dataWithJSONObject:responsDic options:0 error:**nil**];

        return [[HTTPDataResponse alloc] initWithData:responseData];

    }
  
    return [super httpResponseForMethod:method URI:path];

}

//解析 POST 数据(GET 参数数据是通过地址传来了,所以这里不做过多解析,匹配字符串即可)
- (void)parseRequest
{
    [self getRequestParam:request.body];
}

//解析 post body 数据(AF里面的参数拼接其实也是递归执行拼接)
- (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 stringByRemovingPercentEncoding];
        if (value && value.length) {
            [paramDic setValue:value forKey:arr.firstObject];
        }
    }

    NSLog(@"POST param = %@",paramDic);

    return [paramDic copy];

}

好了,运行一下:

//本地服务器(初始化即开启)
self.httpServer = [[KDSHttpServer alloc] init];

demo 运行会提醒防火墙是否允许介入网络,这里点击允许。

image.png

看一下手机本地服务器的地址及端口

image.png

postman 工具跑一下

image.png

可以看到,返回成功了,这里注意 body 里面的数据上传格式,不同的提交方式,本地服务器解析的代码也要随之改变。

xcode 控制台输出:

image.png

好了,到这里 http 请求方式就结束了,下面设置一下 https 请求方式

步骤三、实现 https 请求方式

KDSHTTPConnection.m 文件里设置一下开启 https 功能,代码如下:

//允许 https

- (BOOL)isSecureServer
{
    return YES;
}

//返回相关证书(集合的第一个必须为 SecIdentityRef)
- (NSArray *)sslIdentityAndCertificates
{
    SecIdentityRef identityRef = NULL;
    SecCertificateRef certificateRef = NULL;
    SecTrustRef trustRef = NULL;
    //这里需要一个p12文件
    NSString *thePath = [[NSBundle mainBundle] pathForResource:@"private_key" ofType:@"p12"];

    NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];

    CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(PKCS12Data);
    //p12 文件密码
    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:(id)CFBridgingRelease(identityRef),   (id)CFBridgingRelease(certificateRef), nil];

    return result;

}

这里 p12 文件终端命令:

openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt

工程内目录结构如下:

image.png

其他文件生成参考 # iOS 简单模拟 https 证书信任逻辑

postman 访问一下地址

image.png

这里的第三方 GCDAsynScoket 报错了

image.png

简单粗暴的方式,直接注释掉

image.png

再来 postman 访问一下,

image.png

这里看到,postman 提示是自签证书,但是它没有阻断请求,依旧返回了数据。

那么,用 AFNetwoeking 进行访问一下:


   //这里初始化必须用 baseUrl 方式进行初始化,域名鉴权需要
   AFHTTPSessionManager * m = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://10.10.60.20"]];
   //加载签名证书(用来验证服务器返回的公钥证书)
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"rsaCert" ofType:@"cer"];//证书的路径 xx.cer

    NSData *cerData = [NSData dataWithContentsOfFile:cerPath];

    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

    securityPolicy.pinnedCertificates = [[NSSet alloc] initWithObjects:cerData, nil];

    // 是否允许无效证书, 默认为NO
    securityPolicy.allowInvalidCertificates = NO;

    // 是否校验域名, 默认为YES
    securityPolicy.validatesDomainName = NO;

    [m setSecurityPolicy:securityPolicy];

    m.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/html",@"text/css",@"text/plain",@"application/xhtml+xml",@"application/xml", nil];

    NSDictionary * dic = @{@"title":@"123"};

    //NSLog(@"fileSize = %llu",[dic fileSize]);

    [m POST:@"https://10.10.60.20:12345/doPost" parameters:dic headers:nil progress:^(NSProgress * _Nonnull downloadProgress) {


        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

            NSLog(@"responseObject = %@",responseObject);

        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

        }];

其他文件生成参考 # iOS 简单模拟 https 证书信任逻辑 

由于上面的请求不允许自签证书(securityPolicy.validatesDomainName = NO),那么,AFNetwoeking 请求必定失败:

image.png

securityPolicy.validatesDomainName 设置为 YES,再访问一下:

image.png

image.png

可以看到,鉴权成功了,并且输出了正确的返回值。

这里注意 validatesDomainName 是否域名鉴权,如果设置为 YES,那么,在用终端命令行创建证书的时候 Common Name 一定要输入正确的域名!

image.png

好了,到这里搭建手机内部服务器,实现 httphttps 访问、传输数据的功能就完成了,在这样的基础上是不是就能做很多事了呢?代码拙劣,大神勿笑。