您的位置:首页 > 理论基础 > 计算机网络

iOS开发之CFHttpMessageRef的那些坑

2016-07-29 16:17 579 查看
在上一篇文章iOS开发之NSURLProtocol的那些坑中,介绍了一些NSURLProtocol使用过程中可能遇到的问题,今天我们就接着说CFHttpMessageRef。

或许你还没用过CFHttpMessageRef,应该说还没遇到需要使用这个的场景,当然,老司机就另当别论了

。这是个iOS系统中非常底层的网络通讯接口,已经接近于UNIX系统的socket通信了,使用CFHttpMessageRef进行HTTP连接的好处就是控制的粒度更细了,例如你可以设置SSL连接的PeerName,证书验证的方式,还可以控制每个响应包的接收。好了,废话不多说,先讲讲CFHttpMessageRef的连接过程吧。

以下的代码的场景是现在有一个NSURLRequest请求示例curRequest,我们需要复制他的请求头和请求body,并设置SSL连接的PeerName,完成HTTPS连接。

- (void)startRequest {
// 原请求的header信息
NSDictionary *headFields = curRequest.allHTTPHeaderFields;
// 添加http post请求所附带的数据
CFStringRef requestBody = CFSTR("");
CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0);
if (curRequest.HTTPBody) {
bodyData = (__bridge_retained CFDataRef) curRequest.HTTPBody;
} else if (headFields[@"originalBody"]) {
// 使用NSURLSession发POST请求时,将原始HTTPBody从header中取出
bodyData = (__bridge_retained CFDataRef) [headFields[@"originalBody"] dataUsingEncoding:NSUTF8StringEncoding];
}

CFStringRef url = (__bridge CFStringRef) [curRequest.URL absoluteString];
CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);

// 原请求所使用的方法,GET或POST
CFStringRef requestMethod = (__bridge_retained CFStringRef) curRequest.HTTPMethod;

// 根据请求的url、方法、版本创建CFHTTPMessageRef对象
CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1);
CFHTTPMessageSetBody(cfrequest, bodyData);

// copy原请求的header信息
for (NSString *header in headFields) {
if (![header isEqualToString:@"originalBody"]) {
// 不包含POST请求时存放在header的body信息
CFStringRef requestHeader = (__bridge CFStringRef) header;
CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header];
CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue);
}
}

// 创建CFHTTPMessage对象的输入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest);
inputStream = (__bridge_transfer NSInputStream *) readStream;

// 设置SNI host信息,关键步骤
NSString *host = [curRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = curRequest.URL.host;
}
[inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:
host, (__bridge id) kCFStreamSSLPeerName,
nil];
[inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
[inputStream setDelegate:self];

if (!curRunLoop)
// 保存当前线程的runloop,这对于重定向的请求很关键
curRunLoop = [NSRunLoop currentRunLoop];
// 将请求放入当前runloop的事件队列
[inputStream scheduleInRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream open];

CFRelease(cfrequest);
CFRelease(requestURL);
CFRelease(url);
cfrequest = NULL;
CFRelease(bodyData);
CFRelease(requestBody);
CFRelease(requestMethod);
}

当runloop发出请求,有响应包返回时,会调用NSInputStream的委托方法handleEvent,我们可以在该回调方法中获取响应包内容。
/**
* input stream 收到header complete后的回调函数
*/
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
if (eventCode == NSStreamEventHasBytesAvailable) {
CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (CFHTTPMessageIsHeaderComplete(message)) {
// 以防response的header信息不完整
UInt8 buffer[16 * 1024];
UInt8 *buf = NULL;
unsigned long length = 0;
NSInputStream *inputstream = (NSInputStream *) aStream;
NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, kAnchorAlreadyAdded);
if (!alreadyAdded || ![alreadyAdded boolValue]) {
objc_setAssociatedObject(aStream, kAnchorAlreadyAdded, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY);
// 通知client已收到response,只通知一次
NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
// 获取响应头部的状态码
CFIndex myErrCode = CFHTTPMessageGetResponseStatusCode(message);
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:curRequest.URL statusCode:myErrCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict];

[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

// 验证证书
SecTrustRef trust = (__bridge SecTrustRef) [aStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
SecTrustResultType res = kSecTrustResultInvalid;
NSMutableArray *policies = [NSMutableArray array];
NSString *domain = [[curRequest allHTTPHeaderFields] valueForKey:@"host"];
if (domain) {
[policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
} else {
[policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
}
/*
* 绑定校验策略到服务端的证书上
*/
SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies);
if (SecTrustEvaluate(trust, &res) != errSecSuccess) {
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
}
if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
/* 证书验证不通过,关闭input stream */
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];

} else {
// 证书通过,返回数据
if (![inputstream getBuffer:&buf length:&length]) {
NSInteger amount = [inputstream read:buffer maxLength:sizeof(buffer)];
buf = buffer;
length = amount;
}
NSData *data = [[NSData alloc] initWithBytes:buf length:length];

[self.client URLProtocol:self didLoadData:data];
}
} else {
// 证书已验证过,返回数据
if (![inputstream getBuffer:&buf length:&length]) {
NSInteger amount = [inputstream read:buffer maxLength:sizeof(buffer)];
buf = buffer;
length = amount;
}
NSData *data = [[NSData alloc] initWithBytes:buf length:length];

[self.client URLProtocol:self didLoadData:data];
}
}
} else if (eventCode == NSStreamEventErrorOccurred) {
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
// 通知client发生错误了
[self.client URLProtocol:self didFailWithError:[aStream streamError]];
} else if (eventCode == NSStreamEventEndEncountered) {
[self handleResponse];
}
}

这里值得注意的是,该方法只有等待http响应包头部完整才会回调,一开始我也觉得很神奇,NSInputStream是底层的socket通信,还会对上层协议http进行头部完整验证,但后来一想,这个stream是由CFReadStreamCreateForHTTPRequest创建的,这可能并不是简单的NSInputStream,应该是其子类,override了调用handleEvent的方法。
相信大家已经看出来了,这里通过self.client通知NSURLProtocol所监听的连接,告知其目前请求的状态。

本篇文章和上一篇其实是NSURLProtocol和CFHttpMessageRef结合使用的实践,文章的篇幅有限,完整的代码可到https://github.com/Dave1991/alicloud-ios-demo/blob/master/httpdns_ios_demo/httpdns_ios_demo/CFHttpMessageURLProtocol.m下载。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: