聊天至少需要同時運行兩個 MacChatty 終端,其中至少有一個作為服務(wù)器,其他終端才能作為客戶端連接到服務(wù)器進(jìn)行對話。作為服務(wù)器的終端,需要創(chuàng)建一個 socket 來監(jiān)聽(listen)其他終端的連接請求(請參考 Sever class 中的 listeningSocket)。這項工作是在 Server 類中的 createServer 中完成的。
客戶端如何知道怎樣連接到服務(wù)器呢?每一個網(wǎng)絡(luò)終端必須有獨一無二的 ip 和 port,ip 地址是由動態(tài)獲取的或由用戶設(shè)定的,因此我們在這里無需操心 ip 地址問題,因此在代碼中我們使用了 INADDR_ANY。那又如何設(shè)定我們想要監(jiān)聽的 port 呢?一些服務(wù)必須監(jiān)聽約定的 port 才能工作,比如 80,20, 21等端口都是有約定用途的。在這里我們把端口設(shè)定問題交給 OS 來處理,OS 會為我們設(shè)定一個沒有被占用的 port。為了實現(xiàn)這個目的,我們傳入 port 為 0。為了讓其他客戶端能夠連接到服務(wù)器,我們需要告知其他客戶端服務(wù)器實際使用的 port,因此,我們在 createServer 方法 PART 3中獲取實際使用 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 注冊為 application run loop 的消息源,這樣當(dāng)有新連接到來的時候, OS 就會調(diào)用 serverAcceptCallback 這個回調(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)建一個新的 Connection 對象,然后將它與 OS 自動創(chuàng)建的響應(yīng)新連接的 socket 綁定起來。然后再將這個 Connection 對象傳遞給 Server delegate。
通過 Bonjour 發(fā)布服務(wù)
Bonjour 并非在網(wǎng)絡(luò)查找服務(wù)的唯一途徑,但它是最容易使用的方法之一。我們在 publishService 方法中創(chuàng)建一個 NSNetService 對象來發(fā)布服務(wù)。我們根據(jù)服務(wù)類型在網(wǎng)絡(luò)查找感興趣的服務(wù),本聊天服務(wù)使用“_chatty._tcp.”作為服務(wù)類型。在同一網(wǎng)絡(luò)中,服務(wù)類型名必須唯一,這樣才能精準(zhǔn)定位服務(wù),而不至于引發(fā)沖突。
Bonjour 操作也如 socket 一樣需要異步進(jìn)行,以避免長時間阻塞主線程。因此在實際發(fā)布服務(wù)時,我們將發(fā)布任務(wù)交給當(dāng)前 run loop 去調(diào)度,然后設(shè)定其 delegate,由 delegate 來處理相關(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;
}
通過 Bonjour 查詢服務(wù)
我們在 ServerBrowser 類中實現(xiàn) Bonjour 查詢網(wǎng)絡(luò)服務(wù)的功能。我們創(chuàng)建一個 NSNetServiceBrowser 對象來查詢類型為 “_chatty._tcp.” 的服務(wù)。當(dāng)前網(wǎng)絡(luò)中發(fā)現(xiàn)有服務(wù)被添加到或移除時,NSNetServiceBrowser 的 delegate 即我們的 ServerBrowser 就能得到通知,以進(jìn)行相應(yīng)的邏輯處理:更新服務(wù)列表,刷新 UI 等。
通過 Bonjour 決議服務(wù)
當(dāng)用戶選擇其中一個 chat room,并加入其中時,客戶端將會連接到發(fā)布該 chat room 服務(wù)的服務(wù)器。這個連接過程在 ChattyViewController 類的 joinChatRoom: 方法中實現(xiàn)。首選我們通過選擇的 NSNetService 發(fā)送 resolveWithTimeout: 消息來進(jìn)行決議應(yīng)該連接到哪個服務(wù)器(請參考 Connection 類的 connect 方法中最后一種情形),同時設(shè)定 NSNetService 的 delegate 來響應(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é)就不多羅嗦了。
參考資料: