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

今回はちゃんとアプリの形にしてみる。
タッチした座標を送って、その座標に点を描画してみる。iPhone/iPad<=>iPhone/iPadの双方向でやる。


◯まずサービス発行がわから。
Window-Basedでプロジェクトを作成する。プロジェクト名は NetTestServicePublish とする。
次に、UIViewのサブクラスとして MyViewクラスを作成し、NSObjectのサブクラスとして AppControllerクラスを作成する。これは、メニューの新規ファイルからCocoa Touch ClassのObjectic C classとして作成すればよい。
ここからコードを追加していく。まずは NetTestServicePublishAppDelegateクラスを変更する。


NetTestServicePublishAppDelegate.h で定義されている NetTestServicePublishAppDelegateクラスのメンバーに AppController のインスタンスを追加する。ただし、id で宣言しておく。

@interface NetTestServicePublishAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    id controller;
}

NetTestServicePublishAppDelegate.c に AppController.h を読み込むように、以下を追加する

#import "AppController.h"

didFinishLaunching に AppController のイニシャライズを追加する。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{    
    [self.window makeKeyAndVisible];
    [controller initController];

    return YES;
}

終了時に後始末のためのメソッドを追加しておく

- (void)applicationWillTerminate:(UIApplication *)application 
{
    [controller releaseController];
}


次は AppController クラス。これは殆どからのはずなので、色々追加していく。
ヘッダは以下のとおり

#import <Foundation/Foundation.h>

@interface AppController : NSObject <NSNetServiceDelegate> {
    IBOutlet UILabel* stateLabel;
    IBOutlet id view;
	
    NSNetService* service;
    NSFileHandle* streamHandle;
}

- (void) initController;
- (void) releaseController;

- (void) servicePublish;
- (void) acceptConnect:(NSNotification*)n;
- (void) readData:(NSNotification*)n;
- (void) sendPoint:(CGPoint)pt;

@end

ラベルとビューはAppControllerからアクセスするためのもの。ラベルには接続状態を表示する。ビューには相手からデータが来たときに、それを教えないといけない。そのためのインスタンス変数だ。
メソッドは、イニシャライズと後処理用。あとは通信用だ。
そうそう、iOS4になってから?厳密になったようで、 NSNetServiceDelegateを使っていることを宣言しないとワーニングが出る。
では、本体のほう。これまでに書いた部分の説明は省く。結構長い

#import "AppController.h"
#import "MyView.h"

#import <unistd.h>
#import <netinet/in.h>
#import <sys/socket.h>

#define PORT			8000
#define SERVICE_NAME	@"BonjureTest"
#define TYPE_NAME		@"_bonjuretest._tcp"

@implementation AppController

- (void) initController
{
    [self servicePublish];
}

- (void) releaseController
{
    if(streamHandle)
        [streamHandle closeFile];
    if(service)
        [service release];
}

- (void) servicePublish
{
    service = [[NSNetService alloc] initWithDomain:@"" type:TYPE_NAME name:SERVICE_NAME port:PORT];
    if(service){
        [service setDelegate:self];
        [service publish];
    }
}

- (void) netServiceDidPublish:(NSNetService*)sender
{
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0){
        close(sock);
        [stateLabel setText:@"サービス発行に失敗しました。"];
    }
    if(listen(sock, 1)){
        close(sock);
        [stateLabel setText:@"サービス発行に失敗しました。"];
    }
	
    NSFileHandle* socketHandle = [[NSFileHandle alloc] initWithFileDescriptor:sock
															   closeOnDealloc:YES];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(acceptConnect:) name:NSFileHandleConnectionAcceptedNotification object:socketHandle];
    [socketHandle acceptConnectionInBackgroundAndNotify];
    [stateLabel setText:@"サービス発行中"];
}

