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アドレス指定で通信する方法について書くことにする。
あー、やっぱ長いのは面倒だ。通信以外の説明重っきり省いたけど大丈夫だったか。。。ま、いっか。そのへんは調べればゴロゴロ出てくるし。