iOS-MultipeerConnectivity框架開發(一)

ZFJ_張福傑發表於2016-03-07

英文原文:Understanding Multipeer Connectivity Framework in iOS 7 – Part 1


Multipeer Connectivity Framework 是iOS 7 推出的眾多新框架的一種,它拓寬了作業系統中應用的範圍。其目的是使開發者可以建立通過Wi-Fi或藍芽在近距離建立連線的應用。是在近距離裝置間建立互動,交換資料和其他資源的很好的簡單工具。 

繼續我們的介紹,在看到例子之前我們有必要討論一下Multipeer Connectivity Framework的細節部分使讓我們對它更熟悉。首先,我們必須強調一下這種框架合作只適用於近距離的裝置,意思是隻能在同一個網路裝置下進行(Wi-Fi或者藍芽),也就是說不要期望在遠距離的環境下工作。在多使用者端連線下,一個裝置可以和很多裝置連線並且和其所有的同時進行互動。一個單一的裝置就是一個埠。當兩個埠連線起來後,就成了一個Multipeer Connectivity的會話, 這個會話負責管理所有埠間的互動和資料交換。在多個裝置下,可以建立多個會話。 

multipeer connectivity featured

在討論建立連線之前,(需要連線的)裝置們首先需要發現對方的存在。在應用中使用 Multipeer Connectivity,“發現”是第一個階段。那麼裝置是如何發現彼此的呢?我們假設,現在有兩個裝置想要連線。那麼至少要有一個裝置作為瀏覽器(browser),用來搜尋其他裝置;第二個裝置必須是可發現的,它要宣告它在這裡,並且它想要與別的裝置連線。換句話說,第二個裝置需要宣傳自己。一般來說,兩個裝置都要宣傳自己,但至少要有一個能瀏覽其他裝置,從而建立連線。

在瀏覽方面,Apple 提供了兩種方法。第一種簡單一些,是一個框架中直接嵌入的瀏覽器UI,被呼叫的時候就彈出一個浮層,上面列出所有可連線的裝置。第二種為開發者提供了更強的靈活性,是一種完全用程式碼實現的方式,因此您可以根據應用需求來定製這個瀏覽器。本文是對這個框架的入門教程,因此接下來我們只用第一種方法。

一旦發現了其他對等實體,查詢廣播實體的對等實體就會傳送請求建立連線的資訊。如果第二個對等實體(也就是廣播實體)接收這條訊息,那麼就會建立一個會話,至此這兩個對等實體就可以交換資料了。 

接下來說說資料,多對等實體連線框架可以用來傳送和接收三種型別的資料。這三種資料是: 

  • 訊息資料(包括文字、影象以及可以轉換為NSData物件的任何其他資料) 

  • 流資料 

  • 資源資料 

接下來更詳細的討論一下訊息資料,傳輸這樣的資料可以使用兩種模式:可靠傳輸模式和不可靠傳輸模式。框架在使用可靠模式傳送資料的時候,它一定會確保傳送的任何資料都會到達接收者,而且是按照傳送的順序到達接收者的。不過,要完成可靠模式的資料傳輸就需要更多的時間。然而,使用不可靠模式傳送資料幾乎不花時間,傳送也真的很快,不過它不能確保傳送的所有資料都能到達另一端,當然,接收資料的順序也不是按照傳送順序的。究竟哪一種模式是最優選擇這一點是完全由每個應用的要求來確定的,因此開發者要決定使用哪種模式。

通常,多實體連線框架為高階語言開發者提供所需的類和庫,因此不需要有任何C或者其他低階語言的程式設計經驗。在這個框架下,你可以使用許多內建的功能,而不會為實現或者解決任何與網路相關的問題而困惑。在這篇程式設計指南里,我們不會耗費許多時間對這個框架做一些理論性的介紹,因此我們強烈建議你訪問Apple文件,進行一些學習,當然,也可以看看蘋果全球開發者大會(WWDC)2013的708號視訊會話。特別要注意的是:要對多實體連線框架進行測試的話,你手頭最少要有兩臺裝置,或者一臺裝置和一個iPhone模擬器。

為了很容易地在實踐中使用多實體連線功能以及搞清楚多實體連線框架是怎樣進行裝置通訊的,請向下閱讀!

示例應用概覽

在這篇程式設計指南里,我們將建立一個應用樣例,用以說明多實體連線框架最重要的部分。前面我已經提到使用這個框架可以交換三種型別的資料:訊息資料、流資料和資源資料。在我們即將實現的示例應用裡,我們將看到如何通過裝置間傳送NSData物件來傳送訊息資料,以及如何共享資源資料的,比如共享檔案。不過,在我們建立示例應用之前,我們需要快速瀏覽一下示例應用究竟做了哪些事情。

我們將建立一個帶有標籤的應用, 總共帶有三個標籤。讓我們從最後一個名稱為我的連線的標籤說起,我們將建立一個檢視控制器,用它來管理對等實體(裝置)名,正在廣播的實體和這臺裝置上已經建立的所有連線(實際上是會話)。尤其要注意的是,我們使用了文字輸入,這樣可以為這臺裝置自定義一個名稱,以便在其他對等實體上顯示這臺裝置。再還有一個按鈕,一旦輕擊這個按鈕就會顯示預設的瀏覽器介面,同時控制開關用來開啟或者關閉這臺裝置的廣播功能。接下來是表檢視,在這裡列出了這臺裝置正在進行的所有連線。表檢視下有一個或者多個按鈕,通過這些按鈕可以斷開這個裝置上的會話。下圖顯示了簡要的檢視控制器:

Multipeer Connectivity Demo App1

接下來介紹的另一個檢視控制器是位於第一個標籤下。這個檢視控制器的名字為聊天視窗,用它來在裝置間通過多實體連線框架傳送文字訊息。這個檢視控制器由一個文字輸入框、一個文字檢視和兩個按鈕組成, 文字輸入框用來書寫訊息,文字檢視用來顯示整個聊天內容,而兩個按鈕中一個用來傳送訊息,一個用來取消訊息傳送 。下面是這樣一個檢視控制器的螢幕截圖:

