iPhoneでBonjour使ってホストを見つける/見つけてもらう

OSC(Open Sound Control)のパケットを送信する/受信するまでやった続き。
送受信する相手のIPとポートをちまちま手入力するのは禿げるので、Bonjourでかっこよく設定できるようにする。


まずは相手に見つけてもらう方。
Bonjour

でホストを探索するから、NSNetServiceでサービスを開始すればいい。


OSCの場合はサービス名は_oscが推奨っぽい。
supercollider、OSCulator、TouchOSCは_osc._udpでOKだし、vvoscの実装も_osc._udpになってた。
コードは大体こんな感じ。

NSString *domain = @"local";
NSString *protocol = @"_osc._udp";
NSString *name = @"";
int portNumber = 12345;

self.netService = [[NSNetService alloc] initWithDomain:domain type:protocol name:name port: portNumber];	
[self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.netService publish];
[self.netService setDelegate:self];


ソケットつくるのは全部ObjeCOSCの中でやってるから今回は不要だけど、念のためUDPの受信用ソケット作るコードは大体こんな感じ。
iPhoneじゃNSSocketPortが無いのでCFSocketCreate使う。
BSDソケットの単なるラッパーなのでやることはUNIX Cと手順は同じ。

struct sockaddr_in myAddress;
	
myAddress.sin_family = AF_INET;
myAddress.sin_port = htons(12345);
myAddress.sin_addr.s_addr = htonl(INADDR_ANY);
	
CFSocketContext socketContext;
	
socketContext.version = 0;
socketContext.info = (void *) self;
socketContext.retain = NULL;
socketContext.release = NULL;
socketContext.copyDescription = NULL;
	
socket = CFSocketCreate(
	 NULL,
	 PF_INET,
	 SOCK_DGRAM,
	 IPPROTO_UDP,
	 kCFSocketDataCallBack,
	 SocketCallback,
	 &socketContext);
	
CFDataRef addressData = CFDataCreateWithBytesNoCopy(NULL, (UInt8 *)&myAddress, sizeof(struct sockaddr_in), kCFAllocatorNull);
if (kCFSocketSuccess != CFSocketSetAddress(socket, addressData)) {
	CFSocketInvalidate(socket);
	CFRelease(socket);
}

CFRunLoopRef cfrl = CFRunLoopGetCurrent();
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
CFRunLoopAddSource(cfrl, source, kCFRunLoopCommonModes);
CFRelease(source);

もちろんCで

socket(AF_INET, SOCK_DGRAM, 0);

とか書いても動く。OSCInPortの中はこっち。


当たり前だけど同じポート使う受信ソケットは2つ作れないからCFSocketSetAddressで失敗する。
ObjCOSC使う時はOSCInPort中でもソケット作ってるからポート番号に注意。


ちなみにTCPだと

CFSocketCreate(
	kCFAllocatorDefault, 
	PF_INET, SOCK_STREAM, 
	IPPROTO_TCP, 
	kCFSocketAcceptCallBack, 
	SocketCallback, 
	&socketContext);

とか。Appleのサンプルにいかにもなのがあったと思うので詳しくはそっち参照で。
以上で、受信側終わり。


次はNSNetServiceBrowser使って送信先を探す。


delegateを指定して探索開始。

[self.netServiceBrowser stop];
	
NSNetServiceBrowser *myNetServiceBrowser = [[NSNetServiceBrowser alloc] init];

myNetServiceBrowser.delegate = self;
self.netServiceBrowser = myNetServiceBrowser;
[aNetServiceBrowser release];

[self.netServiceBrowser searchForServicesOfType:@"_osc._udp" inDomain:@"local"];

NSNetServiceBrowserのdelegateメソッドは

- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing;
- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing;

が最低限必要。

上から、

  • ネットワーク上で該当するサービスが見つかったとき
  • ネットワーク上で該当するサービスがいなくなったとき

に呼ばれる。

- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing {
	//見つかったNSNetServiceをNSMutableArrayに追加しておくなど適宜やっとく
	[self.services addObject:service];
		
	if (!moreComing) {
		//見つかったのがこれで全部なのでUIの書き換えとかやる
	}
}

ソースには書いてないけどNSNetServiceでサービスを開始してる場合は自分もdidFindServiceで見つかってややこしいので、適宜チェックして排除するようにしたほうがいい。
didRemoveServiceの時はホスト名を解決しようとしてるNSNetServiceだった場合はストップさせてから消す。

- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing {
	 //解決中のNSNetServiceかどうかチェック
	 if (self.currentService && [service isEqual:self.currentService]) {
		[self.currentService stop];
		self.currentService = nil;
	}
	
	//いなくなったNSNetServiceを消す
	[self.services removeObject:service];
	
	if (!moreComing) {
		//UIの書き換えとかやる
	}
}	


機器のホストのマシン名はnameプロパティで簡単に取得できる。

NSLog(@"HostName: &@", [service name]);


でも、肝心のIPとポートの取得はちょっと面倒。
delegateを指定してから解決開始。

[self.currentService setDelegate:self];
[self.currentService resolveWithTimeout:0.0];

ホスト名解決で呼ばれるdelegateメソッドは以下。
解決した時と失敗した時。

- (void)netServiceDidResolveAddress:(NSNetService *)sender
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict


解決した場合のIPとポート番号の取得は以下。
IPV6はよく分からないので放棄。

- (void)netServiceDidResolveAddress:(NSNetService *)service {
	NSString *ip;
	NSString *port;
	
	if ([[service addresses] count] > 0) {
		NSData *address;
		struct sockaddr *socketAddress;
		
		char buffer[256];
		int index;
		
		for (index = 0; index < [[service addresses] count]; index++) {
			address = [[service addresses] objectAtIndex:index];
			socketAddress = (struct sockaddr *)[address bytes];
			
			if (socketAddress->sa_family == AF_INET) break;
		}
		
		if (socketAddress) {
			switch(socketAddress->sa_len) {
				case sizeof(struct sockaddr_in):
					if (inet_ntop(AF_INET, &((struct sockaddr_in *)socketAddress)->sin_addr, buffer, sizeof(buffer))) {
						ip = [NSString stringWithCString:buffer];
					}
					
					port = [NSString stringWithFormat:@"%d", ntohs(((struct sockaddr_in *)socketAddress)->sin_port)];
					break;
				case sizeof(struct sockaddr_in6):
					NSLog(@"IPV6 no support yet");
					return;
			}
		}
	}
	
	NSLog(@"IP: &@", ip);
	NSLog(@"Port Number: &@",port);
}


あとは適宜IPとポートをOSCPortに割り当てるだけ。
これで面倒くさい初期設定でミスったりしないで済む。