このトピックでは、iOS 用の Simple Log Service ソフトウェア開発キット ( SDK ) を使用してログデータを収集する方法について説明します。
前提条件
iOS SDK がインストールされています。 詳細については、「iOS SDK のインストール」をご参照ください。
クイックスタート
SDK を初期化し、addLog メソッドを呼び出してログを送信します。
iOS SDK は複数のインスタンスの初期化をサポートしています。
LogProducerConfigインスタンスとLogProducerClientインスタンスはペアで使用しなければなりません。Simple Log Service にログを送信する場合は、Alibaba Cloud アカウントまたは Resource Access Management ( RAM ) ユーザーの AccessKey ペアを使用して認証と改ざん防止を行う必要があります。モバイル アプリケーションに AccessKey ペアを保存すると、セキュリティリスクが発生します。このリスクを回避するには、モバイルデバイスからログを直接転送するサービスを使用し、サービス側で AccessKey ペアを構成します。 詳細については、「モバイルデバイスから Simple Log Service にログをアップロードするサービスを構築する」をご参照ください。
@interface ProducerExampleController ()
// LogProducerConfig インスタンスと LogProducerClient インスタンスをグローバルに保存します。
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end
@implementation ProducerExampleController
// コールバック関数はオプションです。ログが送信されたかどうかを知る必要がない場合は、コールバック関数を登録する必要はありません。
// AccessKey ペアを動的に構成するには、コールバック関数を設定し、コールバック関数が呼び出されたときに 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 {
// Simple 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"];
// タグを設定します。タグは各ログに添付されます。
[_config AddTag:@"example" value:@"example_tag"];
// 期限切れのログを破棄するかどうかを指定します。値 0 は、期限切れのログが破棄されず、ログ時間が現在の時間に更新されることを示します。値 1 は、期限切れのログが破棄されることを示します。デフォルト値: 1。
[_config SetDropDelayLog:1];
// 認証に失敗したログを破棄するかどうかを指定します。値 0 は、ログが破棄されないことを示します。値 1 は、ログが破棄されることを示します。デフォルト値: 0。
[_config SetDropUnauthorizedLog:0];
// ログが送信されたかどうかを確認する場合は、2 番目のパラメーターとしてコールバック関数を渡します。
_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 ペアを取得する場合、AccessKey ペアにはセキュリティトークンが含まれています。この場合、次の方法で AccessKey ペアを更新する必要があります。
if (securityToken.length > 0) {
if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
[_config ResetSecurityToken:accessKeyId
accessKeySecret:accessKeySecret
securityToken:securityToken
];
}
} else {
// STS を使用して AccessKey ペアを取得しない場合は、次の方法で 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 などのパラメーターの動的構成をサポートしています。 エンドポイントの取得方法については、「エンドポイント」をご参照ください。 AccessKey ペアの取得方法については、「AccessKey ペア」をご参照ください。
Endpoint、ProjectName、Logstore を動的に構成します。
// Endpoint、ProjectName、Logstore パラメーターは個別に、またはまとめて構成できます。 // エンドポイントを更新します。 [_config setEndpoint:@"your new-endpoint"]; // ProjectName を更新します。 [_config setProject:@"your new-project"]; // Logstore を更新します。 [_config setLogstore:@"your new-logstore"];AccessKey ペアを動的に構成します。
AccessKey ペアを動的に構成する場合は、コールバック関数も使用する必要があります。
// LogProducerClient を初期化するときにコールバック関数を初期化している場合は、次のコードを無視できます。 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 は現在のクラスインスタンスへの参照です。 } } // ログが送信されたかどうかを確認する場合は、2 番目のパラメーターとしてコールバック関数を渡します。 _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 ペアを取得する場合、AccessKey ペアにはセキュリティトークンが含まれています。この場合、次の方法で AccessKey ペアを更新する必要があります。 if (securityToken.length > 0) { if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config ResetSecurityToken:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken ]; } } else { // STS を使用して AccessKey ペアを取得しない場合は、次の方法で AccessKey ペアを更新します。 if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config setAccessKeyId: accessKeyId]; [_config setAccessKeySecret: accessKeySecret]; } } }source、topic、tag を動的に構成します。
重要これらの設定はグローバルに適用され、現在リトライバッファにあるログを含め、後続のすべてのログに影響します。ビジネスロジックでこれらのパラメーターを使用して特定のログタイプを追跡する必要がある場合、このグローバルな動作によって予期しない結果が生じる可能性があります。各ログにカスタムフィールドを追加して、そのタイプを識別できます。
// ログトピックを設定します。 [_config SetTopic:@"your new-topic"]; // ログソースを設定します。 [_config SetSource:@"your new-source"]; // タグを設定します。タグは各ログに添付されます。 [_config AddTag:@"test" value:@"your new-tag"];
再開可能なアップロード
iOS SDK は再開可能なアップロードをサポートしています。この機能が有効になっている場合、addLog メソッドを使用して送信されたログは、最初にローカルバイナリログ ( binlog ) ファイルに永続化されます。ローカルデータは、ログが正常に送信された後にのみ削除されます。これにより、ログのアップロードで少なくとも 1 回のセマンティクスが保証されます。
再開可能なアップロードを実装するには、SDK の初期化中に次のコードを追加します。
複数の LogProducerConfig インスタンスを初期化する場合は、各
LogProducerConfigインスタンスのsetPersistentFilePathメソッドに一意のファイルパスを指定する必要があります。アプリケーションに複数プロセスがあり、再開可能なアップロードが有効になっている場合は、メインプロセスでのみ 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];
// 各永続ファイルのサイズ ( バイト単位 )。値は式 N × 1024 × 1024 を使用して計算されます。N を 1 から 10 までの値に設定します。
[_config SetPersistentMaxFileSize:N*1024*1024];
// ローカルにキャッシュできるログの最大数。このパラメーターを 1,048,576 より大きい値に設定しないでください。デフォルト値は 65,536 です。
[_config SetPersistentMaxLogCount:65536];
}構成パラメーター
LogProducerConfig クラスのすべての構成パラメーターを設定できます。次の表に、これらのパラメーターを示します。
パラメーター | データ型 | 説明 |
SetTopic | String | topic フィールドの値を設定します。デフォルト値は空の文字列です。 |
AddTag | String |
|
SetSource | String | source フィールドの値を設定します。デフォルト値: iOS。 |
SetPacketLogBytes | Int | キャッシュされたログパッケージの最大サイズ。制限を超えると、ログはすぐに送信されます。 有効な値: 1 ~ 5,242,880。デフォルト値: 1024 × 1024。単位: バイト。 |
SetPacketLogCount | Int | キャッシュされたログパッケージ内のログの最大数。制限を超えると、ログはすぐに送信されます。 有効な値: 1 ~ 4,096。デフォルト値: 1024。 |
SetPacketTimeout | Int | キャッシュされたログを送信するためのタイムアウト期間。タイムアウト期間が経過すると、ログはすぐに送信されます。 デフォルト値: 3,000。単位: ミリ秒。 |
SetMaxBufferLimit | Int | 単一の Producer Client インスタンスが使用できる最大メモリ。制限を超えると、add_log インターフェイスはすぐに失敗を返します。 デフォルト値: 64 × 1024 × 1024。 |
SetPersistent | Int | 再開可能なアップロードを有効にするかどうかを指定します。
|
SetPersistentFilePath | String | 永続化ファイルの名前。ファイルが格納されているフォルダーが作成されていることを確認してください。複数の LogProducerConfig インスタンスを構成する場合は、名前が一意であることを確認してください。 デフォルト値は空です。 |
SetPersistentForceFlush | Int | 各 AddLog 呼び出しで強制フラッシュ機能を有効にするかどうかを指定します。
高信頼性シナリオでは、この機能を有効にします。 |
SetPersistentMaxFileCount | Int | ローテーションする永続ファイルの数。推奨値: 1 ~ 10。デフォルト値: 0。 |
SetPersistentMaxFileSize | Int | 各永続ファイルのサイズ ( バイト単位 )。値は式 N × 1024 × 1024 を使用して計算されます。N を 1 から 10 までの値に設定します。 |
SetPersistentMaxLogCount | Int | ローカルにキャッシュできるログの最大数。このパラメーターを 1,048,576 より大きい値に設定しないでください。デフォルト値は 65,536 です。 |
SetConnectTimeoutSec | IntInt | 接続タイムアウト期間。デフォルト値: 10。単位: 秒。 |
SetSendTimeoutSec | Int | ログを送信するためのタイムアウト期間。デフォルト値: 15。単位: 秒。 |
SetDestroyFlusherWaitSec | Int | フラッシャースレッドが破棄されるまでの最大待機時間。デフォルト値: 1。単位: 秒。 |
SetDestroySenderWaitSec | Int | 送信者スレッドプールが破棄されるまでの最大待機時間。デフォルト値: 1。単位: 秒。 |
SetCompressType | Int | データアップロードの圧縮タイプ。
|
SetNtpTimeOffset | Int | デバイス時間と標準時間の差。値は、標準時間 - デバイス時間の式を使用して計算されます。この差は通常、クライアントデバイスの時間が同期されていないために発生します。デフォルト値: 0。単位: 秒。 |
SetMaxLogDelayTime | Int | ログ時間と現地時間の差。差がこの値を超えると、SDK は setDropDelayLog オプションに基づいてログを処理します。単位: 秒。デフォルト値: 7 × 24 × 3600 ( 7 日間 )。 |
SetDropDelayLog | Int | setMaxLogDelayTime の値を超える期限切れのログを破棄するかどうかを指定します。
|
SetDropUnauthorizedLog | Int | 認証に失敗したログを破棄するかどうかを指定します。
|
エラーコード
すべてのエラーコードは log_producer_result で定義されています。詳細については、次の表を参照してください。
エラーコード | 値 | 説明 | 解決策 |
LOG_PRODUCER_OK | 0 | 成功。 | 該当なし。 |
LOG_PRODUCER_INVALID | 1 | SDK が破棄されているか無効です。 |
|
LOG_PRODUCER_WRITE_ERROR | 2 | データ書き込みエラーが発生しました。原因は、プロジェクトの書き込みトラフィックが上限に達している可能性があります。 | プロジェクトの書き込みトラフィックの上限を調整します。 詳細については、「リソースクォータの調整」をご参照ください。 |
LOG_PRODUCER_DROP_ERROR | 3 | ディスクまたはメモリキャッシュがいっぱいで、ログを書き込めません。 | maxBufferLimit、persistentMaxLogCount、persistentMaxFileSize パラメーターの値を調整して、再試行してください。 |
LOG_PRODUCER_SEND_NETWORK_ERROR | 4 | ネットワークエラーが発生しました。 | Endpoint、Project、Logstore の構成を確認します。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | 5 | プロジェクトの書き込みトラフィックが上限に達しました。 | プロジェクトの書き込みトラフィックの上限を調整します。 詳細については、「リソースクォータの調整」をご参照ください。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | 6 | AccessKey ペアの期限が切れているか無効であるか、アクセス ポリシーが正しく構成されていません。 | AccessKey ペアを確認します。 RAM ユーザーには、Simple Log Service リソースを管理するための権限が必要です。 詳細については、「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. システムディスクに十分な空き容量があるかどうかを確認します。 |
FAQ
重複ログが存在するのはなぜですか?
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]; } @endSDK の反復初期化のもう 1 つの原因は、複数プロセスを使用していることです。メインプロセスでのみ SDK を初期化します。異なるプロセスで SDK を初期化する必要がある場合は、プロセスごとに
SetPersistentFilePathに一意の値を指定します。
弱いネットワーク環境の構成の最適化
アプリケーションが弱いネットワーク接続環境で使用されている場合は、次の例に示すように SDK 構成パラメーターを最適化できます。
// SDK を初期化します。 - (void) initProducer() { // ログの重複率を減らすために、HTTP 接続と送信のタイムアウト期間を調整します。 // 必要に応じて、特定のタイムアウト期間を調整します。 [_config SetConnectTimeoutSec:20]; [_config SetSendTimeoutSec:20]; // その他の初期化パラメーター。 // ... }
ログが失われた場合はどうすればよいですか?
ログの送信は非同期プロセスです。ログが送信される前にアプリケーションが閉じると、ログが失われる可能性があります。これを防ぐには、再開可能なアップロードを有効にします。 詳細については、「再開可能なアップロード」をご参照ください。
ログレポートが遅延する場合はどうすればよいですか?
SDK はログを非同期で送信します。ネットワーク環境または特定のアプリケーションシナリオによっては、ログがすぐに送信されない場合があります。少数のデバイスでのみログの送信が遅延している場合は、これは正常です。それ以外の場合は、次の表のエラーコードを使用して問題のトラブルシューティングを行うことができます。
エラーコード | 説明 |
LOG_PRODUCER_SEND_NETWORK_ERROR | Endpoint、Project、Logstore パラメーターが正しく構成されているかどうかを確認します。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | AccessKey ペアの期限が切れているか有効であるか、アクセス ポリシーが正しく構成されているかどうかを確認します。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | プロジェクトの書き込みトラフィックが上限に達しました。プロジェクトの書き込みトラフィックの上限を調整します。 詳細については、「リソースクォータの調整」をご参照ください。 |
iOS SDK は DNS 事前解決とキャッシュポリシーをサポートしていますか?
次の例は、iOS SDK と HTTPDNS SDK を使用して DNS 事前解決とキャッシュポリシーを実装する方法を示しています。
注: Simple Log Service エンドポイントで 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; } // 必要に応じて、ホストアレイを構成してアレイ内のホストのみをインターセプトするなど、さらに条件を追加できます。 // ホストが 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 { // 元のリクエストのヘッダー情報。 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); } // 元のリクエストのヘッダー情報をコピーします。 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 ) ホスト情報を設定します。これは重要なステップです。 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) { // 現在のスレッドのランループを保存します。これは、リダイレクトされたリクエストにとって重要です。 self.curRunLoop = [NSRunLoop currentRunLoop]; } // 現在のランループのイベントキューにリクエストを追加します。 [_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) { // バッファには、処理される result バイトのデータが含まれています [data appendBytes:buffer length:result]; } else if (result < 0) { // ストリームにエラーが発生しました。[iStream streamError] を使用して NSError オブジェクトを取得できます data = nil; break; } } [stream close]; return data; } #pragma mark - NSStreamDelegate /** * 入力ストリームが完全なヘッダーを受信した後のコールバック関数。 */ - (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); // クライアントにレスポンスが受信されたことを通知します。この通知は 1 回だけ送信されます。 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) { // 証明書の検証に失敗した場合、入力ストリームを閉じます。 [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); // クライアントにエラーが発生したことを通知します。 [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); // クライアントにレスポンスが受信されたことを通知します。この通知は 1 回だけ送信されます。 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 { // レスポンスヘッダーが不完全な場合。 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 が出力バッファの不足が原因で発生した場合、入力バッファは完全に処理されず、出力バッファはいっぱいになります。この場合、ループはバッファを拡張し続ける必要があります。 // 反対の状態は、エラーが出力バッファの不足が原因ではないことを示しています。この場合、ループを終了する必要があり、これはエラーを示しています。 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