11月末にAsyncUdpSocketのGCD対応版であるGCDAsyncUdpSocketが公開されていました。
過去に記事を書いたAsyncUdpSocketクラスを使うと、Cocoaアプリで簡単にUDP通信を実装できるのですが、今回はそれのGrand Central Dispatchに対応したバージョンが公開されていました。(TCP版であるGCDAsyncSocketは既に大分前にリリースされていました)2012年1月13日現在のリビジョンでは、ARCには対応していません。
GCDAsyncUdpSocketの入手
GCDAsyncUdpSocketはcocoaasyncsocketというプロジェクトの一部として開発されています。github上で開発/管理されているので、最新のソースコードをgithubからcloneしてきましょう。
git clone https://github.com/robbiehanson/CocoaAsyncSocket.git
cloneしてできたディレクトリの中のGCDディレクトリにあるGCDAsyncUdpSocket.h, GCDAsyncUdpSocket.mを使います。
インストール
基本的にはGCDAsyncUdpSocket.hとGCDAsyncUdpSocket.mをプロジェクトに追加するだけです。iOSアプリの場合はCFNetwork.frameworkを追加する必要があります。Macアプリの場合は必要ありません。
初期化と解放
初期化
ソースコード内には下記のような記述があり、socketを使うときには必ずデリゲートとキューを指定しなければいけません。
You MUST set a delegate AND delegate dispatch queue before attempting to use the socket, or you will get an error.
イニシャライザで指定できるので、指定しておきましょう。
GCDAsyncUdpSocket* sock = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
なお、GCDAsyncUdpSocketはGCDを使った実装になっていますが、パケットを受信した際のコールバック等はランループ版のAsyncUdpSocket同様に通常のデリゲートメソッドで通知されます。ランループ版同様、デリゲート用にGCDAsyncUdpSocketDelegateというプロトコルが用意されていますが、このdelegateは特にGCDAsyncUdpSocketDelegateに準拠していなくても警告はでません。(デリゲートメソッドを@optionalにする等して、delegateはこのプロトコルに準拠させるようにしたほうがいいと思うんだけどな…)
ランループ版と違って、受信したパケットを処理する際に使うキューを指定可能になっています。メインキューで問題ない場合は、dispatch_get_main_queue()を使えば良いですが、メインキューが埋まっている時にも裏でパケットを処理できるようにしたい場合などに、独自のキューを設定することができ、便利です。
// このqueueは後でreleaseしてね dispatch_queue_t queue = dispatch_queue_create("com.example.foo", NULL); GCDAsyncUdpSocket* sock = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:queue];
あるいは、グローバルキューも指定可能です。
解放
必要なくなったタイミングでreleaseを呼んで解放します。また、closeを呼んでソケットを閉じてやります。
[udpSock close]; [udpSock release];
基本的には、UDP送受信を扱う適当なクラスのメンバ変数としてAsyncUdpSocketのインスタンスを一つ用意しておき、それを使い回す形にするのがよいでしょう。インスタンスを複数作ったり、送信の度に作るのはわかりにくくなったり負荷が大きくなるので避けましょう。
データの送信
connectして使う方法とそうでない方法がありますが、とりあえずここではわかりやすいconnectしない使い方について書いています。
送信方法はランループ版と変わりません。sendData:toHost:port:withTimeout:tag:メソッドを使います。
NSString* someString = @"foobar"; [udpSock sendData:[someString dataUsingEncoding:NSASCIIStringEncoding] toHost:@"localhost" port:8000 withTimeout:100 tag:0];
送るデータはNSData型なので、文字列を送りたい場合はdataUsingEncoding等を使って変換する必要があります。
データの受信
受信部分はランループ版と少々使い方が異なります。
準備と受信の開始
何番のポートで受信するかをbindToPort:error:を使って設定します。(ここではエラーハンドリングのコードを省略してますが、通常はエラー処理を書いてください)。bindしたら、beginReceivingを呼ぶと届いたパケットが処理されるようになります。
GCDAsyncUdpSocket* sock = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; NSError* error; [sock bindToPort:8010 error:&error]; [sock beginReceiving:&error];
受信イベント
パケットを受信すると、delegateのonUdpSocket:didReceiveData:fromAddress:withFilterContext:が呼ばれます。当然ここで受信したデータもNSData型です。文字列として扱いたい場合はここでも変換を行います。
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)filterContext { NSLog(@"Received: %@", [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]); return YES; }
ランループ版と違って、receiveWithTimeout:tag:を呼ぶ必要はありません。
受信を中断したい場合は、pauseReceivingを呼びます。あるいは、socketをクローズすると以降は受信イベントが発生しなくなります。
フィルタリング
GCDAsyncUdpSocketではパケットのフィルタリングができます。
[sock setReceiveFilter:^BOOL(NSData *data, NSData *address, id *context) { // ここでフィルタリングをする return YES; } withQueue:dispatch_get_main_queue()];
データを受信するとudpSocket:didReceiveData:fromAddress:withFilterContext:が呼ばれる前に、setReceiveFilterで指定したブロックが実行され、ここでNOを返したデータについてはudpSocket:didReceiveData:fromAddress:withFilterContext:が呼ばれなくなります。あるいは、ここでフィルタリングをした結果を*contextにセットしておいて、デリゲート側の処理を書きやすくすることもできます。
このフィルタリングはデリゲートのキューとは別のキューで動作させることができるので、フィルタリングをかけたからといってデリゲート側でのイベントを遅延させずにすむようです。
結論
ランループ版とそこまで使い方は変わりませんが、receiveWithTimeout:tag:を呼ばなくてよくなっていたり、GCDを使うことでアプリ全体でみると処理が効率化される等いくつかメリットがあります。個人的には、受信部分(udpSocket:didReceiveData:fromAddress:withFilterContext:)もブロックで指定できたらよいのになあと思います。
一方で、ランループ版では送受信スレッドの処理の優先度を簡単にコントロールする方法があるのに対して、GCD版では優先度のコントロールが難しいため、通信の安定が求められるプログラムでは引き続きランループ版を使うべきかもしれません。詳しくはこちらの記事をどうぞ。
新たに加わったフィルタリングについては、うまく活用すればファイヤーウォールを自前で作る必要がある場合等に活躍しそうですが、実際に使ってみて良いパターンが見えてきた頃にまた考察してみましょう。