« 「ずっと好きだった」が、好きだった。 | ホーム | もっとCore Foundation / CFStreamこそがtoll-freeだよ »

2011年5月13日

Core Foundationをもっと使おうじゃないか / CFHostで非同期に名前解決

ちょっと自分で必要があって調べたので、ここにまとめておこうと思う。Core Foundationネタである。

大抵のことはCocoa(というかFoundationとAppKit)レイヤーで済んでしまうので、割とこう厄介になることは少ないもんなんだけど、いざ細かいところが気になり出して手を加えようとしはじめると粒度が荒くて...なんてことが割とある。以下はそのときに調べて使ったものを少しずつ小分けにまとめてみたもの。備忘録的に行ってみよう。

***

CFHostで非同期に名前解決を行えることを知ったのでそのやり方をまとめてみた。CFHostは普通の同期解決も非同期解決もできるんだが、メインループとの親和的な統合を歌っているだけあり、非同期解決の方でこそその旨味が発揮されるというものかもしれない。っていうか同期解決なら普通にNSHost(Cocoa)でもできるし。

非同期で名前解決することで得られるメリットは「アプリの挙動が安定すること」だ。MacOSX/iOSいずれであっても、ユーザの操作にはできるだけダイレクトに応答するようにした方が、アプリが安定して動いている感じがしてよろこばれるし、また何かしらのブロッキングな処理で一見アプリの動作が止まっているように見えても、「名前解決をしているのか」「データを読もうとしているのか」がわかる、あるいは本当に異常で「フリーズして」しまっているのかを見分けられるというのはアプリケーションに対する安心感として帰ってくる。また、もし誤操作でブロッキングさせた、もしくはブロックしている間に気が変わって他のことをしたくなったなら、簡単にキャンセルして元に戻せるようにもしたい。快適なインターネットライフ(何)のために、アプリケーションの挙動はできるだけユーザに安心と信頼を与えられるようでありたいものだ。

ネットワークアプリケーションを作るときに、ブロッキングが発生するタイミングは大きく三つある。

  • 名前解決
  • TCP接続の確立(connect(),accept())
  • データの送受信

これらの状況で何も考えずに処理を書くと簡単にブロックが発生してしまう。ブロックが発生するというのは即ちGUI操作が止まってウィンドウは動かせずボタンは押せず、レインボーカーソルのお出ましである。「サーバとの通信を行います」とモーダルダイアログで告知してたまに通信を行うだけの必要最低限のネットワーク機能しか無いようなやつならまだしも、メールソフトやブラウザが頻繁に固まるようなアプリケーションばかりでは、正直快適とは言いがたい。

Cocoaではこの「データの送受信」については既に非同期処理が導入されている(NSStreamsとか)。このため大抵のケースはこれで間に合うが、前の二つ「名前解決」「connect()/accept()」については同期のみ、というか手の入れようが無い。まあDNSはローカル上でもキャッシュ機構が働いているし、NSURLなどではそこも含めて隠蔽しているはずなので(未確認だけど)、こまけえこたぁ気に(ryってことなのかもしれない。

しかしNSURLにはそぐわないがヘビーにTCP/IPをつつきたい用途なんかもやっぱりあるわけで、そうするとあとはBSDソケットレイヤーでO_NONBLOCKを...とか言いたくなるところだが、Core FoundationにはCFHostというAPIがあって、こいつで非同期クエリが飛ばせるらしいと知った(しかもNSHostのtoll-free)←勘違いでした(_ _)。

http://developer.apple.com/library/ios/#documentation/CoreFoundation/Reference/CFHostRef/Reference/reference.html
CFHost Reference

おおまかな流れとしては以下の通り。

  1. CFHostCreateWithName()でインスタンス生成
  2. CFHostSetClient()でコールバックを設定
  3. CFHostScheduleWithRunLoop()でメインループに接続
  4. CFHostStartInfoResolution()でクエリ送信
  5. 2.で設定したコールバックでクエリ応答を受信
static void
_host_client_callback(CFHostRef host, CFHostInfoType typeInfo, const CFStreamError *err, void* info)
{
    Boolean resolved = FALSE;
    
    CFArrayRef addr = CFHostGetAddressing(host, &resolved);
    
    NSLog(@"ASYNC: resolved = %d, err = %d/%ld", resolved, err->error, err->domain);
    NSLog(@"ASYNC: result = %@", addr);    
}

static void
_do_query(CFStringRef s)
{
    Boolean ret, resolved;
    CFStreamError err;
    
    CFHostRef host = CFHostCreateWithName(NULL, s);
    CFHostClientContext context = { 0, NULL, CFRetain, CFRelease, NULL };
    
    CFHostSetClient(host, _host_client_callback, &context);
    
    CFHostScheduleWithRunLoop(host, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    ret = CFHostStartInfoResolution(host, kCFHostAddresses, &err);
    {
        CFArrayRef addr;
        
        NSLog(@"SYNC: ret = %d", ret);
        
        addr = CFHostGetAddressing(host, &resolved);
        
        NSLog(@"SYNC: resolved = %d, err = %d/%ld", resolved, err.error, err.domain);
        NSLog(@"SYNC: result = %@", addr);    
    }
}

CFHostStartInfoResolution()に至る前にCFHostSetClient()を行ってコールバックを設定していなければ、非同期解決にはならずにそのままCFHostStartInfoResolution()でブロックがかかる。

そして_do_query()の引数にCFStringRefが与えられているがこれもNSStringのtoll-free bridgeがかかっているので、呼び出す時はNSStringオブジェクトにキャストして渡してやればよい。

    NSString* s = @"www.google.com";
    _do_query((CFStringRef)s);

そして実行結果は下記のようになる。

2011-05-13 12:17:54.757 CFHostTester[65231:903] SYNC: ret = 1
2011-05-13 12:17:54.760 CFHostTester[65231:903] SYNC: resolved = 0, err = 0/0
2011-05-13 12:17:54.761 CFHostTester[65231:903] SYNC: result = (null)
2011-05-13 12:17:54.860 CFHostTester[65231:903] ASYNC: resolved = 1, err = 0/0
2011-05-13 12:17:54.861 CFHostTester[65231:903] ASYNC: result = (
    <10020000 40e9b769 00000000 00000000>,
    <10020000 40e9b76a 00000000 00000000>,
    <10020000 40e9b793 00000000 00000000>,
    <10020000 40e9b763 00000000 00000000>,
    <10020000 40e9b767 00000000 00000000>,
    <10020000 40e9b768 00000000 00000000>
)

ログの出力に「SYNC:」と「ASYNC:」とそれぞれ付く箇所の出力内容に注意。SYNC:の付く箇所はCFHostStartInfoResolution()を呼んだ直後に取ったデータだが、ここには決して名前解決したデータが乗ってくることはない。何度も同じ名前解決を行って確実にDNSキャッシュに乗ったと思われる状況でも同じだったので、非同期設定を行ったCFHostではコールバックが行われるまではDNS情報はCFHostオブジェクトには渡って来ない、と見るべきなよう。もちろん、非同期設定を行わないつまりCFHostSetClient()およびCFHostScheduleWithRunLoop()を呼ばなければ、普通に「SYNC:」の行で結果は返る。

トラックバック(0)

トラックバックURL: http://foursics.jp/cgi-bin/mt/mt-tb.cgi/338

コメントする