iPhone/iPadネットワークプログラミング(3)Bonjourを使ってデータを送る (1) Cocoa編

そろそろネットワークプログラミングを進めないとね。
前回HTTPによるデータ取得法を書いたが、これで双方向は基本的にしないよね。できないことはないけど。。。
今回は Bonjour を使って双方向にやり取りする方法を記しておく。Bonjourって何?って人は調べてね。Wikipediaにもあるし。


さて、Bonjourは零コンフィグネットワークプロトコルの一種で、サービス発行側とサービス探す側に分かれる。サーバとクライアントみたいにね。今回はサービス発行側の実装方法について書くんだけど、この実装をMacでやってみる。いきなりiPhoneは敷居が少し高いから、というよりちょいと面倒なことが起こるから。


Cocoa には、Bonjourのためのクラスが用意されているのでそれを使う。ここからはソケット通信になるので、それなりの知識がいることを書いておく。


まずはサービスを発行する。

#define PORT_NUMBER    7000

NSSocketPort* socket;  // <--クラス変数にしておく

- (void) publish
{
    socket = [[NSSocketPort alloc] initWithTCPPort:PORT_NUMBER];
    if(socket){
        NSNetService* service = [[NSNetService alloc] initWithDomain:@"" type:@"_test._tcp" name:@"NetTest" port:PORT_NUMBER];
        if(service){
            [service setDelegate:self];
            [service publish];
        }else{
            NSLog(@"NSNetServiece が失敗してるよ!");
        }
    }else{
        NSLog(@"NSSocketPortが失敗してるよ!");
    }
}

NSSocketPort の initWithTCPPort でソケットを作成する。このときポート番号を指定しているが、番号も自動で振る方法が確かあったはず。ちょいと面倒なので今回は明示的にポート番号を与えることにする。
次に NSNetService の initWithDomain: type: name: port: で発行するサービスの作成をする。ドメインは普通は空文字で問題ない。ローカル全体になる。typeは勝手に決められるが、_***._プロトコル名 とするのが一般的。name は何でも良い。ただし、サービス探す側は type, name で探すことになるので、そのことに注意して決めること。
サービスの作成に成功したら、デリゲートの登録をし、publish で発行する。発行に成功するとサービスを探す側が見つけられるようになる。


さて、サービス発行したら終わり・・・ではない。ソケットに対して、アクセス可能にしておかないと、探す側が接続できない。そこで、サービス発行に成功したら呼ばれるメソッド netServiceDidPublish: の実装をする。

- (void) netServiceDidPublish: (NSNetService*) sender
{
    NSFileHandle* socketHandle = [[NSFileHandle alloc] initWithFileDescriptor:[socket socket] closeOnDealloc:YES];
    if(socketHandle){
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(acceptConnect:) name:NSFileHandleConnectionAcceptedNotification object:socketHandle];
        [socketHandle acceptConnectionInBackgroundAndNotify];
    }
}

サービス発行に成功したら、ソケットを開かないといけない。今回は、NSFileHandle を使って行う。initWithFileDescriptor: closeOnDealloc: を使って socket のストリームをファイルハンドルへ割り当てる。次に、NotificationCenter へファイルハンドルへアクセスしてきたときのコールバックを登録する。
そしてファイルハンドルへのアクセスイベントをバックで監視する設定をする。
このサンプルだと、誰かが接続してきたら acceptConnect: が呼ばれるというわけだ。


接続時の処理を実装しよう。

NSFileHandle* readHandle; //<--クラス変数にしておく

- (void) acceptConnect:(NSNotification*)n
{
    readHandle = [[[n userInfo] objectForKey:NSFileHandleNotificationFileHandleItem] retain];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(readData:)
                                                 name:NSFileHandleDataAvailableNotification
                                               object:readHandle];
    [readHandle waitForDataInBackgroundAndNotify];
}

まず、NSNotification* の引数がある。こいつからいろいろ抜き出せるが、今回は接続に使ったファイルハンドルを取り出し、 readHandle へ retain する。今度はデータを送ってきたときに、処理するメソッドを NSNotificationCenterに登録する。あとは、データが送られてきたかどうかを裏で監視するように設定する。
今回の場合、相手からデータが送られてきたら readData: が呼ばれる。


いよいよデータ読み込みの実装だ。これでサービス発行側は最後

- (void) readData:(NSNotification*)n
{
    NSData* d = [readHandle availableData];
    NSString* str = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];

    NSLog(@"%@", str);
    [str release];
    [readHandle waitForDataInBackgroundAndNotify];
}

データは NSData*型で受け取るので、そこから変換することになる。
まず、NSFileHandleのavailableData で NSData*型のデータを取得する。
今回は文字列を送る前提で書いているので、initWithData: encoding: を使って文字列へ変換する。
再度、バックグラウンドで監視する設定をして終了。これやらないと駄目。データを受け取るたびに設定しないと駄目なので、注意!!


今回はここまで。次回はサービスを探す方をやる。サービス探す側が文字列を送るサンプルになる。
あ〜そうそう、サービス発行に失敗したときによばれるメソッドとかもあるので、本来はそれも実装すべきだが、面倒なので省略した。まぁ、なくても動くし問題ないでしょ。