本文介绍如何快速使用日志服务iOS SDK采集日志数据。
前提条件
已安装iOS SDK。具体操作,请参见安装iOS SDK。
快速使用
您可以按照以下方式对SDK进行初始化,并调用addLog
方法上报日志。
iOS SDK支持初始化多个实例,
LogProducerConfig
实例与LogProducerClient
实例要成对使用。上报日志到日志服务时需使用阿里云账号或RAM用户的AccessKey,用于鉴权及防篡改。为避免将AccessKey保存在移动端应用中,造成安全风险,推荐您使用移动端日志直传服务配置AccessKey。具体操作,请参见采集-搭建移动端日志直传服务。
@interface ProducerExampleController ()
// 建议您全局保存LogProducerConfig实例和LogProducerClient实例。
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end
@implementation ProducerExampleController
// callback为可选配置,如果您不需要关注日志的发送成功或失败状态,可以不注册callback。
// 如果需要动态化配置AccessKey,建议设置callback,并在callback被调用时更新AccessKey。
static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) {
if (result == LOG_PRODUCER_OK) {
NSString *success = [NSString stringWithFormat:@"send success, config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id];
SLSLogV("%@", success);
} else {
NSString *fail = [NSString stringWithFormat:@"send fail , config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s, error message : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id, message];
SLSLogV("%@", fail);
}
}
- (void) initLogProducer {
// 日志服务的服务接入点。此处必须是以https://或http://开头。
NSString *endpoint = @"your endpoint";
NSString *project = @"your project";
NSString *logstore = @"your logstore";
_config = [[LogProducerConfig alloc] initWithEndpoint:endpoint
project:project
logstore:logstore
];
// 设置日志主题。
[_config SetTopic:@"example_topic"];
// 设置tag信息,此tag信息将被附加在每条日志上。
[_config AddTag:@"example" value:@"example_tag"];
//是否丢弃过期日志。0表示不丢弃,把日志时间修改为当前时间; 1表示丢弃。默认值为1。
[_config SetDropDelayLog:1];
// 是否丢弃鉴权失败的日志,0表示不丢弃,1表示丢弃。默认值为0。
[_config SetDropUnauthorizedLog:0];
// 需要关注日志的发送成功或失败状态时, 第二个参数需要传入一个callback。
_client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
}
// 请求AccessKey信息。
- (void) requestAccessKey {
// 推荐您先使用移动端日志直传服务配置AccessKey信息。
// ...
// 获取到AccessKey信息后,完成更新。
[self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
}
// 更新AccessKey信息。
- (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
// 通过STS服务获取的AccessKey会包含securitToken,需要使用以下方式更新。
if (securityToken.length > 0) {
if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
[_config ResetSecurityToken:accessKeyId
accessKeySecret:accessKeySecret
securityToken:securityToken
];
}
} else {
// 不是通过STS服务获取的AccessKey,使用以下方式更新。
if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
[_config setAccessKeyId: accessKeyId];
[_config setAccessKeySecret: accessKeySecret];
}
}
}
// 上报日志。
- (void) addLog {
Log *log = [Log log];
// 您可以根据实际业务需要调整需上报的字段。
[log putContent:@"content_key_1" intValue:123456];
[log putContent:@"content_key_2" floatValue:23.34f];
[log putContent:@"content_key_3" value:@"中文"];
[_client AddLog:log];
}
@end
高级用法
动态配置参数
iOS SDK支持动态化配置ProjectName、Logstore、Endpoint、AccessKey等参数。其中Endpoint的获取方式,请参见服务接入点。AccessKey的获取方式,请参见访问密钥。
动态化配置Endpoint、ProjectName、Logstore。
// 支持独立配置或一起配置Endpoint、ProjectName、Logstore。 // 更新Endpoint。 [_config setEndpoint:@"your new-endpoint"]; // 更新ProjectName。 [_config setProject:@"your new-project"]; // 更新Logstore。 [_config setLogstore:@"your new-logstore"];
动态配置AccessKey。
动态配置AccessKey时,一般建议与callback结合使用。
// 如果您在初始化LogProducerClient时已经完成了callback的初始化,以下代码可忽略。 static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) { if (LOG_PRODUCER_SEND_UNAUTHORIZED == result || LOG_PRODUCER_PARAMETERS_INVALID) { [selfClzz requestAccessKey]; // selfClzz为对当前类的持有。 } } // 需要关注日志的发送成功或失败状态时, 第二个参数需要传入一个callback。 _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done]; // 请求AccessKey信息。 - (void) requestAccessKey { // 推荐您先使用移动端日志直传服务配置AccessKey信息。 // ... // 获取到AccessKey信息后,完成更新。 [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken]; } // 更新AccessKey信息。 - (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken { // 通过STS服务获取的AccessKey会包含securitToken,需要使用以下方式更新。 if (securityToken.length > 0) { if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config ResetSecurityToken:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken ]; } } else { // 不是通过STS服务获取的AccessKey,使用以下方式更新。 if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config setAccessKeyId: accessKeyId]; [_config setAccessKeySecret: accessKeySecret]; } } }
动态化配置 source、topic、tag。
重要source、topic、tag无法针对某类日志进行设置。一旦设置后,所有未成功发送到日志服务的日志,都可能会更新。如果您需要通过source、topic、tag来跟踪具体类别的日志,可能会导致与您的业务预期不相符合。建议您在生成Log时新增字段来标识对应的类别信息。
// 设置日志主题。 [_config SetTopic:@"your new-topic"]; // 设置日志来源。 [_config SetSource:@"your new-source"]; // 设置tag信息,此tag信息会附加在每条日志上。 [_config AddTag:@"test" value:@"your new-tag"];
断点续传
iOS SDK支持断点续传。开启断点续传后,每次通过addLog方法上传成功的日志都会先保存在本地binlog文件中,只有日志发送成功后才会删除本地数据,确保日志上传实现At Least Once。
您可以在SDK初始化时加入以下代码,实现断点续传。
初始化多个LogProducerConfig实例时,
LogProducerConfig
类的setPersistentFilePath
方法需要传入不同的值。如果您的App存在多进程且开启了断点续传功能,您应只在主进程初始化SDK。如果子进程也有采集数据的需求,您需要确保
SetPersistentFilePath
方法传入的文件路径的唯一性,否则可能会导致日志数据错乱、丢失等问题。使用时应注意多线程导致的LogProducerConfig重复初始化问题。
- (void) initLogProducer {
// 1表示开启断点续传功能,0表示关闭。默认值为0。
[_config SetPersistent:1];
// 持久化的文件名,需要保证文件所在的文件夹已创建。
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *Path = [[paths lastObject] stringByAppendingString:@"/log.dat"];
[_config SetPersistentFilePath:Path];
// 持久化文件滚动个数,建议设置成10。
[_config SetPersistentMaxFileCount:10];
// 每个持久化文件的大小,单位为Byte,格式为N*1024*1024。建议N的取值范围为1~10。
[_config SetPersistentMaxFileSize:N*1024*1024];
// 本地最多缓存的日志数量,不建议超过1048576,默认为65536。
[_config SetPersistentMaxLogCount:65536];
}
配置参数说明
所有的配置参数由LogProducerConfig类提供,详见下表:
参数 | 数据类型 | 说明 |
SetTopic | String | 设置topic字段的值。默认值为空字符串。 |
AddTag | String | 设置tag,格式为 |
SetSource | String | 设置source字段的值。默认值为iOS。 |
SetPacketLogBytes | Int | 每个缓存的日志包大小上限。超过上限后,日志会被立即发送。 取值范围为1~5242880,默认值为1024 * 1024,单位为字节。 |
SetPacketLogCount | Int | 每个缓存的日志包中包含日志数量的最大值。超过上限后日志会被立即发送。 取值范围为1~4096,默认值为1024。 |
SetPacketTimeout | Int | 被缓存日志的发送超时时间,如果缓存超时,日志会被立即发送。 默认值为3000,单位为毫秒。 |
SetMaxBufferLimit | Int | 单个Producer Client实例可以使用的内存的上限,超出缓存时add_log接口会立即返回失败。 默认值为64 * 1024 * 1024。 |
SetPersistent | Int | 是否开启断点续传功能。
|
SetPersistentFilePath | String | 持久化的文件名,需保证文件所在的文件夹已创建。配置多个LogProducerConfig实例时,需确保唯一性。 默认值为空。 |
SetPersistentForceFlush | Int | 是否开启每次AddLog强制刷新功能。
在高可靠性场景下建议开启。 |
SetPersistentMaxFileCount | Int | 持久化文件滚动个数,建议取值范围为1~10,默认值为0。 |
SetPersistentMaxFileSize | Int | 每个持久化文件的大小,单位为Byte,格式为N*1024*1024。建议N的取值范围为1~10。 |
SetPersistentMaxLogCount | Int | 本地最多缓存的日志数量,不建议超过1048576,默认为65536。 |
SetConnectTimeoutSec | IntInt | 网络连接超时时间。默认值为10,单位为秒。 |
SetSendTimeoutSec | Int | 日志发送超时时间。默认值为15,单位为秒。 |
SetDestroyFlusherWaitSec | Int | flusher线程销毁最大等待时间。默认值为1,单位为秒。 |
SetDestroySenderWaitSec | Int | sender线程池销毁最大等待时间。默认值为1,单位为秒。 |
SetCompressType | Int | 数据上传时的压缩类型。
|
SetNtpTimeOffset | Int | 设备时间与标准时间的差值,值为标准时间-设备时间。一般这种差值是由于用户客户端设备时间不同步场景。默认值为0,单位为秒。 |
SetMaxLogDelayTime | Int | 日志时间与本机时间的差值。超过该差值后,SDK会根据setDropDelayLog选项进行处理。单位为秒,默认值为7243600,即7天。 |
SetDropDelayLog | Int | 是否丢弃超过setMaxLogDelayTime的过期日志。
|
SetDropUnauthorizedLog | Int | 是否丢弃鉴权失败的日志。
|
错误码
全部的错误码定义在log_producer_result
,详细说明如下表所示。
错误码 | 数值 | 说明 | 解决方案 |
LOG_PRODUCER_OK | 0 | 成功。 | 不涉及。 |
LOG_PRODUCER_INVALID | 1 | SDK已销毁或无效。 |
|
LOG_PRODUCER_WRITE_ERROR | 2 | 数据写入错误,可能原因是Project写入流量已达上限。 | 调整Project写入流量上限。具体操作,请参见调整资源配额。 |
LOG_PRODUCER_DROP_ERROR | 3 | 磁盘或内存缓存已满,日志无法写入。 | 调整maxBufferLimit、persistentMaxLogCount、persistentMaxFileSize参数值后重试。 |
LOG_PRODUCER_SEND_NETWORK_ERROR | 4 | 网络错误。 | 检查Endpoint、Project、Logstore的配置情况。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | 5 | Project写入流量已达上限。 | 调整Project写入流量上限。具体操作,请参见调整资源配额。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | 6 | AccessKey过期、无效或AccessKey权限策略配置不正确。 | 检查AccessKey。 RAM用户需具备操作日志服务资源的权限。具体操作,请参见为RAM用户授权。 |
LOG_PRODUCER_SEND_SERVER_ERROR | 7 | 服务错误 | 提请工单联系技术支持。 |
LOG_PRODUCER_SEND_DISCARD_ERROR | 8 | 数据被丢弃,一般是设备时间与服务器时间不同步导致 | SDK会自动重新发送。 |
LOG_PRODUCER_SEND_TIME_ERROR | 9 | 与服务器时间不同步。 | SDK会自动修复该问题。 |
LOG_PRODUCER_SEND_EXIT_BUFFERED | 10 | SDK销毁时缓存数据还没有发出。 | 建议开启断点续传功能,以避免数据丢失。 |
LOG_PRODUCER_PARAMETERS_INVALID | 11 | SDK初始化参数错误 | 检查AccessKey、Endpoint、Project、Logstore等参数配置。 |
LOG_PRODUCER_PERSISTENT_ERROR | 99 | 缓存数据写入磁盘失败 | 1、检查缓存文件路径配置是否正确。 2、检查缓存文件是否写满。 3、检查系统磁盘空间是否充足。 |
常见问题
为什么会存在重复日志?
iOS SDK发送日志的过程是异步的,受网络状态影响,日志可能会发送失败并重新发送。由于SDK只在接口返回200状态码时才认为发送成功,因此日志会存在一定的重复率。建议您通过SQL查询分析语句对数据进行去重。
如果日志重复率较高,您需要先排查下SDK初始化是否存在问题。主要原因和解决方案如下:
断点续传配置错误
针对断点续传配置错误,您需要确认下
SetPersistentFilePath
方法传入的文件路径是否全局唯一。SDK重复初始化
造成SDK重复初始化的常见原因是单个实例写法错误,或者没有使用单例模式封装SDK的初始化,建议您参考以下方式完成SDK初始化。
// AliyunLogHelper.h @interface AliyunLogHelper : NSObject + (instancetype)sharedInstance; - (void) addLog:(Log *)log; @end // AliyunLogHelper.m @interface AliyunLogHelper () @property(nonatomic, strong) LogProducerConfig *config; @property(nonatomic, strong) LogProducerClient *client; @end @implementation AliyunLogHelper + (instancetype)sharedInstance { static AliyunLogHelper *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (instancetype)init { self = [super init]; if (self) { [self initLogProducer]; } return self; } - (void) initLogProducer { // 以下代码替换为您的初始化代码。 _config = [[LogProducerConfig alloc] initWithEndpoint:@"" project:@"" logstore:@"" accessKeyID:@"" accessKeySecret:@"" securityToken:@"" ]; _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done]; } - (void) addLog:(Log *)log { if (nil == log) { return; } [_client AddLog:log]; } @end
另外一个造成SDK重复初始化的原因是多进程。建议您只在主进程中初始化SDK,或者在不同进程中初始化SDK时,设置
SetPersistentFilePath
为不同的值,确保唯一性。
弱网环境配置优化
如果您的应用是在弱网环境下使用,建议您参考如下示例优化SDK配置参数。
// 初始化SDK。 - (void) initProducer() { // 调整HTTP链接和发送超时时间,有利于减少日志重复率。 // 您可以根据实际情况调整具体的超时时间。 [_config SetConnectTimeoutSec:20]; [_config SetSendTimeoutSec:20]; // 其他初始化参数。 // ... }
日志缺失,如何处理?
日志上报的过程是异步的,如果在上报日志前App被关闭,则日志有可能无法被上报,造成日志丢失。建议您开启断点续传功能。具体操作,请参见断点续传。
日志上报延时,如何处理?
SDK发送日志的过程是异步的,受网络环境以及应用使用场景的影响,日志可能不会立即上报。如果只有个别设备出现日志上报延时,这种情况是正常的,否则请您根据如下错误码进行排查。
错误码 | 说明 |
LOG_PRODUCER_SEND_NETWORK_ERROR | 检查Endpoint、Project、Logstore的配置是否正确。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | 检查AccessKey是否过期、有效或AccessKey权限策略配置是否正确。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | 调整Project写入流量已达上限。具体操作,请参见调整资源配额。 |
iOS SDK是否支持DNS预解析和缓存策略?
以下提供iOS SDK结合HTTPDNS SDK实现DNS预解析和缓存策略的示例。更多信息,请参见HTTPS+SNI场景接入方案。
注意:如果SLS的endpoint使用的是HTTPS域名,则您必须参考 HTTPS+SNI场景接入方案。
自定义NSURLProtocol实现。
#import <Foundation/Foundation.h> #import "HttpDnsNSURLProtocolImpl.h" #import <arpa/inet.h> #import <zlib.h> #import <objc/runtime.h> static NSString *const hasBeenInterceptedCustomLabelKey = @"HttpDnsHttpMessagePropertyKey"; static NSString *const kAnchorAlreadyAdded = @"AnchorAlreadyAdded"; @interface HttpDnsNSURLProtocolImpl () <NSStreamDelegate> @property (strong, readwrite, nonatomic) NSMutableURLRequest *curRequest; @property (strong, readwrite, nonatomic) NSRunLoop *curRunLoop; @property (strong, readwrite, nonatomic) NSInputStream *inputStream; @property (nonatomic, assign) BOOL responseIsHandle; @property (assign, nonatomic) z_stream gzipStream; @end @implementation HttpDnsNSURLProtocolImpl - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client { self = [super initWithRequest:request cachedResponse:cachedResponse client:client]; if (self) { _gzipStream.zalloc = Z_NULL; _gzipStream.zfree = Z_NULL; if (inflateInit2(&_gzipStream, 16 + MAX_WBITS) != Z_OK) { [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"gzip initialize fail" code:-1 userInfo:nil]]; } } return self; } /** * 是否拦截处理指定的请求 * * @param request 指定的请求 * @return 返回YES表示要拦截处理,返回NO表示不拦截处理 */ + (BOOL)canInitWithRequest:(NSURLRequest *)request { if([[request.URL absoluteString] isEqual:@"about:blank"]) { return NO; } // 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环 if ([NSURLProtocol propertyForKey:hasBeenInterceptedCustomLabelKey inRequest:request]) { return NO; } NSString * url = request.URL.absoluteString; NSString * domain = request.URL.host; // 只有https需要拦截 if (![url hasPrefix:@"https"]) { return NO; } // 需要的话可以做更多判断,如配置一个host数组来限制只有这个数组中的host才需要拦截 // 只有已经替换为ip的请求需要拦截 if (![self isPlainIpAddress:domain]) { return NO; } return YES; } + (BOOL)isPlainIpAddress:(NSString *)hostStr { if (!hostStr) { return NO; } // 是否ipv4地址 const char *utf8 = [hostStr UTF8String]; int success = 0; struct in_addr dst; success = inet_pton(AF_INET, utf8, &dst); if (success == 1) { return YES; } // 是否ipv6地址 struct in6_addr dst6; success = inet_pton(AF_INET6, utf8, &dst6); if (success == 1) { return YES; } return NO; } // 如果需要对请求进行重定向,添加指定头部等操作,可以在该方法中进行 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } // 开始加载,在该方法中,加载一个请求 - (void)startLoading { NSMutableURLRequest *request = [self.request mutableCopy]; // 表示该请求已经被处理,防止无限循环 [NSURLProtocol setProperty:@(YES) forKey:hasBeenInterceptedCustomLabelKey inRequest:request]; self.curRequest = [self createNewRequest:request]; [self startRequest]; } - (NSString *)cookieForURL:(NSURL *)URL { NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSMutableArray *cookieList = [NSMutableArray array]; for (NSHTTPCookie *cookie in [cookieStorage cookies]) { if (![self p_checkCookie:cookie URL:URL]) { continue; } [cookieList addObject:cookie]; } if (cookieList.count > 0) { NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieList]; if ([cookieDic objectForKey:@"Cookie"]) { return cookieDic[@"Cookie"]; } } return nil; } - (BOOL)p_checkCookie:(NSHTTPCookie *)cookie URL:(NSURL *)URL { if (cookie.domain.length <= 0 || URL.host.length <= 0) { return NO; } if ([URL.host containsString:cookie.domain]) { return YES; } return NO; } - (NSMutableURLRequest *)createNewRequest:(NSURLRequest*)request { NSURL* originUrl = request.URL; NSString *cookie = [self cookieForURL:originUrl]; NSMutableURLRequest* mutableRequest = [request copy]; [mutableRequest setValue:cookie forHTTPHeaderField:@"Cookie"]; return [mutableRequest copy]; } /** * 取消请求 */ - (void)stopLoading { if (_inputStream.streamStatus == NSStreamStatusOpen) { [self closeStream:_inputStream]; } [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]]; } /** * 使用CFHTTPMessage转发请求 */ - (void)startRequest { // 原请求的header信息 NSDictionary *headFields = _curRequest.allHTTPHeaderFields; 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); // 添加http post请求所附带的数据 CFStringRef requestBody = CFSTR(""); CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0); if (_curRequest.HTTPBody) { bodyData = (__bridge_retained CFDataRef) _curRequest.HTTPBody; } else if (_curRequest.HTTPBodyStream) { NSData *data = [self dataWithInputStream:_curRequest.HTTPBodyStream]; NSString *strBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"originStrBody: %@", strBody); CFDataRef body = (__bridge_retained CFDataRef) data; CFHTTPMessageSetBody(cfrequest, body); CFRelease(body); } else { CFHTTPMessageSetBody(cfrequest, bodyData); } // copy原请求的header信息 for (NSString* header in headFields) { CFStringRef requestHeader = (__bridge CFStringRef) header; CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header]; CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue); } // 创建CFHTTPMessage对象的输入流 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest); #pragma clang diagnostic pop self.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,这对于重定向的请求很关键 self.curRunLoop = [NSRunLoop currentRunLoop]; } // 将请求放入当前runloop的事件队列 [_inputStream scheduleInRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [_inputStream open]; CFRelease(cfrequest); CFRelease(requestURL); cfrequest = NULL; CFRelease(bodyData); CFRelease(requestBody); CFRelease(requestMethod); } - (NSData*)dataWithInputStream:(NSInputStream*)stream { NSMutableData *data = [NSMutableData data]; [stream open]; NSInteger result; uint8_t buffer[1024]; while ((result = [stream read:buffer maxLength:1024]) != 0) { if (result > 0) { // buffer contains result bytes of data to be handled [data appendBytes:buffer length:result]; } else if (result < 0) { // The stream had an error. You can get an NSError object using [iStream streamError] data = nil; break; } } [stream close]; return data; } #pragma mark - NSStreamDelegate /** * input stream 收到header complete后的回调函数 */ - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { if (eventCode == NSStreamEventHasBytesAvailable) { CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader); #pragma clang diagnostic pop if (CFHTTPMessageIsHeaderComplete(message)) { NSInputStream *inputstream = (NSInputStream *) aStream; NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded)); NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message)); if (!alreadyAdded || ![alreadyAdded boolValue]) { objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY); // 通知client已收到response,只通知一次 CFStringRef httpVersion = CFHTTPMessageCopyVersion(message); // 获取响应头部的状态码 CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message); if (!self.responseIsHandle) { NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.responseIsHandle = YES; } // 验证证书 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) { [self closeStream:aStream]; [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]]; return; } if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) { // 证书验证不通过,关闭input stream [self closeStream:aStream]; [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]]; } else { // 证书校验通过 if (statusCode >= 300 && statusCode < 400) { // 处理重定向错误码 [self closeStream:aStream]; [self handleRedirect:message]; } else { NSError *error = nil; NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error]; if (error) { [self.client URLProtocol:self didFailWithError:error]; } else { [self.client URLProtocol:self didLoadData:data]; } } } } else { NSError *error = nil; NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error]; if (error) { [self.client URLProtocol:self didFailWithError:error]; } else { [self.client URLProtocol:self didLoadData:data]; } } CFRelease((CFReadStreamRef)inputstream); CFRelease(message); } } else if (eventCode == NSStreamEventErrorOccurred) { [self closeStream:aStream]; inflateEnd(&_gzipStream); // 通知client发生错误了 [self.client URLProtocol:self didFailWithError: [[NSError alloc] initWithDomain:@"NSStreamEventErrorOccurred" code:-1 userInfo:nil]]; } else if (eventCode == NSStreamEventEndEncountered) { CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader); #pragma clang diagnostic pop if (CFHTTPMessageIsHeaderComplete(message)) { NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded)); NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message)); if (!alreadyAdded || ![alreadyAdded boolValue]) { objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY); // 通知client已收到response,只通知一次 if (!self.responseIsHandle) { CFStringRef httpVersion = CFHTTPMessageCopyVersion(message); // 获取响应头部的状态码 CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message); NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.responseIsHandle = YES; } } } [self closeStream:_inputStream]; inflateEnd(&_gzipStream); [self.client URLProtocolDidFinishLoading:self]; } } - (NSData *)readDataFromInputStream:(NSInputStream *)inputStream headerDict:(NSDictionary *)headDict stream:(NSStream *)aStream error:(NSError **)error { // 以防response的header信息不完整 UInt8 buffer[16 * 1024]; NSInteger length = [inputStream read:buffer maxLength:sizeof(buffer)]; if (length < 0) { *error = [[NSError alloc] initWithDomain:@"inputstream length is invalid" code:-2 userInfo:nil]; [aStream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [aStream setDelegate:nil]; [aStream close]; return nil; } NSData *data = [[NSData alloc] initWithBytes:buffer length:length]; if (headDict[@"Content-Encoding"] && [headDict[@"Content-Encoding"] containsString:@"gzip"]) { data = [self gzipUncompress:data]; if (!data) { *error = [[NSError alloc] initWithDomain:@"can't read any data" code:-3 userInfo:nil]; return nil; } } return data; } - (void)closeStream:(NSStream*)stream { [stream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [stream setDelegate:nil]; [stream close]; } - (void)handleRedirect:(CFHTTPMessageRef)messageRef { // 响应头 CFDictionaryRef headerFieldsRef = CFHTTPMessageCopyAllHeaderFields(messageRef); NSDictionary *headDict = (__bridge_transfer NSDictionary *)headerFieldsRef; [self redirect:headDict]; } - (void)redirect:(NSDictionary *)headDict { // 重定向时如果有cookie需求的话,注意处理 NSString *location = headDict[@"Location"]; if (!location) location = headDict[@"location"]; NSURL *url = [[NSURL alloc] initWithString:location]; _curRequest.URL = url; if ([[_curRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) { // 根据RFC文档,当重定向请求为POST请求时,要将其转换为GET请求 _curRequest.HTTPMethod = @"GET"; _curRequest.HTTPBody = nil; } [self startRequest]; } - (NSData *)gzipUncompress:(NSData *)gzippedData { if ([gzippedData length] == 0) { return gzippedData; } unsigned full_length = (unsigned) [gzippedData length]; unsigned half_length = (unsigned) [gzippedData length] / 2; NSMutableData *decompressed = [NSMutableData dataWithLength:full_length + half_length]; BOOL done = NO; int status; _gzipStream.next_in = (Bytef *)[gzippedData bytes]; _gzipStream.avail_in = (uInt)[gzippedData length]; _gzipStream.total_out = 0; while (_gzipStream.avail_in != 0 && !done) { if (_gzipStream.total_out >= [decompressed length]) { [decompressed increaseLengthBy:half_length]; } _gzipStream.next_out = (Bytef *)[decompressed mutableBytes] + _gzipStream.total_out; _gzipStream.avail_out = (uInt)([decompressed length] - _gzipStream.total_out); status = inflate(&_gzipStream, Z_SYNC_FLUSH); if (status == Z_STREAM_END) { done = YES; } else if (status == Z_BUF_ERROR) { // 假如Z_BUF_ERROR是由于输出缓冲区不够大引起的,那么应该满足输入缓冲区未处理完,且输出缓冲区已填满 // 即满足_gzipStream.avail_in != 0 && _gzipStream.avail_out == 0,此时应该继续循环进行扩容 // 对于取反的条件,说明不是由于输出缓冲区不够大引起的,那么此时应该结束循环,代表出现了error if (_gzipStream.avail_in == 0 || _gzipStream.avail_out != 0) { return nil; } } else if (status != Z_OK) { return nil; } } [decompressed setLength:_gzipStream.total_out]; return [NSData dataWithData:decompressed]; } @end
实现自定义 BeforeSend。
- (void)setupHttpDNS:(NSString *)accountId { // 配置 HttpDNS HttpDnsService *httpdns = [[HttpDnsService alloc] initWithAccountID:accountId]; [httpdns setHTTPSRequestEnabled:YES]; [httpdns setPersistentCacheIPEnabled:YES]; [httpdns setReuseExpiredIPEnabled:YES]; [httpdns setIPv6Enabled:YES]; NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:configuration.protocolClasses]; // 设置上文自定义的 NSURLProtocol [protocolsArray insertObject:[HttpDnsNSURLProtocolImpl class] atIndex:0]; [configuration setProtocolClasses:protocolsArray]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil]; [SLSURLSession setURLSession:session]; [SLSURLSession setBeforeSend:^NSMutableURLRequest * _Nonnull(NSMutableURLRequest * _Nonnull request) { NSURL *url = request.URL; // 这里可以加个判断,仅对需要的url生效 HttpdnsResult *result = [httpdns resolveHostSync:url.host byIpType:HttpdnsQueryIPTypeAuto]; if (!result) { return request; } NSString *ipAddress = nil; if (result.hasIpv4Address) { ipAddress = result.firstIpv4Address; } else if(result.hasIpv6Address) { ipAddress = result.firstIpv6Address; } else { return request; } NSString *requestUrl = url.absoluteString; requestUrl = [requestUrl stringByReplacingOccurrencesOfString: url.host withString:ipAddress]; [request setURL:[NSURL URLWithString:requestUrl]]; [request setValue:url.host forHTTPHeaderField:@"host"]; return request; }]; }
完成SLS SDK初始化。
@implementation AliyunSLS - (void)initSLS { // 给SLS SDK设置自定义 HttpDNS // !!!注意!!! // 该设置会对所有SLS SDK的示例生效 [self setupHttpDNS:accountId]; [self initProducer]; } - (void)initProducer { // 这里正常实现 LogProducerConfig 和 LogProducerClient 的初始化,与之前保持一致即可。 // ... } @end