Multipeer Connectivity Demo App2


最後介紹的這個檢視控制器是位於第二個標籤下的,名字為檔案共享。這兒有一個表檢視,它包含一些(樣例)檔案列表。當選中一行時,就會有顯示這樣的選項:動作選項和所有對等實體的名字。選擇好對等實體後,就會直接把所選的檔案傳送給該對等實體。為了讓這個檢視更吸引使用者並且更具有互動性,當裝置接收一個檔案的時候,在這個表檢視的最後一行顯示檔名、傳送實體以及正在接收檔案的進度。當這個裝置傳送一個檔案的時候 ,這個檔名的右邊將以傳送百分比來顯示傳送的進度。下圖是第二個標籤下的檢視控制器起始狀態的截圖:

Multipeer Connectivity Demo App3

我已經介紹了示例應用的所有檢視控制器,特別注意的是我從第三個標籤開始介紹 ,因為在這篇程式設計指南里,處於第三個標籤下的檢視控制器對我們來說最重要。通過對這個檢視的瞭解,我們就能明白如何處理和管理髮現過程和會話建立過程。 

在我們結束簡要地介紹示例應用之前,還有一些十分重要的事情必須提及一下。為了在該應用範圍內使用多實體連線框架和避免重複編寫同樣的功能三次(每個檢視控制器各一次),我們建立一個用於管理所有與這個框架相關的物件和任務的類。接著我們就可以在應用的委派類(AppDelegate)裡初始化這個類對應的物件。另外,通過應用委派,我們就可以跨應用訪問該物件了。畢竟與僅構建一次並把其當工具多次使用相比,對某個東西開發多次是一個非常糟糕的程式設計實踐。 

在我們瀏覽完這個應用之後,我們就要進行開發了,現在是時候進行這個應用的開發了 。如果你打算逐步構建這個應用,請你下載位於這篇程式設計指南末尾的兩個樣例檔案。另外,為了便利起見,你可以下載整個示例應用,它包含了所有的程式碼。

演示應用建立

啟動 Xcode 通過點選歡迎螢幕左側相應的按鈕建立一個新工程:

Xcode Welcome Dialog

在嚮導的第一步,在 iOS 大類下的 Application 類別中選擇 Tabbed Application 選項。

Xcode New Project Template

點選 Next 按鈕轉入嚮導的下一步。在 Product Name 輸入框中我命名這個應用的名稱為 MCDemo,但你也可以命名為你喜歡的其它名稱。 此外,確保在 Devices 的下拉選單中選擇 iPhone。其它保持原樣,點選 Next 按鈕前進。

Xcode Project Options

最後,為你的工程選擇一個儲存位置,點選 Create 按鈕結束嚮導。現在工程已經準備好了。

新增新Tab

就像前面提到的,這個應用裡總共有三個tab,但預設建立的只有兩個。所以,我們的第一個任務是新增一個新tab,然後把這個新tab連線上一個新類。除此之外,我們還要為所有tab設定標題和icon。

首先在專案裡新增一個新的 UIViewController 類,用於連線我們一會兒將要新增的新tab。在專案導航(左邊欄)中, 按著Ctrl點選或者右擊 MCDemo 這個 group ,然後從彈出選單中選擇 New File…選項,如下圖所示:

Multipeer Connectivity - Add New File

在左邊的 iOS 一項下,要確定選的是 Cocoa Touch 分類。然後,選擇 Objective-C class 圖示,點選 Next 按鈕繼續。

Multipeer Connectivity - Add New File

在 Class 輸入框,填寫 ConnectionsViewController 。並且,要確保 Subclass of 這一欄選擇的是 UIViewController 。不要勾選下面兩個選項。點選 Next 進入下一步。

Multipeer Connectivity - Add New File

最後一步,點選 Create 按鈕,就大功告成啦。現在,在左邊欄的專案導航中應該可以看到 ConnectionsViewController.h 和 ConnectionsViewController.m 兩個檔案了。

Multipeer Connectivity - Add New File

現在點選 Main.storyboard ,讓它開啟 Interface Builder 。首先,新增一個新的 view controller ,,方法是把它從 Object Library 中拖出來放在 canvas 上,就放在 second view controller 的下面。

Multipeer Connectivity - Designing Interface

然後,一直按著鍵盤上的 Ctrl 鍵,點選 Tab Bar Controller ,然後一直按著滑鼠左鍵,拖到我們剛新增的新 view controller 上。現在放開 Ctrl 鍵和滑鼠左鍵了。會彈出一個黑色的視窗,其中的 Relationship Segue 部分你必須點選 view controllers 選項,這樣這個新的 view controller 就會被加為 tab bar controller 的一個新 tab。

Multipeer Connectivity - Designing Interface

tab bar controller 目前包含了3個 tab,並且連線上了我們新建立的 view。我們現在需要將新 view controller 的類設定為之前新增的 ConnectionsViewController 。為此,首先點選新 view controller,然後在 Utilities Pane (右邊欄)中,開啟 Identity Inspector (第3欄),之後在 Custom Class 部分將 ConnectionsViewController 填進 Class 對應的輸入框。

Multipeer Connectivity - Set Class Name

最後,我們只需為 tab 們設定正確的標題。要設定一個 tab 的標題,需要首先在場景中(而不是 tab bar controller上)選中需要修改的 tab。然後,再回到 Utilities pane(右邊欄),開啟 Attributes Inspector(第4欄),然後在 Bar Title 部分將標題填入 Title 對應的輸入框。

Multipeer Connectivity - Set Title

設定標題如下:

  • First View Controller: Chat Box

  • Second View Controller: File Sharing

  • Connections View Controller: My Connections

注意,教程中所有在 Interface Builder 進行的視覺化配置都是針對 4 寸螢幕裝置的,如 iPhone 5 或 5S。如果你想要讓這個應用在較老的3.5寸裝置上執行,只需變通一下,應用 Xcode 的推薦約束,然後就可以輕鬆執行了。

連線檢視控制器:設定介面 

現在,第三個標籤連同其自己的檢視控制器已經被新增了,是時候從最後一個 tab 的檢視控制器,連線檢視控制器,開始建立我們的應用程式了。我們的第一個任務是設定它的介面,以及宣告和連線所需的任何 IBOutlet 屬性和 IBAction 方法。所以,在介面生成器中,從物件庫中拖放下面提到的控制元件到連線檢視控制器場景中。

請注意,提供的每個控制元件的所有這些屬性你都應該修改: 

  1. UITextField

    • Frame: X=20, Y=20, Width=280, Height=30 

    • Placeholder: The device name displayed to others…

  2. UILabel

    • Frame: X=20, Y=63, Width=180, Y=21 

    • Text: Visible to others?

  3. UISwitch

    • Frame: X=251, Y=58, Width=51, Y=31 

    • State: ON 

  4. UIButton

    • Frame: X=94, Y=92, Width=132, Y=30 

    • Title: Browse for devices

  5. UITableView

    • Frame: X=0, Y=130, Width=320, Y=352 

  6. UIButton

    • Frame: X=121, Y=490, Width=78, Y=30 

    • Text: Disconnect

    • Enabled: False (Unchecked)

新增完所有上面這些控制元件之後,你的場景現在看來應該會象這樣: 

Multipeer Connectivity - UI

讓我們現在宣告一些 IBOutlet 屬性和一些 IBAction 方法。開啟ConnectionsViewController.h 檔案,在 interface body 內新增下面的屬性: 

@property (weak, nonatomic) IBOutlet UITextField *txtName;
@property (weak, nonatomic) IBOutlet UISwitch *swVisible;
@property (weak, nonatomic) IBOutlet UITableView *tblConnectedDevices;
@property (weak, nonatomic) IBOutlet UIButton *btnDisconnect;

另外,新增這些 IBAction 方法: 

- (IBAction)browseForDevices:(id)sender;
- (IBAction)toggleVisibility:(id)sender;
- (IBAction)disconnect:(id)sender;

現在回到Main.storyboard把控制元件與它連線起來。做之前,確保左側的Document Outline皮膚開啟。下面,按下Ctrl鍵或在Connections View Controller – Connections物件上右擊,就會彈出一個黑色視窗。在每一個屬性(和IBAction方法)旁邊都有一個圓。點選IBOutlet屬性旁邊的圓,不要釋放滑鼠按鈕,拖放到對應控制元件的螢幕上。下圖是詳細操作:

Multipeer Connectivity - Setup IBOutlet

連線應按以下規則建立:

  • txtName屬性放到 UITextField物件.

  • swVisible 屬性放到 UISwitch 物件.

  • blConnectedDevices 屬性放到 UITableView 物件.

  • btnDisconnect 屬性放到 UIButton (Disconnect) 物件.

其餘螢幕中未出現的屬性,就不需要使用 IBOutlet 屬性.

用上述相同的方法,把 IBAction方法連線到相應的控制元件. 下面告訴你如何搭配各種方法和控制元件s:

  • browseForDevices: 連線到第一個 UIButton (Browse for devices) 物件.

  • toggleVisibility: 連線到 UISwitch 物件.

  • disconnect: 連線到第二個 UIButton 物件.

 Connections View Controller的介面已經就緒。我們接著討論我們上面新增的每一個控制元件的功能和目的。 現在到了我們第一次需要關注Multipeer Connectivity框架細節的時候了。

一個基於Multipeer Connectivity框架的類

現在,我們要做點大轉變:先不管Interface Builder和其他任何的視覺化設定,我們將要全部注意力集中在程式碼上面。在這一章節,我們的目標是建立一個新類,在這個類中,我們需要實現框架相關的全部邏輯,為此我們將會使用任何必須的框架類和執行任何必須的任務。說明一下,通過框架來做到這些並不是必須的,然而,這確實是最簡單的方式來使這個簡單應用能夠執行完全部應用端的任務而不需要多次重新編碼。

在開始做上述事情之前,首先要建立一個新類。所以,在專案導航視窗右鍵點選MCDemo組,在彈出選單中選擇New File...選項,或使用快捷鍵Command-N(Ctrl+N)。

在嚮導視窗中,選擇Object-C class選項來作為新檔案的模板。點選Next繼續。接下來,在Subclass of框中,設定為NSObject,在正上方的Class框中,必須填入新類的名字。這裡我填寫的是MCManager,當然,你和我命名一致會是個不錯的選擇,僅僅為了能保證正確的學習本教程。

Multipeer Connectivity - Create new class mcmanager

最後,點選Next,然後點選Creat按鈕為專案中新增新類。新增成功之後,專案導航中應該會列出MCManager.hMCManager.m這兩個檔案

讓我們現在寫點程式碼。開啟 MCManager.h 宣告一些所需物件。但在做那些之前,我們必須引入 Multipeer Connectivity 庫到我們的專案,因此,開頭在檔案的頂部新增下面一行:

#import <MultipeerConnectivity/MultipeerConnectivity.h>

如你所見,我沒有手動新增框架到專案中,而是直接使用它。編譯器會為我們做這件事,這要感謝它採用的一個新功能,叫作自動連結(Auto Linking)

現在,修改介面的header行來採用 MCSesssionDelegate 協議,如下:

@interface MCManager : NSObject <MCSessionDelegate>

然後,宣告下面這些物件:

@property (nonatomic, strong) MCPeerID *peerID;
@property (nonatomic, strong) MCSession *session;
@property (nonatomic, strong) MCBrowserViewController *browser;
@property (nonatomic, strong) MCAdvertiserAssistant *advertiser;

PeerID 物件表示裝置,它包含發現裝置和建立會話階段所需的各種屬性。Session物件是最重要的,因為它代表目前的對等點(這個程式將執行的裝置)將建立的會話。任何資料交換和通訊細節都由該物件控制。瀏覽器物件實際上是代表蘋果提供的用於瀏覽其他對等點的預設UI,我們將為此目的而使用它。對於框架的瀏覽功能更先進的處理,蘋果提供了一個可程式設計的替代方式,但它超出了現在本文的範圍。最後,有一個廣告物件,這是用來從目前的裝置去宣傳自己,使其容易被發現。

