本文介紹如何快速使用Log ServiceiOS SDK採集日誌資料。
前提條件
已安裝iOS SDK。具體操作,請參見安裝iOS SDK。
快速使用
您可以按照以下方式對SDK進行初始化,並調用addLog方法上報日誌。
iOS SDK支援初始化多個執行個體,
LogProducerConfig執行個體與LogProducerClient執行個體要成對使用。上報日誌到Log Service時需使用阿里雲帳號或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 {
// Log Service的服務存取點。此處必須是以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無法針對某類日誌進行設定。一旦設定後,所有未成功發送到Log Service的日誌,都可能會更新。如果您需要通過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預解析和緩衝策略的樣本。
注意:如果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