天天看點

[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計

深入淺出Cocoa之Bonjour網絡程式設計

羅朝輝 (http://www.cnblogs.com/kesalin/)

本文遵循“署名-非商業用途-保持一緻”創作公用協定

本文高度參考自 Tutorial: Networking and Bonjour on iPhone,在那個文章裡 iphone 版本的代碼采用的是 MIT 開源協定,是以本例子中的 Mac 版本亦采用 MIT 開源協定。E文較好的童鞋建議閱讀原文。

本文通過使用 Bonjour 實作了一個簡單的伺服器/用戶端聊天程式,示範了 CFSocket,NSNetService/NSNetServiceBrowser, NSInStream/NSOutStream 的用法。

代碼下載下傳:點選這裡

效果圖如下:

[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計
[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計

Cocoa 網絡架構:

Cocoa 網絡架構有三層,最底層的是基于 BSD socket庫,然後是 Cocoa 中基于 C 的 CFNetwork,最上面一層是 Cocoa 中 Bonjour。通常我們無需與 socket 打交道,我們會使用經 Cocoa 封裝的 CFNetwork 和 Bonjour 來完成大多數工作。注:cocoa 很多元件都有兩種實作,一種是基于 C 的以 CF 開頭的類(CF=Core Foundation),這是比較底層的;另一種是基于 Obj-C 的以 NS 開頭的類(NS=Next Step),這種類抽象層次更高,易于使用。對于網絡架構也一樣。Bonjour 中 NSNetService 也有對應的 CFNetService,NSInputStream 有對應的 CFInputStream。

Sockets vs Streams:

Socket 相當于一條通信信道,應用程式通過建立 socket,然後使用這個 socket 連接配接到其他應用程式進行資料交換。我們可以通過同一個 socket 來發送資料或者接收資料。每個 socket 有一個 ip 位址和 port(通信端口,介于 1 ~ 65535之間)。

Stream 是傳送資料的單向通道,正因為是單向的,是以我們有輸入/輸出兩種 streams:instream/outstream。stream 隻是臨時緩存資料,我們需要将它與檔案或記憶體綁定,進而可以從/向檔案或記憶體中讀/寫資料。在這個教程中,我們使用 stream 結合 socket 在網絡上傳送和接收資料。

Bonjour 簡介:

Bonjour(法語中的你好)是一種能夠自動查詢接入網絡中的裝置或應用程式的協定。Bonjour 抽象掉 ip 和 port 的概念,讓我們聚焦于更容易為人類思維了解的 service。通過 Bonjour,一個應用程式 publish 一個網絡服務 service,然後網絡中的其他程式就能自動發現這個 service,進而可以向這個 service 查詢其 ip 和 port,然後通過獲得的 ip 和 port 建立 socket 連結進行通信。通常我們是通過 NSNetService 和 NSNetServiceBrowser 來使用 Bonjour 的,前者用于建立與釋出 service,後者用于監聽查詢網絡上的 service。

同步與異步操作:

大多數網絡操作是阻塞模式的,比如連結的建立,等待接收資料,或發送資料給網絡另一端。是以如果我們不進行異步處理的話,當在進行網絡通信時,我們的 UI 機會被阻塞。有兩種辦法來處理阻塞問題:啟用多個線程或更有效地利用目前線程。在這個例子中,我們使用後一種辦法,我們通過 cocoa 提供的 run loop 來做這個事情,其工作原理是:将網絡消息當作普通的事件丢到目前的 run loop 中,進而我們可以異步處理它們。

Run loops 簡介:

run loop 是 thread 中的消息處理循環,有事件來則處理,無事件則啥也不做。cocoa 中的 run loop 可以處理使用者 UI 消息,網絡連接配接消息,timer 消息等。我們也可以添加其他的消息來源,如 socket 和 stream,進而讓 run loop 也可以處理它們。

程式架構:

理論介紹得差不多了,更多細節,請翻閱官方文檔。下面我們來看看整個程式的架構設計圖:

[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計

從上圖可以清晰地看出,程式分為三個主要子產品:UI子產品,邏輯子產品,網絡子產品。下面我們打開工程,看看代碼實作:

[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計

從工程圖可以看出,代碼結構相當清晰,所有的類被分為四個 group:

Networking: 網絡相關的代碼,包括 socket 的建立,連接配接的建立,service 的 publish 和 browser;

Business Logic:業務邏輯相關代碼。在這個例子中,我們通過 room service 來提供聊天服務。我們通過建立一個 localroom 來建立伺服器,并釋出一個 room service,用戶端(remoteroom)能夠連接配接到一個已有的 room service,進而加入該 room 進行對話活動。

UI :在這個例子中,UI 很簡單,隻有兩個 view,一個顯示目前網絡中的 service 清單,另一個顯示 room,以及在該 room service 上進行的對話。

Misc:一個輔助類,用于存儲使用者設定名稱。

網絡類:

Server class:建立 server,并釋出 service;

Connection class:決議 service;與伺服器建立連接配接;通過 socket stream 交換資料;

ServerBrowser class:查詢可用的 service;

Room類:

Room class: Room 基類

LocalRoom class: 建立伺服器,釋出 service,相應用戶端的連接配接請求

RemoteRoom:  連接配接到伺服器已有的 service,

網絡資料傳輸過程:

[Cocoa]深入淺出Cocoa之Bonjour網絡程式設計

從上圖可以看出,資料從 A 的邏輯層,經 outgoing buffer 寫入 write stream,然後經 socket 通過網絡傳輸到 B 的網絡層,然後 B 端的  read stream 從 socket 中讀取資料,寫入 incoming buffer,然後在 B 的邏輯層以及 UI 上顯示出來。

使用者互動操作都在 UI layer 上進行,當使用者通過 broadcastChatMessage:fromUser: 發送一條聊天資訊,由邏輯層來決定是發送給伺服器(由 Remote room 處理),還是發送給連接配接到伺服器自身的所有用戶端(由 Local room 處理)。當從網絡連接配接接收到一條聊天資訊時,邏輯層會得到通知,用戶端隻會簡單地将消息顯示在 UI 上,而伺服器首先将收到的聊天資訊轉發給所有連接配接到它的用戶端,然後将該資訊在 UI 上顯示出來。

Socket+Streams+Buffers = Connection

Connection 類對一些的互動進行了封裝:

兩個 socket stream,一個用來寫入,一個用來讀取;兩塊 data buffer,每個 socket stream 對應一個 data buffer;以及各種控制 flag 和值

因為 stream 是單向的,是以我們需要為每一個 socket 建立兩個 stream,一個用來從 socket 讀取資料,一個用來向 socket 寫入資料。我們在 connect 和 setupSocketStreams 中初始化它們。

在本例中,我們通過兩種方式來建立 socket:

1,(用戶端)通過建立 socket 連接配接到指定 ip 和 port 的伺服器;

2,(伺服器)通過接收來自用戶端的連接配接請求,在這種情況下,OS 會自動建立一個用于響應的 socket,并通過 native socket handle 傳遞給我們使用;

無論 socket 是由哪種方式建立的,我們都是通過相同的代碼 setupSocketStreams 來初始化 stream。

建立 server

聊天至少需要同時運作兩個 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。

// Handle new connections
- (void) handleNewNativeSocket:(CFSocketNativeHandle)nativeSocketHandle
{
    Connection* connection = [[[Connection alloc] initWithNativeSocketHandle:nativeSocketHandle] autorelease];

// In case of errors, close native socket handle
    if ( connection == nil ) {
        close(nativeSocketHandle);
return;
    }

// finish connecting
    BOOL succeed = [connection connect];
if ( !succeed ) {
        [connection close];
return;
    }

// Pass this on to our delegate
    [delegate handleNewConnection:connection];
}


// This function will be used as a callback while creating our listening socket via 'CFSocketCreate'
static void serverAcceptCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
// We can only process "connection accepted" calls here
    if ( type != kCFSocketAcceptCallBack ) {
return;
    }

// for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
    CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;

    Server *server = (Server *)info;
    [server handleNewNativeSocket:nativeSocketHandle];

    NSLog(@" >> server accepted connection with socket %d", nativeSocketHandle);
}      

通過 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 等。

// New service was found
- (void) netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
            didFindService:(NSNetService *)netService
                moreComing:(BOOL)moreServicesComing
{
// Make sure that we don't have such service already (why would this happen? not sure)
    if ( ! [servers containsObject:netService] ) {
// Add it to our list
        [servers addObject:netService];
    }

// If more entries are coming, no need to update UI just yet
    if ( moreServicesComing ) {
return;
    }

// Sort alphabetically and let our delegate know
    [self sortServers];

    [delegate updateServerList];
}


// Service was removed
- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
         didRemoveService:(NSNetService *)netService
               moreComing:(BOOL)moreServicesComing
{
// Remove from list
    [servers removeObject:netService];

// If more entries are coming, no need to update UI just yet
    if ( moreServicesComing ) {
return;
    }

// Sort alphabetically and let our delegate know
    [self sortServers];

    [delegate updateServerList];
}      

通過 Bonjour 決議服務

當使用者選擇其中一個 chat room,并加入其中時,用戶端将會連接配接到釋出該 chat room 服務的伺服器。這個連接配接過程在 ChattyViewController 類的 joinChatRoom: 方法中實作。首選我們通過選擇的 NSNetService 發送 resolveWithTimeout: 消息來進行決議應該連接配接到哪個伺服器(請參考 Connection 類的 connect 方法中最後一種情形),同時設定 NSNetService 的 delegate 來響應決議相關的事件:didNotResolve: 和 netServiceDidResolveAddress:。當決議完成之後,在 netServiceDidResolveAddress: 方法中,我們可以建立到服務的 socket 連接配接并建立用于資料傳輸的 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 網絡程式設計介紹就結束了,代碼中的注釋相當詳細,細節就不多羅嗦了。

為了示範效果,我們需要運作該程式的兩個執行個體,可以在如下路徑找到可執行檔案:

/Users/username/Library/Developer/Xcode/DerivedData/MacChatty-XXXX/Build/Products/Debug

參考資料:

Tutorial: Networking and Bonjour on iPhone:http://mobileorchard.com/tutorial-networking-and-bonjour-on-iphone/

Introduction to Bonjour Overview:http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/NetServices/Introduction.html

Introduction to NSNetServices and CFNetServices Programming Guide:http://developer.apple.com/library/mac/#documentation/Networking/Conceptual/NSNetServiceProgGuide/Introduction.html#//apple_ref/doc/uid/TP40002736