2010/11/13

CFNetworkフレームワークを用いた入力HTTPストリームの作成

前回の記事「NSURLConnectionを用いたURLローディングシステムを用いた通信」について説明しました。今回はBSDソケットとNSURLの中間に位置するフレームワークであるCFNetworkフレームワークを用いた入力HTTPストリームの作成について説明します。詳細についてはAppleのドキュメント「CFNetwork Programming Guide」の「Communicating with HTTP Servers」の章を参照してください。

CFNetworkフレームワークを用いた入力HTTPストリームによる通信の手順は次の通りです。

  1. 入力ストリームを作成
  2. クライアントとなるコールバック関数を設定
  3. 入力ストリームを開く
  4. コールバック関数が入力を処理
  5. 入力ストリームを閉じる

1. 入力ストリームの作成

CFHTTPMessageRef httpRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
                                                          CFSTR("GET"),
                                                          url,
                                                          kCFHTTPVersion1_1);
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault,
                                                              httpRequest);

HTTPストリーム作成は上記のように作成します。ファイルストリーム作成なら次のように作成します。

CFReadStreamRef readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL);

2-(a). クライアントコールバック関数の作成

void readHttpStreamCallBack(CFReadStreamRef stream,
     CFStreamEventType event, void* info);

まず、入力データを処理するクライアントとなる上記のようなコールバック関数を用意します (詳細は後述)。入力ストリームにおいては、イベントkCFStreamEventHasBytesAvailableが発生したときに、関数CFReadStreamReadを用いることでデータを読み込むことができます。

2-(b). クライアントの設定

CFReadStreamSetClient(readStream,
 kCFStreamEventHasBytesAvailable | kCFStreamEventOpenCompleted | kCFStreamEventErrorOccurred,
 &readHttpStreamCallBack, &context);

CFReadStreamSetClientではコールバックを設定するイベントマスクでは受け入れたいCFStreamイベントを設定します。第1引数はストリーム、第2引数がイベントマスク、第3引数はコールバック、第4引数がストリームのコンテキスト情報になります (第4引数はコピーされるので、この引数が示したメモリを保持する必要はない)。第2, 3, 4引数でNULL指定するとそのストリームに対するクライアントが削除されます。

第4引数のCFStreamClientContext構造体は次の通りです。

struct CFStreamClientContext {
   CFIndex version;
   void* info;
   void* (*retain)(void* info);
   void (*release)(void* info);
   CFStringRef (*copyDescription)(void* info);
} CFStreamClientContext;
  • version
    整数。現在は0のみが使用可能
  • info
    ユーザ定義の情報を含んでおり、クライアントがストリームに登録されている限り有効であるような、割り当て済みメモリへのポインタ。 もし3つのコールバック関数retain, release, copyDescriptionがユーザ定義の情報を必要としないときには、NULLでもよい
  • retain
    infoで指されたデータをretainするためのコールバック関数。 この関数ポインタはNULLでもよい
  • release
    infoで指されたデータをreleaseするためのコールバック関数。 この関数ポインタはNULLでもよいが、その結果メモリリークが発生するかもしれない
  • copyDescription
    infoで指されたデータの文字情報を提供するためのコールバック関数。 この関数の実装ではアロケータやユーザ定義データの特性について述べたCFStringオブジェクトへの参照を返すこと。 この関数ポインタはNULLでもよく、その場合Core Foundationは基本的な情報を提供する

フィールドinfoはCFReadStreamSetClientで指定したコールバック関数の第3引数になります。残り3つのコールバックはinfo自体で通常用いるはずのretain, release, copyDescriptionを定義します。この3つをNULLにしたとき、infoは自分で管理することになります。

CFStreamClientContext myContext = {
    0, self,    
    (void* (*)(void* info))CFRetain,
    (void  (*)(void* info))CFRelease,
    (CFStringRef (*)(void* info))CFCopyDescription
};

なお、関数CFReadStreamSetClientの説明は次の通りです。

ポーリングやブロッキングを避けるため、ストリーム上で発生するイベントの中から関心のあるもののみを受け入れるように登録することができる。ひとつのストリームあたりひとつのクライアントのみが許可されているので、新規クライアント登録では以前のものが置き換えられる。

