苹果公司在全球开发者大会(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限制就好了~~~
通过查询文档和动手实践,发现ATS只限制了NSURLConnection 和 NSURLSession 等应用层的网络接口,并没有限制直接使用CFNetwork发送http请求,比如著名的网络库AFNetworking 和 ASIHTTPRequest,新版AFNetworking已经基于NSURLSession,而ASIHTTPRequest是基于CFNetwork,通过实际测试,AFNetworking会受ATS限制而ASIHTTPRequest不会,但是直接引入ASIHTTPRequest会导致包增长,而且这个库已经很久不维护了,所以就自己动手封装一个轻量的http库吧,部分逻辑参照ASI做了精简优化,请看以下CFNetwork介绍和http请求示例代码
CFNetwork是一个高性能的低级框架,可以控制一些更底层的东西,如各种常用网络协议、socket通讯等,实际上除了socket是传输层之外,本质上还是应用层上的封装的通用API。使用者可以不用关心底层协议的实际细节。
(图片来源于官方文档)
目前iOS的网络编程分四层:
框架结构
CFNetwork框架包括的类库如下:
(图片来源于官方文档)
可以看到,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的基础。
//定义头信息
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);
}
@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];
}
}
以上代码只是基本的CFNetwork示例,还未达到直接给app使用的要求,其实我们大多时候只需要一个很简单的http库来实现发送网络请求的功能,一般来说要点如下
* 支持http&https(iOS中系统会自动校验合法的https证书)
* 支持get和post
* 支持cancel操作
* 支持超时设置
* 子线程处理请求,主线程回调
* 简单的错误处理
* 程序健壮不crash