注意所有這些物件的類都屬於 Multipeer Connectivity framework。你會看到我們將如何使用它們,但是現在讓我們宣告一些接下來會用到的公有方法:

-(void)setupPeerAndSessionWithDisplayName:(NSString *)displayName;
-(void)setupMCBrowser;
-(void)advertiseSelf:(BOOL)shouldAdvertise;

現在來到 MCManager.m 檔案,加入如下 init 方法來書實話我們的類:

-(id)init{
    self = [super init];
    
    if (self) {
        _peerID = nil;
        _session = nil;
        _browser = nil;
        _advertiser = nil;
    }
    
    return self;
}

這時 Xcode 通常會報一些警告了。這是因為我們還沒有實現這些公有方法,還有 MCSessionDelegate 協議中的代理方法。為了讓這些警告消失,我們首先增加如下必要的代理方法:

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
    
}


-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    
}


-(void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{
    
}


-(void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error{
    
}


-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID{
    
}

儘管這些方法的名字顯而易見的說明了它們是幹嘛的,我們還是快速提及一下。第一個方法在節點改變狀態的時候被呼叫,已連線或已斷開。有三個狀態: MCSessionStateConnected , MCSessionStateConnecting  and MCSessionStateNotConnected。最後一個狀態在節點從連線斷開後依然有效。第二個方法在有新資料從節點過來時被呼叫。記住有三種資料可以交換:訊息,流和資源。這個是訊息的代理。下面兩個方法在收到資源時被呼叫,最後一個用來接收流。在這個教程裡除了最後一個我們全都會用到。

現在讓我們去實現上面的公有方法。然後我們接下來開始下一個程式碼片段。

-(void)setupPeerAndSessionWithDisplayName:(NSString *)displayName{
     _peerID = [[MCPeerID alloc] initWithDisplayName:displayName];
     
     _session = [[MCSession alloc] initWithPeer:_peerID];
     _session.delegate = self;
 }

首先,peerID物件初始化,因為一切都基於它。displayName字串值提供給init方法(作為該方法的引數)一個裝置名字,這個名字會出現在其他對等實體上。這可以是裝置的名稱(如“約翰的iPhone”),或一個自定義名稱。如果你還記得,我們 為此在介面新增一個文字域,以後,我們將看到更多關於它的東西。

然後,我們初始化session物件,這是最重要的,因為一切都基於它。我們為其初始化提供peerID,然後我們在最後一行把自己賦給delegate。

接下來:

-(void)setupMCBrowser{
     _browser = [[MCBrowserViewController alloc] initWithServiceType:@"chat-files"session:_session];
 }

該方法僅有一行重要的程式碼。這是預設的初始化,由蘋果預製檢視控制器,顯示一個瀏覽器搜尋其他對等實體。在初始化上它接受兩個引數:serviceType定義瀏覽器應該尋找的型別的服務,通過一個小的文字描述。為了瀏覽器能夠發現廣播者,這個小文字應該是相同的。  關於它的名字有兩個規則:

  1. 必須是1 - 15字元。

  2. 只能包含ASCII小寫字母,數字和連字元。

第二個引數是先前初始化的session物件

一個公共方法:

-(void)advertiseSelf:(BOOL)shouldAdvertise{
     if (shouldAdvertise) {
         _advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:@"chat-files"
                                                            discoveryInfo:nil
                                                                  session:_session];
         [_advertiser start];
     }
     else{
         [_advertiser stop];
         _advertiser = nil;
     }
 }

我們將使用這個公共方法切換裝置的廣播狀態。你注意到,引數定義裝置是否應該廣播自己,取決於我們的應用程式的設定。記住,我們新增了一個UISwitch物件設定我們是否對其他裝置可見。

當我們希望開啟廣播時,我們初始化它,我們開始它。注意serviceType文字要與瀏覽器的相匹配。當我們想要關掉廣播,我們簡單地停止它,我們使我們的物件置為nil。

那麼,現在本類已經準備被用作一個工具。我們將用它很多次,我們會將程式碼新增到session的delegate裡面。對你來說也許不是所有的東西都是有意義的,但是別擔心,這些都會在用的時候清除。

最後一件事留給我們了,去在AppDelegate.h檔案,宣告MCManager物件:

@property (nonatomic, strong) MCManager *mcManager;

不要忘記引入這個類:

#import "MCManager.h"

然後,開啟 AppDelegate.m檔案,在 application:didFinishLaunchingWithOptions:  delegate方法裡面加入下面的一行初始化程式碼:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
     // Override point for customization after application launch.
     
     _mcManager = [[MCManager alloc] init];
     
     return YES;
 }

我們通過應用程式的代理來使用 MCManager類。讓我們繼續 Mutlipeer Connectivity 吧! 

發現階段

通過我們之前所建立的ConnectionsViewController這個類,瀏覽器檢視控制器將會被顯示出來,因此我們需要使它遵照MCBrowserViewControllerDelegate這個協議,來使它能夠操作瀏覽器。開啟ConnectionsViewController.h檔案,並像你以前在McManager類中那樣引入Multipeer Connectivity架構:

1 #import <MultipeerConnectivity/MultipeerConnectivity.h>

接下來,修改像下面這樣修改類頭部:

1 @interface ConnectionsViewController : UIViewController <MCBrowserViewControllerDelegate>

我已經提到過很多次,我們會通過應用程式的代理來使用我們自定義的類(MCManager這個類)。這意味著我們需要訪問它,所以開啟ConnectionsViewController.m檔案,轉到類的私有區域。在那裡定義下一個物件:

1 @property (nonatomic, strong) AppDelegate *appDelegate;

不用提示XCode提示錯誤。你只需要像下面這樣匯入:

1 #import "AppDelegate.h"

現在我們可以訪問應用程式代理的mcManager物件,但是如何操作呢?直接跳轉到viewDidLoad方法。在那裡我們可以新增以下三行:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    [[_appDelegate mcManager] setupPeerAndSessionWithDisplayName:[UIDevice currentDevice].name];
    [[_appDelegate mcManager] advertiseSelf:_swVisible.isOn];
}