- (void) acceptConnect:(NSNotification*)n
{
    [service stop];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleConnectionAcceptedNotification object:[[n userInfo] objectForKey:NSFileHandleNotificationFileHandleItem]];
	
    streamHandle = [[[n userInfo] objectForKey:NSFileHandleNotificationFileHandleItem] retain];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(readData:) name:NSFileHandleDataAvailableNotification object:streamHandle];
    [streamHandle waitForDataInBackgroundAndNotify];
    [stateLabel setText:@"接続中"];
}

- (void) readData:(NSNotification*)n
{
    NSData* rData = [streamHandle availableData];
    if([rData length] > 0){
        CGPoint pt;
        memcpy(&pt, [rData bytes], sizeof(CGPoint));
        [view setReceivePoint:pt];
        [view setNeedsDisplay];
        [streamHandle waitForDataInBackgroundAndNotify];
    }else{
        [stateLabel setText:@"切れました。"];
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleDataAvailableNotification object:streamHandle];
        [streamHandle closeFile];
        [self servicePublish];
    }	
}

- (void) sendPoint:(CGPoint)pt;
{
    if(streamHandle){
        NSData* sData = [NSData dataWithBytes:&pt length:sizeof(pt)];
        [streamHandle writeData:sData];
    }
}

@end

ほとんどこれまでと同じはず。releaseControllerで後処理(releaseやcloseね)を行っている。あと、readDataが少し違うので説明しておくと、相手がアプリを終了するなどして通信路を切断したときも readData が呼ばれる。このとき rDataに格納されるデータサイズは0である。これを利用して相手からの切断を判定している。今回は、切れたら、ストリームを閉じてサービスの発行を行なっている。


最後に MyView クラスのコーディングを行う
MyView.hは以下

#import <UIKit/UIKit.h>

@interface MyView : UIView {
	id controller;
	
	CGPoint touchedPoint;
	CGPoint receivePoint;
}

@property (nonatomic, assign) CGPoint receivePoint;

@end

受け取った座標は外からアクセスできるように property で設定しておく。
MyVIew.m は以下

#import "MyView.h"
#import "AppController.h"

@implementation MyView

@synthesize receivePoint;

- (id)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code.
    }
    return self;
}

- (void) awakeFromNib
{
    touchedPoint.x = touchedPoint.y = receivePoint.x = receivePoint.y = -100;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetRGBFillColor(ctx, 1, 0, 0, 1);
    CGContextFillEllipseInRect(ctx, CGRectMake(touchedPoint.x-10, touchedPoint.y-10, 20, 20));
    CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
    CGContextFillEllipseInRect(ctx, CGRectMake(receivePoint.x-10, receivePoint.y-10, 20, 20));
}

- (void)dealloc {
    [super dealloc];
}

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    touchedPoint = [[touches anyObject] locationInView:self];
    [controller sendPoint:touchedPoint];
    [self setNeedsDisplay];
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    touchedPoint = [[touches anyObject] locationInView:self];
    [controller sendPoint:touchedPoint];
    [self setNeedsDisplay];
}

@end

initWithFrameは削除しても良い。このViewは Interface Builder で追加するので、init系はほとんど呼ばれない(initWithCorderだけ別)。なので awakeFromNibで初期化を行う。


以上でコーディングは終わり。最後に Interface Builder で MyView と AppController と状態表示用ラベルを追加し、接続設定をして終わりだ。
UIは図のとおり。

Labelの下に Viewが貼ってあり、クラスを MyViewクラスに変更してある。また、Objectを追加して、クラスをAppControllerに追加してある。
接続は、Net Test Service Publish App Delegate→AppController、MyView→AppController、AppController→LabelとMyViewだ。


あと、これはどちらでも良いんだけども、NetTestServicePublish-Info.plist にApplication does not run in backgroundを加えてチェックを入れておく。こうしておけばアプリがバックグラウンドで走らなくなるので。こっちをデフォにして欲しかったなぁ・・・。