クライアントをセットした後、クライアントが非同期通知を受けられるように、CFReadStreamScheduleWithRunLoopを呼び出してランループ下でのストリームのスケジュールを行う必要がある。(例えば、スレッドプールを用いるなどで) 複数のランループ下での各ストリームのスケジュールを行うこともできる。スケジュールされたランループの少なくともひとつが起動していることを保証するのは呼び出し側の責任である。ひとつも起動していなければコールバックが呼び出されることはない。

現在、全てのCore Foundationストリームでは非同期通知をサポートしているが、将来のストリームがそうとは限らない。もし、ストリームが非同期通知をサポートしないならば、この関数はfalseを返す。通常、そのようなストリームは決っしてデバイスI/Oをブロックしない (例えば、メモリ読み込みストリーム) ので、同期通知による利点がないためである。

3. 入力ストリームのオープン

CFReadStreamOpen(readStream);

なお、CFReadStreamOpenはタイムアウトしないため、NSTimerなどを用いて自分でタイムアウト処理を行う必要があります。

4. コールバック関数の処理

void readHttpStreamCallBack(CFReadStreamRef stream, CFStreamEventType event, void* info) {
    switch(event) {
        case kCFStreamEventOpenCompleted:     // (省略)
        case kCFStreamEventHasBytesAvailable: // (省略)
        case kCFStreamEventErrorOccurred:     // (省略)
        case kCFStreamEventEndEncountered:    // (省略)
    }
}

このコールバック関数は次のようなイベントをトリガとして呼び出されます。なお、トリガとするかどうかをイベントマスクで指定することで、関数CFReadStreamSetClientで設定可能です。

  • kCFStreamEventNone
    イベントの発生なし
  • kCFStreamEventOpenCompleted
    ストリームのオープンの完了
  • kCFStreamEventHasBytesAvailable
    ストリームに読み込みデータあり
  • kCFStreamEventCanAcceptBytes
    ストリームがデータ書き込みを受け入れ可能
  • kCFStreamEventErrorOccurred
    ストリーム上でエラー発生
  • kCFStreamEventEndEncountered
    ストリームの終端に到達

5. 入力ストリームを閉じる

CFReadStreamClose(stream);
CFRelease(stream);

ソースコード

- (void)startDownload:(UIButton*)button {
    CFStringRef urlstr = CFSTR("http://www.example.com/sample.mp3");
    CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, urlstr, NULL);

    CFHTTPMessageRef httpRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), url, kCFHTTPVersion1_1);
    CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, httpRequest);

    CFStreamClientContext context = {0,self,NULL,NULL,NULL};
    CFOptionFlags networkEvents =  kCFStreamEventOpenCompleted  | kCFStreamEventHasBytesAvailable |
                                   kCFStreamEventEndEncountered | kCFStreamEventErrorOccurred;
    if (CFReadStreamSetClient(readStream, networkEvents, &readHttpStreamCallBack, &context)) {
        CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    }
    CFReadStreamOpen(readStream);
}

// ストリームのダウンロードが完了したらファイルに書き込む (あまりよい例ではない)
static NSMutableData* myData = nil;
void readHttpStreamCallBack(CFReadStreamRef stream, CFStreamEventType event, void* info) {
    switch(event) {
        case kCFStreamEventOpenCompleted:
            myData = [[NSMutableData alloc] init];
            break;
        case kCFStreamEventHasBytesAvailable: {
            UInt8 buf[2048];
            CFIndex bytesRead = CFReadStreamRead(stream, buf, sizeof(buf));
            if (bytesRead > 0) {
                [myData appendBytes:buf length:bytesRead]
            }
            // bytesReadが0以下になるときは別のイベントが発生するため、考慮する必要はない
        } break;
        case kCFStreamEventErrorOccurred: {
            CFErrorRef error = CFReadStreamCopyError(stream);
            myReportError(error);

            [myData release];

            CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
                                              kCFRunLoopCommonModes);
            CFReadStreamClose(stream);
            CFRelease(stream);
        } break;
        case kCFStreamEventEndEncountered:
            NSString* path = [NSHomeDirectory() stringByAppendingPathComponent:@"saved.mp3"];
            [myData writeToFile:path atomically:YES];
            [myData release];

            CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
                                              kCFRunLoopCommonModes);
            CFReadStreamClose(stream);
            CFRelease(stream);
            break;
    }
}

まとめ

CFNetworkフレームワークを用いた入力HTTPストリームの作成について説明しました。

関連項目

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。