第一行我們使用sharedApplication類方法初始化appDelegate物件。這樣操作之後,我們能夠呼叫mcManager物件任何需要的公共方法,這正是我們接下來要做的。當我們在初始化我們自定義的類時,呼叫setupPeerAndSessionWithDisplayName:這個方法,我們為裝置的顯示名字指定其真實的名稱。在執行的過程當中,如果沒有自定義的名字被指定到輸入框中,這個名字將會出現到其它端點上。最終,我們呼叫advertiseSelf:方法,傳入當中開關的狀態來使能或者置空advertiser這個物件。

現在讓我們新增一些必要的程式碼到browseForDevices:這個IBAction方法中來實現這個瀏覽器檢視控制器的外觀。在你的ConnectionsViewController.m這個檔案中新增下面這個程式碼段:

- (IBAction)browseForDevices:(id)sender {
    [[_appDelegate mcManager] setupMCBrowser];
    [[[_appDelegate mcManager] browser] setDelegate:self];
    [self presentViewController:[[_appDelegate mcManager] browser] animated:YES completion:nil];
}

第一行,我們呼叫MCManager這個類的公共方法setupMCBrowser。接下來,我們設定self物件(就是這個類的物件本身)作為它的代理,最後以模態的方式將它呈現出來。在實現這個方法之後,帶著"Browse for devices"標題的按鈕就可以完美的工作了,如果你編譯並執行這個應用,然後點選這個按鈕,這就是你希望看到的:

Multipeer Connectivity - Browser Running

太棒了!我們剛從Multipeer Connectivity框架中嚐到了一點甜頭。然而,如果你嘗試使用這個檢視控制器的Done或者Cancel按鈕,你會發現它們沒有正常工作。為了啟用它們,我們必須實現MCBrowserViewControllerDelegate協議的兩個代理方法。所以,回到XCode並新增這兩個方法:

-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{
    [_appDelegate.mcManager.browser dismissViewControllerAnimated:YES completion:nil];
}

-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
    [_appDelegate.mcManager.browser dismissViewControllerAnimated:YES completion:nil];
}

當點選這個檢視控制器的Done按鈕時,我們簡單的讓它消失,當點選Cancel按鈕裡也希望這個瀏覽器有同樣的動作。因此,所有這些方法包含同樣的程式碼,都讓這個瀏覽器檢視控制器消失。第一個方法是Done按鈕的代理,第二個方法是Cancel按鈕的代理。

在深入研究之前,只需要記住如何使用下面的程式碼來訪問browser這個物件:

1 _appDelegate.mcManager.browser

現在,檢視控制器的兩個按鈕都正常工作了。然後,你還不能測試Done按鈕,因為Done按鈕被禁用了,除非其它裝置發現並連線到當前這個端點。不過在我們理解它之前,讓我們再實現兩個功能:如何為裝置自定義名字;如果使用/禁用廣告。

對等點顯示名稱和廣告狀態

讓我們從這個文字域開始。轉到 ConnectionsViewController.h 檔案,採用 UITextFieldDelegate 協議,如下所示:

@interface ConnectionsViewController : UIViewController <MCBrowserViewControllerDelegate, UITextFieldDelegate>

現在,在 ConnectionsViewController.m 檔案的viewDidLoad 方法中,加入下面內容:

- (void)viewDidLoad
{
    ...
   
    [_txtName setDelegate:self];
}

我們將實現文字欄位的 textFieldShouldReturn: 委託方法,因為我們希望返回按鈕被按下後鍵盤消失,並且 MCManager 類的 PeerID 物件得到我們設定到文字欄位的名稱。然而,在 viewDidLoad 方法中我們已經初始化了 PeerID 物件和會話物件,所以我們首先需要設定它們為 nil,然後通過呼叫 setupPeerAndSessionWithDisplayName: 方法使用指定名稱重新初始化它們。實現如下:

-(BOOL)textFieldShouldReturn:(UITextField *)textField{
    [_txtName resignFirstResponder];
   
    _appDelegate.mcManager.peerID = nil;
    _appDelegate.mcManager.session = nil;
    _appDelegate.mcManager.browser = nil;
   
    if ([_swVisible isOn]) {
        [_appDelegate.mcManager.advertiser stop];
    }
    _appDelegate.mcManager.advertiser = nil;
   
   
    [_appDelegate.mcManager setupPeerAndSessionWithDisplayName:_txtName.text];
    [_appDelegate.mcManager setupMCBrowser];
    [_appDelegate.mcManager advertiseSelf:_swVisible.isOn];
   
    return YES;
}

請注意,我們檢查廣告是否開啟,如果是這樣的話,我們首先停止它,然後設定相應的物件為 nil。

在這裡我要指出一個重要的事實:一旦我們的裝置連線到另一個點,我們就不應該再改變其顯示名稱,因為一個會話已經被建立,任何資料交換都可能已經在前進。因此,為了防止我們在裝置連線上之後改變名字,我們將文字域禁用。只有當沒有連線到其他點時,文字欄位才被啟用,我們稍後會把這個功能與“斷開連線”按鈕關聯起來。更具體地說,一旦連線被建立,文字欄位將被禁用,當“斷開連線”按鈕被用來終止一個連線時,文字域將再次成為可用。所以,對現在來說,只把它記在心裡,讓我們繼續前進。

接下來讓我們使開關控制元件能啟用和禁用廣告。執行以下IBAction方法:

- (IBAction)toggleVisibility:(id)sender {
    [_appDelegate.mcManager advertiseSelf:_swVisible.isOn];
}

很簡單的實現,對嗎?我們只要呼叫advertiseSelf方法就能根據開關的狀態設定廣告的狀態。

現在如果你測試這個應用程式(用兩個裝置,或一個裝置和一個模擬器),你可以玩玩對等點發現和我們這裡所加的另外兩個功能。這裡是一些截圖:

當發現附近的裝置時:

Search for Nearby Devices

開關第二個裝置的廣播功能:

Multipeer Advertising

更改設定的顯示名稱:

Multipeer - Changing Device Name

