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