iOS发送http请求

前言

苹果公司在全球开发者大会(WWDC)的一场安全演示会上,公布了一个最后期限——2017 年 1 月 1 日——即 App Store 当中的所有应用必须在这个日期之前启用一项名为 App Transport Security 的重要安全功能。

App Transport Security,简称 ATS,是苹果在 iOS 9 当中首次推出的一项安全功能。在启用 ATS 之后,它会强制应用通过 HTTPS(而不是 HTTP)连接网络服务,这能够通过加密来保障用户数据安全。
ATS在 iOS 9 当中是默认开启的,然而,开发者仍然能够关闭 ATS,让自己的应用通过 HTTP 连接传输数据——现在的情况是,这招在年底之后就行不通了。另外,ATS 要求使用 TLS v 1.2,但那些已经经过加密的批量数据例外,比如流媒体数据。

那么问题来了,2017年开始提交审核的app该怎么办?其实无非以下几点:
* 要求你的接口方全部支持https
* 可以添加 NSAllowsArbitraryLoads 为 YES 来禁用 ATS,一般来说,可能类似浏览器类的 app 比较容易能通过,不然等着被拒
* 选择使用 NSExceptionDomains 来针对特定的域名开放 HTTP 调用。如果是第三方接口的话更容易审核通过,如果访问的是自己的服务器的话,可能这个理由无法通过

当然还有一些问题:
* 请求只发生在公司内的网络环境,或者访问的就是局域网(难道内网那么多服务也搞个https么~)
* https证书太贵了买不起(大公司应该没问题
~)
* 万一把第三方接口加入了白名单可还是被苹果拒了。。。

想了想,应该还是有解决方案的:
* 通过统一的proxy来代理所有的请求,只需proxy部署https即可

然后问题又来了:
* proxy压力好大,又多了一个维护的东西,而且万一挂了~~~(大公司有专人负责proxy不怕)

不过不管怎么样,app总还是有发送http的需求,要是能够避开ATS限制就好了~~~

AFNetworking&ASIHTTPRequest

通过查询文档和动手实践,发现ATS只限制了NSURLConnection 和 NSURLSession 等应用层的网络接口,并没有限制直接使用CFNetwork发送http请求,比如著名的网络库AFNetworking 和 ASIHTTPRequest,新版AFNetworking已经基于NSURLSession,而ASIHTTPRequest是基于CFNetwork,通过实际测试,AFNetworking会受ATS限制而ASIHTTPRequest不会,但是直接引入ASIHTTPRequest会导致包增长,而且这个库已经很久不维护了,所以就自己动手封装一个轻量的http库吧,部分逻辑参照ASI做了精简优化,请看以下CFNetwork介绍和http请求示例代码

CFNetwork 简介

CFNetwork是一个高性能的低级框架,可以控制一些更底层的东西,如各种常用网络协议、socket通讯等,实际上除了socket是传输层之外,本质上还是应用层上的封装的通用API。使用者可以不用关心底层协议的实际细节。
iOS发送http请求_第1张图片
(图片来源于官方文档)

目前iOS的网络编程分四层:

  • WebKit:属于Cocoa层,苹果很多地方用到的页面渲染引擎;
  • NSURL:也属于Cocoa层,对各类URL请求的封装;
  • CFNetwork:属于Core Foundation层,基于C的封装,同样的还有CFNetServices;
  • BSD sockets:属于OS层,也是基于C的封装;

框架结构

CFNetwork框架包括的类库如下:
iOS发送http请求_第2张图片
(图片来源于官方文档)

可以看到,CFNetwork的基础是CFSocket和CFStream。

CFSocket API
Socket是网络通讯的底层基础,可以让两个socket端口互发数据。最常用的socket抽象是BSD socket了。而CFSocket则是BSD socket的抽象,基本上实现了几乎所有BSD socket的功能,并且还融入了run loop。

CFStream API
CFStream API提供了与设备无关的读写数据的方法。使用它可以为内存、文件、网络(使用socket)的数据建立stream,能使用stream而不必马上把所有数据都写入到内存中。

CFStream提供API对两种CFType对象提供抽象:CFReadStream and CFWriteStream。它同时也是CFHTTP和CFFTP的基础。

CFNetwork 版本1

//定义头信息
        CFStringRef headerFieldName = CFSTR("this is a header");
        CFStringRef headerFieldValue = CFSTR("value");

        //创建url
        CFStringRef url1 = CFSTR("http://httpbin.org/get");
        CFURLRef myURL = CFURLCreateWithString(kCFAllocatorDefault, url1, NULL);

        //设置请求方式
        CFStringRef requestMethod = CFSTR("GET");

        //创建请求
        CFHTTPMessageRef myRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault,requestMethod, myURL, kCFHTTPVersion1_1);

        //设置头信息
        CFHTTPMessageSetHeaderFieldValue(myRequest, headerFieldName, headerFieldValue);

        //创建CFReadStreamRef对象来序列化并发送CFHTTP请求,注意CFReadStreamCreateForHTTPRequest在iOS 9.0开始已经有DEPRECATED警告,
        CFReadStreamRef myReadStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, myRequest);
        //打开读取流
        CFReadStreamOpen(myReadStream);

        //存放响应数据
        NSMutableData *responseBytes = [NSMutableData data];

        CFIndex numBytesRead = 0;

        //从流中读取数据,读完为止,其中CFReadStreamRead会阻塞代码
        do {
            UInt8 buf[1024];
            numBytesRead = CFReadStreamRead(myReadStream, buf, sizeof(buf));

            if (numBytesRead > 0) {
                [responseBytes appendBytes:buf length:numBytesRead];
            }
        } while (numBytesRead > 0);

        //读取响应头信息
        CFHTTPMessageRef myResponse = (CFHTTPMessageRef) CFReadStreamCopyProperty(myReadStream, kCFStreamPropertyHTTPResponseHeader);

        //读取statusCode
        CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(myResponse);

        //打印数据
        if(statusCode == 200) {
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseBytes options:NSJSONReadingMutableLeaves error:nil];
            NSLog(@"%@", json);
        }

CFNetwork 版本2

@interface CFNetworkTest : NSObject

@property (nonatomic, strong) NSMutableData *responseData;

@end

@implementation CFNetworkTest

- (void)sendMessage
{
    self.responseData = [NSMutableData data];

    [self sendRequest];
}

- (void)sendRequest
{
    CFStringRef url = CFSTR("http://httpbin.org/post");
    CFURLRef myURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
    CFStringRef requestMethod = CFSTR("POST");
    //创建post请求
    CFHTTPMessageRef myRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, myURL, kCFHTTPVersion1_1);

    //设置post数据
    NSString *body = @"key=cfnetwork";
    NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
    CFHTTPMessageSetBody(myRequest, (__bridge CFDataRef)bodyData);

    CFHTTPMessageSetHeaderFieldValue(myRequest, CFSTR("key"), CFSTR("headValue2"));

    CFReadStreamRef requestReadStream = CFReadStreamCreateForHTTPRequest(NULL, myRequest);

    //把self设置到ClientContext
    CFStreamClientContext clientContext =  { 0, (__bridge void *)(self), NULL, NULL, NULL };
    CFOptionFlags flags = kCFStreamEventHasBytesAvailable |
    kCFStreamEventEndEncountered |
    kCFStreamEventErrorOccurred;

    //设置回调函数及相关参数,通过flags标志来设置我们对哪些事件需要处理,myCFReadStreamClientCallback 是一个回调函数,当事件标志对应的事件发生时,该回调函数就会被调用;clientContext是用于传递参数到回调函数中去。
    Boolean result = CFReadStreamSetClient(requestReadStream, flags, myCFReadStreamClientCallback, &clientContext);
    if (result) {
        //当设置好回调函数之后,将requestReadStream 当做事件源调度到 runloop 中去,这样 runloop 就能分发该 requestReadStream 的网络事件了。
        CFReadStreamScheduleWithRunLoop(requestReadStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        if (CFReadStreamOpen(requestReadStream)) {
            CFRunLoopRun();
        } else {
            CFReadStreamUnscheduleFromRunLoop(requestReadStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        }
    }

    CFRelease(myURL);
    CFRelease(myRequest);
}

- (void)requestError
{
    NSLog(@"request error");
}

- (void)finishRequestOK
{
    NSString *responseString = [[NSString alloc] initWithData:_responseData encoding:NSUTF8StringEncoding];
    NSLog(@"resposneString: %@", responseString);
}

- (void)appendBytes:(const void *)bytes length:(NSUInteger)length
{
    [_responseData appendBytes:bytes length:length];
}


//stream的回调函数,读取EventType并进行对应的事件处理
static void myCFReadStreamClientCallback(CFReadStreamRef stream, CFStreamEventType type, void *cientCallbackInfo) {
    CFNetworkTest *delegate = (__bridge CFNetworkTest *)cientCallbackInfo;
    //数据流读取完成
    if (type == kCFStreamEventEndEncountered) {
        [delegate finishRequestOK];
    }
    //处理异常错误
    else if (type == kCFStreamEventErrorOccurred) {
        CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        CFReadStreamClose(stream);
        CFRelease(stream);
        stream = NULL;
        [delegate requestError];
    }
    //读取数据流
    else if (type == kCFStreamEventHasBytesAvailable) {
        UInt8 buffer[1024];
        CFIndex numBytesRead;
        numBytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer));
        [delegate appendBytes:buffer length:numBytesRead];
    }
}
@end