請注意,裝置的顯示名稱和廣播狀態都是可以改變的,因為我們還沒有建立任何連線。在接下來的部分,我們要做的就是建立連線。我們也將看到我們如何得到關於連線的節點的通知,如何在表格檢視中顯示其他節點的名稱,如何使 Disconnect 按鈕工作。

連線

連線真的很簡單。在瀏覽檢視控制器中,你必須點選一個附近裝置的名稱,等到它被連通。檢視控制器顯示著連線的狀態,從“未連線”到“連線中”,最後到“已連線”。一旦已連線,Done 按鈕成為可用狀態。

Multipeer - Making Connection

在其他裝置上會出現類似於下面的報警檢視,提示使用者接受或拒絕連線:

Multipeer - Accept Connection

一旦使用者點選 Accept 按鈕,連線就會被建立。重要的是現在我們如何處理它,首先,我們如何能一直獲知其他對等連線的狀態。值得慶幸的是,多點連線框架給了我們一些 MCSession delegate 方法,這些我們以前在 MCManager 類中實現了,我們不用再為此寫任何程式碼。

現在開啟 MCManager.m 檔案,定位到session:peer:didChangeState: 方法。當新的連線發生時,它會被呼叫,我們的工作就是用它處理所提供的資訊。在我們的例子中,我們要做的很簡單:因為這是一個和我們用來管理我們連線的類(ConnectionsViewController 類)不同的一個類,所以我們將傳送一個通知讓後者知道peer的狀態變化,並且我們將所提供的所有資訊跟通知一起傳送過去。讓我們看看實現程式碼:

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
    NSDictionary *dict = @{@"peerID": peerID,
           @"state" : [NSNumber numberWithInt:state]
           };
   
    [[NSNotificationCenter defaultCenter] postNotificationName:@"MCDidChangeStateNotification"
            object:nil
          userInfo:dict];
}

首先,我們建立一個 NSDictionary 物件,設定 PeerID 和 state 引數值作為它的內容。接下來,我們用 MCDidChangeStateNotification 名稱和字典物件作為使用者資訊字典傳送通知。通過完成所有這些,我們確保每一次一個peer的狀態被改變時,我們的類就能知道。

然而,隨著時間的推移,只完成了一半的工作。我們需要使 ConnectionsViewController 類觀察到這個通知並在收到時能採取適當的行動。所以,開啟 ConnectionsViewController.m 檔案,前往 viewDidLoad 方法。給它新增下面內容:

- (void)viewDidLoad
{
    ...
   
    [[NSNotificationCenter defaultCenter] addObserver:self
         selector:@selector(peerDidChangeStateWithNotification:)
             name:@"MCDidChangeStateNotification"
           object:nil];
}

通過這個命令,我們讓我們的類觀察到特定的通知。當這樣的通知到達時,一個方法將被呼叫,需要採取的任何行為應寫在那個方法中。在我們的例子中,這個方法是 peerDidChangeStateWithNotification:,它是一個自定義的私有方法,我們現在要宣告它。轉到介面的私有部分,加入下面的宣告:

@interface ConnectionsViewController ()
...

-(void)peerDidChangeStateWithNotification:(NSNotification *)notification;

@end

接下來,我們必須實現它,但讓我們對它討論一下。當一個新的連線建立時我們想做什麼?簡單地新增連線點的顯示名稱到表格檢視中,使“斷開連線”按鈕可用,禁用文字欄位(我之前解釋過為什麼)。另一方面,當一個對等點斷開時,我們只要從表格檢視中清除它的名字,如果沒有其他對等點存在,禁用“斷開連線”按鈕,使文字欄位可用。

為了實現這些,我們需要給列表提供一個陣列作為資料來源。所以,我們宣告並初始化這個陣列,然後就可以實現這個私有方法了。

再次來到私有宣告部分並新增以下宣告:

@interface ConnectionsViewController ()
...

@property (nonatomic, strong) NSMutableArray *arrConnectedDevices;

@end

然後,在  viewDidLoad 方法裡初始化它:

- (void)viewDidLoad
{
    ...
    
    _arrConnectedDevices = [[NSMutableArray alloc] init];
}

此外,為了列表能相應我們所要的,我們必須把類設為它的代理和資料來源物件。所以在 viewDidLoad  方法種新增一下兩行:

- (void)viewDidLoad
{
    ...
    
    [_tblConnectedDevices setDelegate:self];
    [_tblConnectedDevices setDataSource:self];
}

Xcode 會對這兩個命令提示一些新的警告。這是因為我們還沒有采用兩個必需的代理方法, UITableViewDelegate 和  UITableViewDatasource。要解決這個問題,我們要在  ConnectionsViewController.h 類種修改宣告頭這行:

@interface ConnectionsViewController : UIViewController <MCBrowserViewControllerDelegate, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource>

現在我們已經完全準備好去這個實現收到通知時呼叫的私有方法了。再次開啟  ConnectionsViewController.m 輸入以下程式碼:

-(void)peerDidChangeStateWithNotification:(NSNotification *)notification{
    MCPeerID *peerID = [[notification userInfo] objectForKey:@"peerID"];
    NSString *peerDisplayName = peerID.displayName;
    MCSessionState state = [[[notification userInfo] objectForKey:@"state"] intValue];
}

我們在這裡所做的不難理解。我們從  userInfo 取出兩個物件( peerID 和  state),賦值給  NSString 物件。我們繼續:

-(void)peerDidChangeStateWithNotification:(NSNotification *)notification{
    ...
    
    if (state != MCSessionStateConnecting) {
        if (state == MCSessionStateConnected) {
            [_arrConnectedDevices addObject:peerDisplayName];
        }
        else if (state == MCSessionStateNotConnected){
            if ([_arrConnectedDevices count] > 0) {
                int indexOfPeer = [_arrConnectedDevices indexOfObject:peerDisplayName];
                [_arrConnectedDevices removeObjectAtIndex:indexOfPeer];
            }
        }
     }
}

首先,只有當 當前狀態 不是 MCSessionStateConnecting 時我們才去執行某些行為。因此,如果 當前狀態 是 MCSessionStateConnected 時,我們把 peer 顯示名稱加入 arrConnectedDevices 陣列中。 否則,如果狀態是 MCSessionStateNotConnected ,我們就在陣列找到當前 peer 的索引,簡單地刪除掉。這個程式碼是我們的方法的心臟。然而,我們還沒有完成。新增缺少的這部分:

-(void)peerDidChangeStateWithNotification:(NSNotification *)notification{
    ...
   
    if (state != MCSessionStateConnecting) {
        ...

        [_tblConnectedDevices reloadData];
       
        BOOL peersExist = ([[_appDelegate.mcManager.session connectedPeers] count] == 0);
        [_btnDisconnect setEnabled:!peersExist];
        [_txtName setEnabled:peersExist];
    }
}

首先,我們過載表格檢視的資料。之後,我們檢查是否有任何peer留下連線,我們指定這個比較的結果為布林值。請注意,會話物件的connectedPeers方法返回一個與會話連線的所有peer的陣列。無論如何,我們設定文字欄位和“斷開連線”按鈕的可用狀態,都要取決於那個布林值。

方法已經準備好了, 但是如果我們執行程式會發現什麼結果也沒有。 為什麼呢? 因為我們仍然沒有實現最基本的 table view 委託和 datasource 方法。讓我們現在開始做。下面,你將一次得到所有這些方法:

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
     return 1;
 }
 
 
 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
     return [_arrConnectedDevices count];
 }
 
 
 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellIdentifier"];
     
     if (cell == nil) {
         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellIdentifier"];
     }
     
     cell.textLabel.text = [_arrConnectedDevices objectAtIndex:indexPath.row];
     
     return cell;
 }
 
 
 -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
     return 60.0;
 }

這裡沒有什麼難以理解的地方. 我們只需要顯示table view的內容.如果你現在測試並連線這個app, 你可能看到類似如下的內容:

Multipeer - Peer Connected

最後, 做最後一件事就會使我們的view controller 功能齊全。 那就是使 disconnect 按鈕生效。只需要實現這個 disconnect:方法:

- (IBAction)disconnect:(id)sender {
     [_appDelegate.mcManager.session disconnect];
     
     _txtName.enabled = YES;
     
     [_arrConnectedDevices removeAllObjects];
     [_tblConnectedDevices reloadData];
 }

如你所見, session 物件有一個能夠斷開連線的 disconnect 方法。 剩下的就比較好理解了。

再次測試這個程式.當你獲得一個連線的時候, 按 Disconnect 按鈕,然後你會看到,在這兩種裝置上的其他同行的顯示名稱會消失,而文字欄位再次被啟用。

在這一點上,連線檢視控制器是準備好了!到現在為止,我們已經看到了有關框架的很多東西,讓我們繼續發現更為有趣的東西!

設定聊天介面

一旦連線建立,我們就能交換所需的資料。在這部份中我們要建立第一個檢視的介面,然後實現聊天功能。開始吧!

點選 Main.storyboard 檔案開啟 Interface Builder。刪除 First View Controller 中的預設內容。Xcode Storyboard Delete Default Content

然後,從 Objects Library 中拖入以下控制元件並設定它們的屬性:

  1. UITextField

    • Frame: X=20, Y=20, Width=280, Height=30

    • Placeholder: Your message…

  2. UIButton

    • Frame: X=254, Y=58, Width=46, Height=30

    • Title: Send

  3. UIButton

    • Frame: X=25, Y=58, Width=48, Height=30

    • Title: Cancel

  4. UITextView

    • Frame: X=0, Y=96, Width=320, Height=422

    • Text: None

    • Background Color: Light Gray

現在這個 scene 看起來會是這樣:

Multipeer Demo - First VC Scene

現在我們要宣告一系列 IBOutlet 屬性。開啟 FirstViewController.h 檔案並新增這兩行:

@interface FirstViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *txtMessage;
@property (weak, nonatomic) IBOutlet UITextView *tvChat;
@end

同樣的,每個按鈕需要一個 IBAction 方法,所以新增這幾行:

@interface FirstViewController : UIViewController
...

- (IBAction)sendMessage:(id)sender;
- (IBAction)cancelMessage:(id)sender;

@end

回到 Interface Builder 之前,我們要讓這個類採用  UITextField 協議,我們需要在後面實現它的代理方法。

@interface FirstViewController : UIViewController <UITextFieldDelegate>

跟之前在 Connections View Controller 中連線 IBOutlet 和 IBAction 屬性的操作一樣,連線 txtMessage 屬性到 text field, tvChat 屬性到 text view。然後連線 sendMessage: IBAction 方法到 Send 按鈕, cancelMessage: 到 Cancel 按鈕。完成這些操作後就可以了。

開始聊天

章節名宣告了我們的目標。我們要做一些必要的操作,讓通過 Multipeer Connectivity framework 連線的裝置能用文字來聊天。

我們從給三個檢視控制器做一些相同的操作開始:宣告並例項化一個 AppDelegate 物件。在 FirstViewController.m 中引入 AppDelegate.h 檔案:

#import "AppDelegate.h"

然後,在私有介面部分做如下宣告:

@interface FirstViewController ()
@property (nonatomic, strong) AppDelegate *appDelegate;
@end

最後,在 viewDidLoad 方法中例項化物件:

- (void)viewDidLoad
{
    [super viewDidLoad];

   _appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
}

現在我們就能訪問在 application 代理中的 mcManager 物件了。接著實現 textFieldShouldReturn: 代理方法。之所以需要這個方法是因為我們要讓返回按鈕的行為與傳送按鈕一樣,就是傳送訊息給其他節點。不過在這之前,我們還要在 viewDidLoad 方法中讓這個類成為 text field 的代理:

- (void)viewDidLoad
{
    ...
    
    _txtMessage.delegate = self;
}

現在我們可以實現 textFieldShouldReturn: 方法了:

-(BOOL)textFieldShouldReturn:(UITextField *)textField{
    [self sendMyMessage];
    return YES;
}

我們在這裡呼叫的  sendMyMessage 方法是一個自定義的私有方法,我們接下來要實現並讓它做傳送的工作。現在我們弦實現之前宣告的兩個 IBAction 方法:

- (IBAction)sendMessage:(id)sender {
    [self sendMyMessage];
}

- (IBAction)cancelMessage:(id)sender {
    [_txtMessage resignFirstResponder];
}

他們已經不能再簡單了。第一個呼叫了 sendMyMessage 方法,第二個讓鍵盤消失,不傳送任何訊息。

現在我們已經進入到所需功能的核心。我們將實現這個私有方法,將會接觸到框架中允許我們傳送資訊的新方法。

轉到介面的私有部分去宣告方法:

@interface FirstViewController ()
...

-(void)sendMyMessage;

@end

現在我們可以繼續實現它,首先讓我們看看它的實現程式碼,稍後再討論它:

-(void)sendMyMessage{
    NSData *dataToSend = [_txtMessage.text dataUsingEncoding:NSUTF8StringEncoding];
    NSArray *allPeers = _appDelegate.mcManager.session.connectedPeers;
    NSError *error;
   
    [_appDelegate.mcManager.session sendData:dataToSend
                                     toPeers:allPeers
                                    withMode:MCSessionSendDataReliable
                                       error:&error];
   
    if (error) {
        NSLog(@"%@", [error localizedDescription]);
    }
   
    [_tvChat setText:[_tvChat.text stringByAppendingString:[NSString stringWithFormat:@"I wrote:\n%@\n\n", _txtMessage.text]]];
    [_txtMessage setText:@""];
    [_txtMessage resignFirstResponder];
}

會話物件具有 sendData:toPeers:withMode:error: 方法,它用來實際傳送訊息。資料應該是一個NSData物件,這正是在第一行用dataUsingEncoding: 方法產生的。

該方法的第二個引數必須是一個含有應接收該訊息的所有點的NSArray。對本教程來說,我們將訊息傳送到所有其它連線的節點。在實際的應用程式中,可以讓使用者選擇訊息的接收人。

第三個引數非常重要,因為這個引數是我們定義我們資料傳送的模式的地方。在這本書的開始部分,我曾經說過兩種模式reliable 和unreliable。這裡我們假設是一個聊天程式,因此我們不希望丟失任何資料包。所以,當呼叫這個方法時我們選擇 MCSessionSendDataReliable 這個值作為引數。

最後一個引數是用於檢查當我們呼叫這個函式後是否有錯誤發生的的經典的錯誤物件。事實上,如果有錯誤發生時,我們只是記錄它的描述,因為沒有理由在我們的演示程式中以任何其他方式去處理它。

最後,我們還需要執行三個步驟,首先,我們顯示訊息的文字檢視,並明確指出,這是我們誰寫的,我們在前面加上I wrote: 文字。接下來,我們從當前內容清除這個文字欄位。最後,我們移除虛擬鍵盤。

與我們剛才遇到的會話物件比起來,這沒什麼困難或怪異的地方。現在讓我們把重點放在當收到節點的訊息時會發生什麼上來,session:didReceiveData:fromPeer: 方法被呼叫了,我們要根據需求去處理收到的訊息。我們在 MCManager 類中實現了這個方法,但是裡面沒有任何內容。所以現在正是時候探究一下它並處理接收到的資料。

在 MCManager.m 檔案種找到 session:didReceiveData:fromPeer: 方法。在這裡我們的做法與 session:peer:didChangeState: 方法相同,這就意味著我們會推送一個通知讓我們的類(FirstViewController)接收並採取行動。

下面是它的實現:

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    NSDictionary *dict = @{@"data": data,
                           @"peerID": peerID
                           };
    
    [[NSNotificationCenter defaultCenter] postNotificationName:@"MCDidReceiveDataNotification"
                                                        object:nil
                                                      userInfo:dict];
}

正如所見,我們建立了一個內容是接受到的 資料和節點的  NSDictionary 物件。接下來,我們推送具有指定名稱的通知,並將字典與它一起傳送。新增完這段簡單的程式碼之後就可以了,我們回到 FirstViewController.m 檔案。

我們下一個任務是讓我們的類能觀察到這個通知,我們會在 viewDidLoad 方法中做這件事。新增下面的程式碼片段:

- (void)viewDidLoad
{
    ...
   
    [[NSNotificationCenter defaultCenter] addObserver:self
         selector:@selector(didReceiveDataWithNotification:)
             name:@"MCDidReceiveDataNotification"
           object:nil];
}

方法 didReceiveDataWithNotification:選擇器中的方法是一個每次這樣的通知到達時都會呼叫的私有方法。我們過會兒要去實現它,但是首先讓我們宣告它。轉到介面的私有部分新增此宣告:

@interface FirstViewController ()
...

-(void)didReceiveDataWithNotification:(NSNotification *)notification;

@end

我們想讓這個方法去做的事很簡單。通知的使用者資訊字典作為一個NSData物件,包含傳送訊息的 peer 和訊息本身。從 peer 物件,我們能得到它的顯示名稱,我們將把這個資料轉換為一個 NSString 物件。一旦完成這些,之後我們就把peer的顯示名稱跟資訊一起新增到文字檢視。

實現程式碼如下:

-(void)didReceiveDataWithNotification:(NSNotification *)notification{
    MCPeerID *peerID = [[notification userInfo] objectForKey:@"peerID"];
    NSString *peerDisplayName = peerID.displayName;
   
    NSData *receivedData = [[notification userInfo] objectForKey:@"data"];
    NSString *receivedText = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
   
    [_tvChat performSelectorOnMainThread:@selector(setText:) withObject:[_tvChat.text stringByAppendingString:[NSString stringWithFormat:@"%@ wrote:\n%@\n\n", peerDisplayName, receivedText]] waitUntilDone:NO];
}

請注意,我們通過呼叫performSelectorOnMainThread:withObject:waitUntilDone: 方法來在文字檢視中設定文字。這是因為這些資料是在一個輔助執行緒中接收的,而任何的可視更新總是發生在應用程式的主執行緒。

我們的聊天功能已經準備好了!如果你想測試,首先建立一個連線,然後從點到點互相開始傳送文字訊息。

Multipeer Demo - Chat Sample



Demo下載地址:http://download.csdn.net/detail/u014220518/9454625






相關文章