◯サービスさがす側の実装
さて、ようやくさがす側だ。こちらも Window-Basedで作成する。名前はNetTestServiceSearchする。
発行側と同様に MyViewとAppControllerを追加する。
AppDelegateの編集も同じで、controller、initController、releaseControllerを追加する。
MyViewも同じソースでOK。
ついでに Interface Builder での作業も同じだ。
違うのは AppControllerのソースだけ。
AppController.h は以下

#import <Foundation/Foundation.h>

@interface AppController : NSObject <NSNetServiceBrowserDelegate, NSNetServiceDelegate, NSStreamDelegate> {
    IBOutlet UILabel* stateLabel;
    IBOutlet id view;

    NSNetServiceBrowser* browser;
    NSNetService* service;
    NSInputStream *iStream;
    NSOutputStream *oStream;
}

- (void) initController;
- (void) releaseController;

- (void) searchService;
- (void) sendPoint:(CGPoint)pt;

@end

後処理がしやすいようにいろいろクラス変数にしている。注意点は、発行側と同様で、デリゲートを使っていることを明示しないとワーニングが出る点だ。
AppController.mは以下

#import "AppController.h"
#import "MyView.h"

#define TYPE_NAME		@"_bonjuretest._tcp"

@implementation AppController

- (void) initController
{
    iStream = nil;
    oStream = nil;
    [self searchService];
}

- (void) releaseController
{
    [oStream close];
    [iStream close];
    [oStream release];
    [iStream release];
}

- (void) searchService
{
    browser = [[NSNetServiceBrowser alloc] init];
    [browser setDelegate:self];
    [browser searchForServicesOfType:TYPE_NAME inDomain:@""];
}

- (void) netServiceBrowser:(NSNetServiceBrowser*)browser didFindService:(NSNetService*)netService moreComing:(BOOL)moreComing
{
    service = [[NSNetService alloc] initWithDomain:[netService domain] type:[netService type] name:[netService name]];
    if(service){
        [service setDelegate:self];
        [service resolveWithTimeout:5.0];
    }else{
        [stateLabel setText:@"サービス発見したけど繋げなかった"];
    }
}

- (void) netServiceDidResolveAddress:(NSNetService*)netService
{
    [netService getInputStream:&iStream outputStream:&oStream];
    if(oStream && iStream){
        [oStream retain];
        [oStream open];
        [iStream retain];
        [iStream setDelegate:self];
        [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [iStream open];
        [stateLabel setText:@"接続しました"];
        [browser release];
        [service release];
    }
}

- (void) sendPoint:(CGPoint)pt
{
    if(oStream){
        NSData* sData = [NSData dataWithBytes:&pt length:sizeof(pt)];
        [oStream write:[sData bytes] maxLength:[sData length]];
    }
}

- (void) stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode
{
    uint8_t buf[100];
    switch(eventCode){
        case NSStreamEventHasBytesAvailable:
            memset(buf, 0, sizeof(buf));
            [iStream read:buf maxLength:100];
            CGPoint pt;
            memcpy(&pt, buf, sizeof(CGPoint));
            [view setReceivePoint:pt];
            [view setNeedsDisplay];
            break;
    }
}

@end

さがす側もこれまでとほぼ同じ。スコープが変わっている程度で、終了時にストリームを閉じることを行っている。また、ストリーム取得時に念のため retain を行っているので、その開放も終了時に処理する。


以上で終了。通信がちゃんとできていれば、タッチした場所にちょっと大きめの点(円)が両方に表示されるはずだ。
でもこれ、streamで受け取る方って結構面倒なんだよね。NSFileHandleの方は NSDataで一括でうけとれるんだけど、streamの方は配列用意しないといけないからねぇ。一応これ、解消する方法あるんで、それはまた今度ということで。と言っても次回はIPアドレス指定で通信する方法について書くことにする。


あー、やっぱ長いのは面倒だ。通信以外の説明重っきり省いたけど大丈夫だったか。。。ま、いっか。そのへんは調べればゴロゴロ出てくるし。