实现超时的逻辑

上面代码中用到的CFReadStreamRead是阻塞性的,所以不能放到主线程去执行,我们可以将网络请求的代码片段放入子线程并启动一个runloop去执行CFReadStreamRead的逻辑

+ (NSThread *)threadForRequest:(LiteHttpRequest *)request{
    if (networkThread == nil) {
        @synchronized(self) {
            if (networkThread == nil) {
                networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequests) object:nil];
                [networkThread start];
            }
        }
    }
    return networkThread;
}

+ (void)runRequests {

    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    // 保持runloop始终运行
    BOOL runAlways = YES;
    while (runAlways) {
        @autoreleasepool {
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }

    // 理论上不会执行以下代码
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

//在子线程处理request
    [self performSelector:@selector(sendrequest) onThread:[[self class] threadForRequest:self] withObject:nil waitUntilDone:NO];

-(void)sendrequest {
//以下省略部分代码
    [self setReadStream:(__bridge NSInputStream *)(CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, myRequest))];

    CFStreamClientContext ctxt = {0, (__bridge void *)(self), NULL, NULL, NULL};
    CFReadStreamSetClient((CFReadStreamRef)[self readStream], kNetworkEvents, myCFReadStreamClientCallback, &ctxt);
//将stream处理放入runloop
    [[self readStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:[self runLoopMode]];
    [self setReadStreamIsScheduled:YES];

    BOOL streamSuccessfullyOpened = NO;
//设置callback并open stream
    if (CFReadStreamSetClient((CFReadStreamRef)[self readStream], kNetworkEvents, myCFReadStreamClientCallback, &ctxt)) {
        if (CFReadStreamOpen((CFReadStreamRef)[self readStream])) {
            streamSuccessfullyOpened = YES;
            self.isRunning = YES;
        }
    }
}

stream的处理在子线程的runloop中,那么设置超时就容易多了,每次stream open前,我们可以启动一个定时器加入runloop,判断当前时间和stream开始处理的时间差值,如果大于超时时间设定,那么就做超时处理,关闭stream

    if(!self.timer) {
//设置timer并加入runloop,此处0.5s轮询的时间可以自己调整,会影响超时的精确性
        [self setTimer:[NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(checkStatus:) userInfo:nil repeats:YES]];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:[self runLoopMode]];
    }

    BOOL streamSuccessfullyOpened = NO;
    if (CFReadStreamSetClient((CFReadStreamRef)[self readStream], kNetworkEvents, myCFReadStreamClientCallback, &ctxt)) {
        if (CFReadStreamOpen((CFReadStreamRef)[self readStream])) {
            streamSuccessfullyOpened = YES;
//标记为running
            self.isRunning = YES;
//记录startTime
            self.startTime = [NSDate date];
        }
    }


-(void)stopTimer {
    [self.timer invalidate];
    self.timer = nil;
}

- (void)checkStatus:(NSTimer*)timer{
    NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.startTime];
    //判断超时
    if (self.isRunning && [self readStream] && [self readStreamIsScheduled] && self.timeout > 0 && time > self.timeout) {
        NSLog(@"timeout...");
//关闭stream流的读取
        [self destroyReadStream];
        [self requestFail:[NSError errorWithDomain:NetworkRequestErrorDomain code:LiteHttpRequestErrorConnectTimeout userInfo:nil]];
    } else if (!self.isRunning) {
//非请求中则直接关闭定时器,以免内存泄露
        [self stopTimer];
    }
}

一个轻量级的http库

以上代码只是基本的CFNetwork示例,还未达到直接给app使用的要求,其实我们大多时候只需要一个很简单的http库来实现发送网络请求的功能,一般来说要点如下
* 支持http&https(iOS中系统会自动校验合法的https证书)
* 支持get和post
* 支持cancel操作
* 支持超时设置
* 子线程处理请求,主线程回调
* 简单的错误处理
* 程序健壮不crash

你可能感兴趣的:(C/C++)