聊天至少需要同時(shí)運(yùn)行兩個(gè) MacChatty 終端,其中至少有一個(gè)作為服務(wù)器,其他終端才能作為客戶端連接到服務(wù)器進(jìn)行對(duì)話。作為服務(wù)器的終端,需要?jiǎng)?chuàng)建一個(gè) socket 來(lái)監(jiān)聽(listen)其他終端的連接請(qǐng)求(請(qǐng)參考 Sever class 中的 listeningSocket)。這項(xiàng)工作是在 Server 類中的 createServer 中完成的。
客戶端如何知道怎樣連接到服務(wù)器呢?每一個(gè)網(wǎng)絡(luò)終端必須有獨(dú)一無(wú)二的 ip 和 port,ip 地址是由動(dòng)態(tài)獲取的或由用戶設(shè)定的,因此我們?cè)谶@里無(wú)需操心 ip 地址問(wèn)題,因此在代碼中我們使用了 INADDR_ANY。那又如何設(shè)定我們想要監(jiān)聽的 port 呢?一些服務(wù)必須監(jiān)聽約定的 port 才能工作,比如 80,20, 21等端口都是有約定用途的。在這里我們把端口設(shè)定問(wèn)題交給 OS 來(lái)處理,OS 會(huì)為我們?cè)O(shè)定一個(gè)沒有被占用的 port。為了實(shí)現(xiàn)這個(gè)目的,我們傳入 port 為 0。為了讓其他客戶端能夠連接到服務(wù)器,我們需要告知其他客戶端服務(wù)器實(shí)際使用的 port,因此,我們?cè)?createServer 方法 PART 3中獲取實(shí)際使用 port。
//// PART 3: Find out what port kernel assigned to our socket
//
// We need it to advertise our service via Bonjour
NSData *socketAddressActualData = [(NSData *)CFSocketCopyAddress(listeningSocket) autorelease];
// Convert socket data into a usable structure
struct sockaddr_in socketAddressActual;
memcpy(&socketAddressActual, [socketAddressActualData bytes], [socketAddressActualData length]);
self.port = ntohs(socketAddressActual.sin_port);
然后在 PART 4 中,我們將 listening socket 注冊(cè)為 application run loop 的消息源,這樣當(dāng)有新連接到來(lái)的時(shí)候, OS 就會(huì)調(diào)用 serverAcceptCallback 這個(gè)回調(diào)函數(shù)通知我們。
//// PART 4: Hook up our socket to the current run loop
//
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
CFRunLoopSourceRef runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, listeningSocket, 0);
CFRunLoopAddSource(currentRunLoop, runLoopSource, kCFRunLoopCommonModes);
CFRelease(runLoopSource);
在 serverAcceptCallback 回調(diào)處理中,我們創(chuàng)建一個(gè)新的 Connection 對(duì)象,然后將它與 OS 自動(dòng)創(chuàng)建的響應(yīng)新連接的 socket 綁定起來(lái)。然后再將這個(gè) Connection 對(duì)象傳遞給 Server delegate。
通過(guò) Bonjour 發(fā)布服務(wù)
Bonjour 并非在網(wǎng)絡(luò)查找服務(wù)的唯一途徑,但它是最容易使用的方法之一。我們?cè)?publishService 方法中創(chuàng)建一個(gè) NSNetService 對(duì)象來(lái)發(fā)布服務(wù)。我們根據(jù)服務(wù)類型在網(wǎng)絡(luò)查找感興趣的服務(wù),本聊天服務(wù)使用“_chatty._tcp.”作為服務(wù)類型。在同一網(wǎng)絡(luò)中,服務(wù)類型名必須唯一,這樣才能精準(zhǔn)定位服務(wù),而不至于引發(fā)沖突。
Bonjour 操作也如 socket 一樣需要異步進(jìn)行,以避免長(zhǎng)時(shí)間阻塞主線程。因此在實(shí)際發(fā)布服務(wù)時(shí),我們將發(fā)布任務(wù)交給當(dāng)前 run loop 去調(diào)度,然后設(shè)定其 delegate,由 delegate 來(lái)處理相關(guān)事件:“Publishing succeeded”, “Publishing failed”等。
- (BOOL) publishService
{
// come up with a name for our chat room
NSString* chatRoomName = [NSString stringWithFormat:@"%@'s chat room", [[AppConfig sharedInstance] name]];
// create new instance of netService
self.netService = [[NSNetService alloc] initWithDomain:@"" type:@"_chatty._tcp." name:chatRoomName port:self.port];
if (self.netService == nil)
return NO;
// Add service to current run loop
[self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// NetService will let us know about what's happening via delegate methods
[self.netService setDelegate:self];
// Publish the service
[self.netService publish];
return YES;
}
通過(guò) Bonjour 查詢服務(wù)
我們?cè)?ServerBrowser 類中實(shí)現(xiàn) Bonjour 查詢網(wǎng)絡(luò)服務(wù)的功能。我們創(chuàng)建一個(gè) NSNetServiceBrowser 對(duì)象來(lái)查詢類型為 “_chatty._tcp.” 的服務(wù)。當(dāng)前網(wǎng)絡(luò)中發(fā)現(xiàn)有服務(wù)被添加到或移除時(shí),NSNetServiceBrowser 的 delegate 即我們的 ServerBrowser 就能得到通知,以進(jìn)行相應(yīng)的邏輯處理:更新服務(wù)列表,刷新 UI 等。
通過(guò) Bonjour 決議服務(wù)
當(dāng)用戶選擇其中一個(gè) chat room,并加入其中時(shí),客戶端將會(huì)連接到發(fā)布該 chat room 服務(wù)的服務(wù)器。這個(gè)連接過(guò)程在 ChattyViewController 類的 joinChatRoom: 方法中實(shí)現(xiàn)。首選我們通過(guò)選擇的 NSNetService 發(fā)送 resolveWithTimeout: 消息來(lái)進(jìn)行決議應(yīng)該連接到哪個(gè)服務(wù)器(請(qǐng)參考 Connection 類的 connect 方法中最后一種情形),同時(shí)設(shè)定 NSNetService 的 delegate 來(lái)響應(yīng)決議相關(guān)的事件:didNotResolve: 和 netServiceDidResolveAddress:。當(dāng)決議完成之后,在 netServiceDidResolveAddress: 方法中,我們可以創(chuàng)建用于數(shù)據(jù)傳輸?shù)?stream 了。
// Called when net service has been successfully resolved
- (void)netServiceDidResolveAddress:(NSNetService *)sender
{
if ( sender != netService ) {
return;
}
// Save connection info
self.host = netService.hostName;
self.port = netService.port;
// Don't need the service anymore
self.netService = nil;
// Connect!
if ( ![self connect] ) {
[delegate connectionAttemptFailed:self];
[self close];
}
}
至此,Bonjour 網(wǎng)絡(luò)編程介紹就結(jié)束了,代碼中的注釋相當(dāng)詳細(xì),細(xì)節(jié)就不多羅嗦了。
參考資料: