ios局域網聯機—蘋果官方源碼之WiTap剖析(一)(二)

http://www.it165.net/pro/html/201204/2094.htmlhtml

http://www.it165.net/pro/html/201204/2165.htmlios

 在過去的時間裏,我一直在考慮的事情是,我該寫一篇什麼樣的文章呢?以前的兩篇文章都是先有問題,而後我纔有目的的解決問題,如今個人困擾是,我不知道該寫什麼了呵呵。由於其實,大多數的問題,只要在網上搜索一下(google遠比baidu要強得多),基本上都能找到解決的辦法,已經有了許多相關方面的教程或參考資料了,我並非一個喜歡重複作別人已經作得很好的工做的人,因此我如今須要你的幫助,若是你有好的關於寫什麼方面的文章的建議,請留言告訴我(聲明:應用我並不在行!),若是我能實現的話,必定會寫出來分享給你們,若是寫不出來,你們一塊兒討論下解決也是很好的!!謝謝!!我甚至翻譯了raywenderlich的「怎麼在ios5上作一個簡單的iphone應用程序系列」的第一部分,可是當我想要把他發佈的時候我放棄了,這個不是我擅長的,子龍山人博客翻譯團隊作這個更專業,我不該該把這種翻譯的文章放在我本身的博客上,因此我想我仍是把raywenderlich的這個「怎麼在ios5上作一個簡單的iphone應用程序系列」的3部分都翻譯完後在送給山人比較好(固然得在人家贊成的前提下哈哈)。objective-c

     最終,我以爲把一些經典的源碼分析一下也許會是一個好主意,因此,今天我要寫的是蘋果官方的源碼witap例子的分析。因此,首先你須要下載這個官方的源碼編程

前提

     咱們文章的標題已經揭示了這個witap例子的內容是局域網聯機的,這個對聯機遊戲來講真的頗有用,聯機的話你須要真機設備才能體驗到,正常狀況下你須要兩個真機,不過其實一個真機加一個模擬器也是能夠的,我在學習這個例子的時候就是一個真機加模擬器的組合呵呵.(人窮沒辦法呀☹)數組

     首先咱們先在模擬器上運行一下這個例子好讓咱們對這個程序先有個直觀的感覺。服務器

\

     沒有太多的東西,一個狀態欄,下面是一個UIView,這個UIView裏有一些元素(每一個元素,在後面的具體代碼裏咱們會一一指出),總之,如今界面上顯示的是3條信息,一條是提示咱們等待另外一個玩家加入,下面一條是設備的名字(這裏我是在模擬器上運行的,因此顯示的是個人計算機的名字),最後一條是提示咱們或者要加入另外一個遊戲。咱們隨便在屏幕上點點看,沒有任何反應。咱們仍是乖乖的聽話,讓另外一個玩家加入咱們吧,否則真的咱們什麼都作不了,首先確保你的兩臺設備(我是個人電腦和touch)都在同一個網絡內,而後打開真機上的這個程序。網絡

\

look,咱們的模擬器發現了個人touch,哈哈,一樣的,在真機的tableView裏,你也會看到你的模擬器的名字。如今兩臺機器已經發現彼此了,然咱們點一下tableView裏的名字試試看吧,有反應了,咱們進入了蘋果給咱們帶來的小遊戲:app

\

     點擊彈出通知的continue來繼續遊戲,在遊戲裏隨便點點,咱們點擊屏幕上的任意一個色塊,咱們的另外一臺設備上的同一色塊出現被同步的點擊的效果,其實還挺好玩的呵呵。框架

開始

     好了,咱們如今對這個例子有了直觀的認識了,讓咱們來開始一步步的分析它吧,對於代碼裏涉及到的知識點咱們會進行不限於代碼範圍的講述。dom

     首先,讓咱們來從main.m文件開始吧。在other Source文件夾裏,咱們選中main.m來看一下它的代碼。

 

01. #import <UIKit/UIKit.h>
02.  
03. int main(int argc, char *argv[])
04. {
05. NSAutoreleasePool *pool = [NSAutoreleasePool new];
06. UIApplicationMain(argc, argv, nil, @"AppController");
07. [pool release];
08. return 0;
09. }

 

  第一行是導入UIKit框架,這個不用說了。

      下面是main函數,和c程序同樣,這個main函數也是咱們的程序的入口。其實咱們的程序的起點是start函數,在start函數裏調用了main函數,而後在main函數裏邊,構建了一個自動釋放池(這個witap的例子最新的版本是1.8,這個版本並無針對ios5更新,因此這裏的代碼仍是用的自動釋放池,在ios5以後因爲引入了ARC,因此ios5的main函數和以前的版本的main函數是有變化的,再也不使用自動釋放池了)。

      在這個main函數裏邊最重要的一句代碼就是:UIApplicationMain(argc, argv, nil, @"AppController"),這句是開始咱們程序的關鍵,前兩個參數就是main本身的參數,一個表明命令行參數的個數,一個是指向全部命令行參數的指針。第三個參數是表明咱們的程序的主類,若是爲nil則表明主類是UIApplication類,若是程序中使用自定義的UIApplication類的子類做爲主類,你須要本身在這裏指定,不過不推薦這樣作!!。第四個參數是咱們的代理類,若是爲nil的話,則程序假設程序的代理來自Main nib文件(ios5以前,ios5改爲用委託類的類名生成的字符串來指定了)。

      那麼UIApplicationMain這個函數又作了什麼呢?在這個函數裏邊,咱們根據咱們的參數「主類名」,這裏是UIApplication類,來實例化一個主類。而後對這個實例會設置他的委託爲咱們在第四個參數裏指定的類,並調用_run方法,_run方法又會調用CFRunLoopRunInMode(⋯⋯),CFRunLoopRunInMode方法又會根據它的參數來以相應的模式運行RunLoop(RunLoop的概念很重要,咱們後邊會說明),這樣註冊到這個模式下的事件在發生時,咱們相應的事件處理方法就會收到消息並處理。(這個須要結合下一段來理解)

      RunLoop是一個運行迴路,每一個線程都有一個本身的RunLoop,咱們平時並不用管理它是由於咱們的主線程中的RunLoop默認狀況下就啓動了,UIApplication類幫咱們作的。RunLoop作的具體的工做是監測輸入源,若是有輸入源事件的話,RunLoop分發這個事件給事件的處理方法來處理事件,若是沒有輸入源事件的話,RunLoop就讓咱們的線程休眠,什麼都不作來節省資源消耗。那麼什麼是輸入源呢?輸入源包括:用戶設備輸入、網絡鏈接、週期或延遲事件、異步回調。RunLoop能監測3中類型的對象:sources (CFRunLoopSource Reference), timers (CFRunLoopTimer Reference), and observers (CFRunLoopObserver Reference),要讓RunLoop監測這些對象,須要先把他們加入到RunLoop中,針對這三種對象,有3個不一樣的方法用來加入到RunLoop中:CFRunLoopAddSourceCFRunLoopAddTimerCFRunLoopAddObserver,這樣加入到RunLoop後,RunLoop纔會監測這些輸入源,若是不想繼續監測這個輸入源的話,能夠用CFRunLoopRemoveSource方法從RunLoop中移出輸入源。還有一點要強調的是,在把這些輸入源加入到RunLoop時,咱們必需要把這些輸入源關聯到一個或多個RunLoop模式,模式決定了在RunLoop的一次迭代中,什麼事件須要處理,由於RunLoop在運行時就指定了以什麼模式運行,因此,RunLoop只處理那些關聯到它當前運行模式的輸入源。一般狀況下咱們若是添加輸入源到RunLoop的話,咱們會把它和默認模式 kCFRunLoopDefaultMode相關聯,在應用程序或線程閒置的時候就會處理這些事件。事實上你能夠定義本身的模式,經過本身的模式你能夠對須要處理的事件進行限制(具體請參考官方文檔)。

     最後對於一個程序來講,簡單地來總結一下,RunLoop作的工做是什麼。

     咱們的程序有一個UIApplication的實例變量app,這個app實例變量爲咱們在咱們的主線程裏(以某種模式,我我的猜想會是默認模式吧)運行了一個RunLoop,而且它把一些輸入源加入到了這個RunLoop中(好比觸摸事件、網絡鏈接⋯⋯等等),並與當前運行的這個模式關聯,這樣有相應事件發生的時候,RunLoop就會監測到這些事件了,當RunLoop監測到這些事件後,就經過委託(前面設定了爲AppController)層層分發,直到分發給咱們的相應事件的處理方法進行處理,(好比觸摸事件的處理方法是touchBegan、touchMove、touchEnd等),這樣咱們就能正確的處理這些事件了。這也正是咱們能處理觸摸事件的緣由,UIApplication類,已經把相應的輸入源加入到咱們主線程的RunLoop中了。

     下面咱們來看看applicationDidFinishLaunching:方法,它是告訴咱們UIApplication已經準備好了,能夠運行了,是咱們的程序可見代碼部分,在main函數以後運行的第一個方法,打開AppController.m文件:

 

01. - (void) applicationDidFinishLaunching:(UIApplication *)application
02. {
03. CGRect      rect;    //1
04. UIView*     view;
05. NSUInteger  x, y;
06.  
07. //Create a full-screen window     //2
08. _window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
09. [_window setBackgroundColor:[UIColor darkGrayColor]];
10.  
11. //Create the tap views and add them to the view controller's view
12. rect = [[UIScreen mainScreen] applicationFrame];    //3
13. for(y = 0; y < kNumPads; ++y) {   //4
14. for(x = 0; x < kNumPads; ++x) {
15. view = [[TapView alloc] initWithFrame:CGRectMake(rect.origin.x + x * rect.size.width / (float)kNumPads, rect.origin.y + y * rect.size.height / (float)kNumPads, rect.size.width / (float)kNumPads, rect.size.height / (float)kNumPads)];  //5
16. [view setMultipleTouchEnabled:NO];    //6
17. [view setBackgroundColor:[UIColor colorWithHue:((y * kNumPads + x) / (float)(kNumPads * kNumPads)) saturation:0.75 brightness:0.75 alpha:1.0]];
18. [view setTag:(y * kNumPads + x + 1)];   //7
19. [_window addSubview:view];  //8
20. [view release];  //9
21. }
22. }
23.  
24. //Show the window
25. [_window makeKeyAndVisible]; //10
26.  
27. //Create and advertise a new game and discover other availble games
28. [self setup];   //11
29. }

 

      註釋1,就是聲明一些變量,一個rect,一個view,兩個整數。

      註釋2,實例化一個UIWindow變量,這是咱們的主窗口,並把它的大小區域設爲全屏大小,[[UIScreen mainScreen] bounds]]獲得的就是全屏的區域大小,而後設置它的

                 顏色爲灰色。

      註釋3,把咱們前面申請的rect變量,設爲整個屏幕除了狀態欄的大小區域,[[UIScreen mainScreen] applicationFrame]獲得的就是除了狀態欄的屏幕大小區域。

      註釋4,這個for循環是添加遊戲中的色塊兒的(就是上面遊戲運行圖中的9個色塊),在AppController.m文件的最上邊,咱們看到咱們用宏定義了KNumPads爲3,因此這裏

                是外循環和內循環都是3次,共9次。

      註釋5,這是實例化咱們的色塊兒,並分配給咱們前面申請的view變量。咱們的色塊是單獨的類TapView的實例,它是繼承自UIView的,咱們先無論它的實現,就把它當一個

                UIView來對待就行了,後面咱們會詳細介紹它的內容。在實例化這些色塊的時候咱們經過簡單的計算來給這9個色塊劃分不一樣的區域和位置,使這9個色塊均勻的分佈在

                咱們的屏幕上,固然,是去除了狀態欄以後的區域。

      註釋6,設置咱們的色塊的多點觸摸爲否,這樣咱們的色塊是不會相應多點觸摸了。

      註釋7,給咱們的色塊設置不一樣的tag,這裏是編號1到9。

      註釋8,把咱們的色塊加入主窗口的子集,這樣當咱們的主窗口顯示的時候,咱們的色塊做爲子視圖,也就會顯示了。

      註釋9,覺得咱們把色塊加入window的時候,他幫咱們retain了,因此這裏咱們要release。

      註釋10,顯示咱們的window。

      註釋11,這是調用咱們這個類的setup方法,這個方法後面會詳述,如今先無論了。

 咱們寫着來看看,這個setup方法是什麼:

 

01. [_server release];  //1
02. _server = nil;
03.  
04. [_inStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; //2
05. [_inStream release];
06. _inStream = nil;
07. _inReady = NO;
08.  
09. [_outStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; //3
10. [_outStream release];
11. _outStream = nil;
12. _outReady = NO;
13.  
14. _server = [TCPServer new];//4
15. [_server setDelegate:self];
16. NSError *error = nil;
17. if(_server == nil || ![_server start:&error]) {   //5
18. if (error == nil) {
19. NSLog(@"Failed creating server: Server instance is nil");
20. else {
21. NSLog(@"Failed creating server: %@", error);
22. }
23. [self _showAlert:@"Failed creating server"]; //6
24. return;
25. }
26.  
27. //Start advertising to clients, passing nil for the name to tell Bonjour to pick use default name
28. if(![_server enableBonjourWithDomain:@"local" applicationProtocol:[TCPServer bonjourTypeFromIdentifier:kGameIdentifier] name:nil]) { //7
29. [self _showAlert:@"Failed advertising server"];
30. return;
31. }
32. NSLog(@"the server in appController is: %@",_server.description);
33.  
34. [self presentPicker:nil];  //8
35. }

 

  註釋一、2和3,都是一些清理方法,用來確保當咱們操做的這些變量以前用內容的話,先把它們清空,再從新進行操做。(當程序裏第二次調用這個方法時,就顯出了這幾句的效果)

                         ,這裏先是對一個TCPServer類(後面會講)實例進行置空操做;而後把一對輸入輸出流對象,從當前RunLoop中移出,這樣咱們的RunLoop就再也不監測這兩個輸入

                           輸出流事件了,從RunLoop中移除後,也對它們進行置空操做;最後又把兩個用來標示輸入輸出流是否準備好的Bool變量設爲假,標示咱們沒有準備好。

     標示4,從新初始化這個TCPServer類變量,並把它的委託設爲這個AppController。

     註釋5,判斷這個TCPServer類實例變量_server是否爲空,並對它調用start:方法,並判斷start:方法的返回值。若是有問題根據判斷條件輸出相應的錯誤信息。

     註釋6,當有錯的時候彈出警告窗口來講明失敗狀況。

     註釋7,對這個_server變量調用一個方法,這個方法是用來發布咱們的服務的。(這個例子用Bonjour實現聯機,而boujour實現是經過NSNetService來發布服務,用

               NSNetServiceBrowser來搜索服務來實現的,這也是一個重要的知識點,後面會講)

     註釋8,這個是顯示咱們的這篇文章中第一個圖中的界面的一個方法。

     這個例子真是千頭萬緒呀,我但願一部分一部分的拆開來分析,但是它的每一部分老是和其餘內容相關聯,摘不出來呀,鬱悶!好了,咱們繼續吧,若是要弄明白這個setup方法到底作了什麼,咱們必需得先分析這個TCPServer類才行,而後還要細分這個註釋8中的方法才能真正瞭解這個setup方法作了哪些工做。

難啃的骨頭

     咱們來見識一下這個TCPServer的真面目吧,打開TCPServer.h文件:

 

01. @class TCPServer;
02.  
03. NSString * const TCPServerErrorDomain;
04.  
05. typedef enum {
06. kTCPServerCouldNotBindToIPv4Address = 1,
07. kTCPServerCouldNotBindToIPv6Address = 2,
08. kTCPServerNoSocketsAvailable = 3,
09. } TCPServerErrorCode;

  在文件的最上面部分,咱們看到,咱們先用@class來修飾咱們的TCPServer,這是告訴編譯器,這個TCPServer是一個類,這樣咱們就能夠在尚未聲明這個類的時候在方法裏先用,在之後在實現它的定義。(這樣也就是爲何咱們後面的協議方法中用到了TCPServer類,但這個類的聲明卻在協議以後,而咱們在編譯是不報錯的緣由)

     咱們又聲明瞭一個字符串常量,它的定義是在TCPServer.m文件裏的,在這裏只是聲明。

     定義了TCPServer的錯誤代碼的枚舉值,用來表示不一樣的錯誤狀況。

     接着,咱們定義了一個協議(協議是objective-c中,不一樣的類之間溝通的好方法,我的以爲和symbian裏的M類基本上同樣):

 

1. @protocol TCPServerDelegate <NSObject>
2. @optional
3. - (void) serverDidEnableBonjour:(TCPServer*)server withName:(NSString*)name;
4. - (void) server:(TCPServer*)server didNotEnableBonjour:(NSDictionary *)errorDict;
5. - (void) didAcceptConnectionForServer:(TCPServer*)server inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr;
6. @end

  協議的名字叫:TCPServerDelegate,這個協議有3個可選的方法,這三個方法第一個是TCPServer用boujour發佈服務成功以後咱們用來處理一些東西的方法,第二個是失敗的時候咱們用來處理一些東西的方法,第三個是當TCPServer接受了其餘設備的鏈接請求以後,咱們用來處理東西的方法。

     下面讓咱們看看這個TCPServer類的聲明:

 

01. @interface TCPServer : NSObject <NSNetServiceDelegate> {
02. @private
03. id _delegate;
04.   uint16_t _port;
05. uint32_t protocolFamily;
06. CFSocketRef witap_socket;
07. NSNetService* _netService;
08. }
09.  
10. - (BOOL)start:(NSError **)error;
11. - (BOOL)stop;
12. - (BOOL) enableBonjourWithDomain:(NSString*)domain applicationProtocol:(NSString*)protocol name:(NSString*)name; 
13. - (void) disableBonjour;
14.  
15. @property(assign) id<TCPServerDelegate> delegate;
16.  
17. + (NSString*) bonjourTypeFromIdentifier:(NSString*)identifier;

 

  這個TCPServer類,被聲明繼承自NSObject類,而且它遵照NSNetServiceDelegate協議,這個協議是咱們的NSNetService類的一些回調方法,就是說若是咱們的NSNetService服務發佈成功或者失敗的話,會調用這個協議裏的相應方法來進行處理。事實上這個協議的全部方法都是可選的,若是你不實現他們也不會出錯,不過那樣的話,咱們就不能在服務發佈狀態改變是作相應的處理了。

     在這個interface裏,聲明瞭5個私有變量,一個id類的_delegate,它用來跟蹤咱們這個TCPServer類的委託,一個uint16_t類型的_port變量,存儲咱們發佈服務時綁定的Socket的端口號,一個uint32_t類型的protocolFamily,來存儲咱們的socket的協議族,一個CFSocketRef類的 witap_socket,就是咱們的等待其餘設備鏈接的socket,一個NSNetService類的_netService,就是咱們用來發布服務的NSNetService。

     start:方法,咱們在這個方法裏建立並配置咱們用來監聽網絡鏈接的socket,並建立RunLoop輸入源,加入到當前RunLoop中,這樣只要有咱們的這個socket有鏈接事件,咱們就能獲得通知並觸發相應的回調。

     stop方法,明顯不過了,它是中止咱們的網絡鏈接服務的,讓咱們取消對網絡鏈接事件的監聽,並釋放這個監聽的socket。

     disableBonjour方法,中止咱們的當前的已經發布的服務。

     enableBonjourWithDomain:applicationProtocol:name:方法是事實上進行NSNetService服務發佈的方法。

     接着是一個聲明,聲明瞭一個id<TCPServerDelegate>的屬性delegate,這是一個知足TCPServerDelegate協議的屬性。

     最後是一個bonjourTypeFromIdentifier:方法,這是個輔助方法,它用來返回咱們要發佈的服務的協議的(這個協議不是委託類的協議,它只是一個表明惟一標識的字符串),而且這個字符串是用要求的,它不能超過14個字符,而且只能包含小寫字母、數字和鏈接符,開關和結尾不能是鏈接符。

      下面,咱們該看看這個類的具體實現了,打開TCPServer.m文件:

 

1. #import "TCPServer.h"
2.  
3. NSString * const TCPServerErrorDomain = @"TCPServerErrorDomain";
4.  
5. @interface TCPServer ()
6. @property(nonatomic,retain) NSNetService* netService;
7. @property(assign) uint16_t port;
8. @end

 

  首先是包含TCPServer.h文件,而後是咱們在.h文件中聲明的那個常量字符串的定義。

      而後下面……(其實我不太明白蘋果這個地方的用法,正常狀況下我會以爲這是一個分類,可是若是是分類的話,是不能夠添加實例變量的,這個地方是添加了兩個屬性,這個什麼狀況??就算能夠在分類裏添加屬性,那爲何要這麼作呢?爲何不在正常的原始類裏添加呢?是否是由於這樣的話這個屬性是在TCPServer.m文件裏的,那麼在這個分類裏聲明的屬性就對外不可見不可用了呢?那又爲何分類沒有本身的implementation呢?它怎麼和原始類共用一個呢?若是你知道這個緣由,請告訴我,不勝感激!!!)

      再接着是這個TCPServer類的實現部分:      

 

01. @implementation TCPServer
02.  
03. @synthesize delegate=_delegate, netService=_netService, port=_port;
04.  
05. - (id)init {
06. return self;
07. }
08.  
09. - (void)dealloc {
10. [self stop];
11. [super dealloc];
12. }

 

    這裏合成了三個屬性的set和get方法;而後是初始化方法,只是簡單地返回本身;而後是dealloc方法,這會在咱們這個類銷燬時調用,這個方法裏是先調用stop方法停掉網絡鏈接服務,而後調用父類的dealloc方法。

     再接着看這個handleNewConnectionFromAddress方法:

 

1. - (void)handleNewConnectionFromAddress:(NSData *)addr inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr {
2.  
3. // if the delegate implements the delegate method, call it  
4. if (self.delegate && [self.delegate respondsToSelector:@selector(didAcceptConnectionForServer:inputStream:outputStream:)]) { 
5. [self.delegate didAcceptConnectionForServer:self inputStream:istr outputStream:ostr];
6. }
7. }

  在這個方法裏,咱們先判斷self的委託是否爲空(咱們在AppController.m的setup方法裏的註釋4中,把委託設爲了AppController),並判斷這個委託響不響應didAcceptConnectionForServer:inputStream:outputStream:方法,若是響應,就對self的委託調用這個方法來處理一些事情。

      如今輪到一個重量級的方法了,TCPServerAcceptCallBack:

 

01. static void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
02.  
03. TCPServer *server = (TCPServer *)info;
04. NSLog(@"the server in call back is: %@",server.description);
05.  
06. if (kCFSocketAcceptCallBack == type) { 
07. // for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
08. CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
09. uint8_t name[SOCK_MAXADDRLEN];
10. socklen_t namelen = sizeof(name);
11. NSData *peer = nil;
12. if (0 == getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen)) {
13. peer = [NSData dataWithBytes:name length:namelen];
14. }
15. CFReadStreamRef readStream = NULL;
16. CFWriteStreamRef writeStream = NULL;
17. CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStream, &writeStream);
18. if (readStream && writeStream) {
19. CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
20. CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
21. [server handleNewConnectionFromAddress:peer inputStream:(NSInputStream *)readStream outputStream:(NSOutputStream *)writeStream];
22. else {
23. // on any failure, need to destroy the CFSocketNativeHandle 
24. // since we are not going to use it any more
25. close(nativeSocketHandle);
26. }
27. if (readStream) CFRelease(readStream);
28. if (writeStream) CFRelease(writeStream);
29. }
30. }

  這是一個回調方法,在咱們的監聽網絡鏈接的socket,接收到鏈接事件後,這個回調方法就被調用。須要說明的是,這個回調方法的格式是固定的。緣由是這樣的,這個回調方法是要在建立socket的時候傳遞給socketCreat方法的callout參數的,這個callout是一個函數指針,這個函數指針是CFSocketCallBack 類型的,因此咱們的這個回調方法也應該是這個類型的。咱們來看一下這個類型:        

 

1. typedef void (*CFSocketCallBack) (
2. CFSocketRef s,
3. CFSocketCallBackType callbackType,
4. CFDataRef address,
5. const void *data,
6. void *info
7. );

  這個類型就是咱們的函數指針的定義,它的返回值是void的,也就是沒有返回值;再來看看它的參數:

      很明顯,第一個參數是觸發了這個回調的socket自己,第二個是觸發這個回調的事件類型,第三個表明請求鏈接的遠端設備的地址,第四個參數有點神奇,它根據回調事件的不一樣,它表明的東西也不一樣,若是這個是鏈接失敗回調事件,那它就表明一個錯誤代碼的指針,若是是鏈接成功的回調事件,它就是一個Socket指針,若是是數據回調事件,這就是包含這些數據的指針,其它狀況下它是NULL的,最後一個參數是咱們建立socket的時候用的那個CFSocketContext結構的info成員。

     明白這個函數指針的類型再對照着咱們的回調函數看是否是結構徹底同樣,呵呵。

       這個例子要寫的東西遠比我想的要多,太長了,因此,這篇就先到這兒吧,後面的內容會再發後續章節。

 

 這篇文章是"ios局域網聯機——蘋果官方源碼之WiTap剖析"系列的第二部分,它和第一部分牢牢相連,所以閱讀此文章的前提是你已經閱讀了ios局域網聯機—蘋果官方源碼之WiTap剖析(一)

打起精神繼續戰鬥

     好吧,讓咱們接着第一部分繼續剖析這個TCPServer.m文件吧,上一部分中咱們是講到了TCPServerAcceptCallBack:這個回調方法,講到了它的格式,它是一個符合CFSocketCallBack這個函數指針類型的函數。

     如今讓咱們繼續研究它的實現吧,爲了方便閱讀,咱們再一次把這個函數的實現展現出來:
 

01. static void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
02.  
03.  
04.  
05. TCPServer *server = (TCPServer *)info;  //1
06.  
07.  
08. if (kCFSocketAcceptCallBack == type) {   //2
09.  
10. // for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
11.  
12. CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;  //3
13.  
14. uint8_t name[SOCK_MAXADDRLEN];   //4
15.  
16. socklen_t namelen = sizeof(name);
17.  
18. NSData *peer = nil; //5
19.  
20. if (0 == getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen)) {
21.  
22. peer = [NSData dataWithBytes:name length:namelen];
23.  
24. }
25.  
26. CFReadStreamRef readStream = NULL;   //6
27.  
28. CFWriteStreamRef writeStream = NULL;
29.  
30. CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStream, &writeStream);
31.  
32. if (readStream && writeStream) {
33.  
34. CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); //7
35.  
36. CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
37.  
38. [server handleNewConnectionFromAddress:peer inputStream:(NSInputStream *)readStream outputStream:(NSOutputStream *)writeStream];
39.  
40. else {
41.  
42. // on any failure, need to destroy the CFSocketNativeHandle
43.  
44. // since we are not going to use it any more
45.  
46. close(nativeSocketHandle);
47.  
48. }
49.  
50. if (readStream) CFRelease(readStream);   //8
51.  
52. if (writeStream) CFRelease(writeStream);
53.  
54. }
55.  
56. }

 

  註釋1,咱們看到這裏它把這個函數的參數info轉成了一個TCPServer類型,並申請了一個TCPServer類的變量來跟蹤它。(咱們在第一部分裏已經說了,這裏的info參數就是觸發這個回調的socket再被建立時,傳入到建立函數裏的CFSocketContext結構的info成員,在後面的start方法裏咱們會看到,這個結構的info成員是咱們的這個TCPServer類自己)

     註釋2,這裏是判斷一下咱們此次回調的事件類型,若是事件是成功鏈接咱們就進行一系列操做,不然咱們什麼也不作。

     註釋3,這裏咱們是把這個函數的參數data轉成了一個CFSocketNativeHandle類型。(一樣的也是咱們在第一部分裏說過的,當這個回調事件的類型是鏈接成功的時候,這個data就是一個CFSocketNativeHandle類型指針,這個CFSocketNativeHandle類型其實就是咱們的特定平臺的socket,你就當成正常的socket理解就好了,值得注意的是這裏的socket是什麼,是呢兒來的?咱們知道,在正常的socket流程中,做爲服務器的一方會有一個socket一直處於監聽鏈接的狀態,一旦有新的鏈接請求到來,系統會本身建立一個新的socket與這個請求的客戶端進行鏈接,此後客戶端和服務器端就經過這個新的鏈接進行通信,而服務器負責監聽網絡鏈接的socket則繼續監聽鏈接。如今這個函數裏的這個data應該就是在響應鏈接請求的時候系統本身建立的新的socket吧。<聲明:這些概念我是瞭解自互聯網,若是有什麼不對的地方請及時指出,我會及時糾正,以避免誤導他人>)

     註釋4,申請了一個255大小的數組用來接收這個新的data轉成的socket的地址,還申請了一個socklen_t變量來接收這個地址結構的大小。

     註釋5,這裏又申請了一個NSData類的變量peer,在這個註釋的if語句的{}裏,咱們看到它是用來存儲咱們的新的socket的地址的。咱們先來看這個if語句的判斷表達式吧,其實這裏是一個getpeername()函數的調用,這個函數有3個參數,第一個參數是一個已經鏈接的socket,這裏就是nativeSocketHandle;第二個參數是用來接收地址結構的,就是說這個函數從第一個參數的socket中獲取與它捆綁的端口號和地址等信息,並把它存放在這第二個參數中;第三個參數差很少了,它是取出這個socket的地址結構的數據長度放到這個參數裏面。若是沒有錯誤的話這個函數會返回0,若是有錯誤的話會返回一個錯誤代碼。這裏判斷了getpeername的返回值,沒有錯誤的狀況下,把獲得的地址結構存儲到咱們申請的peer裏。

     註釋6,申請了一對輸入輸出流,用CFStreamCreatePairWithSocket()方法把咱們申請的這一對輸入輸出流和咱們的已創建鏈接的socket(即如今的nativeSocketHandle)進行綁定,這樣咱們的這個鏈接就能夠經過這一對流進行輸入輸出的操做了,這個函數操做完成以後,這兩個輸入輸出流會被從新指向,使其指向有效的地址區域。此函數的的一個參數是一個內存分配器(蘋果管理優化內存的一種措施,更多信息可網上查詢),第二個參數就是想用咱們第三和第四個參數表明的輸入輸出流的socket,第三和第四個參數就是要綁定到第二個參數表示的socket的輸入輸出流的地址。

     註釋7,若是咱們的CFStreamCreatePairWithSocket()方法操做成功的話,那麼咱們如今的readStream和writeStream應該指向有效的地址,而不是咱們在剛申請時賦給的NULL了。此時,判斷這兩個流是否是NULL就等於說咱們判斷函數CFStreamCreatePairWithSocket()有沒有操做成功。

      若是成功的話,咱們就設置這兩個流的屬性,這裏是把這兩個流的屬性kCFStreamPropertyShouldCloseNativeSocket設置爲真,默認狀況下這個屬性是假的,這個設爲真就是說,若是咱們的流釋放的話,咱們這個流綁定的socket也要釋放。(這裏是對兩個流都進行了相同的屬性設置,事實上蘋果的文檔裏在對CFStreamCreaterPairWithSocket()這個方法說明的時候提到,大多數流的屬性是共享的,你只要對這一對中的一個設置了屬性,那麼也會自動爲另外一個流設置這個屬性)。而後咱們對註釋1中獲得的TCPServer類的server變量調用handleNewConnectionFromAddress:方法,這個方法的三個參數,一個是已鏈接socket的地址,另兩個就是輸入和輸出流了。這個方法的內容很簡單,在本系列的第一部分咱們已經介紹了,這裏就不重複了。

      若是失敗的話,咱們就銷燬着了已經鏈接的socket。

      註釋8,這裏是先對流的內容進行清空操做,防止在使用它們的時候,裏面有咱們不須要的垃圾數據。

      真是出了一頭汗呀,這個函數太費勁了講起來⋯⋯好在是說完了,不過,呵呵,下一個函數依然是艱鉅呀。

艱苦繼續,你敢堅持嗎?

     哈哈,又是一個巨大的函數呀,恐怖的start方法,不要懼怕,咱們會把它分析的支離破碎的哈哈哈,好吧,先看看它的實現:

 

001. (BOOL)start:(NSError **)error {
002.  
003.  
004.  
005. CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};        //1
006.  
007.  
008.  
009. // Start by trying to do everything with IPv6.  This will work for both IPv4 and IPv6 clients
010.  
011. // via the miracle of mapped IPv4 addresses.   
012.  
013.  
014.  
015. witap_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET6, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&TCPServerAcceptCallBack, &socketCtxt);       //2
016.  
017.  
018.  
019. if (witap_socket != NULL) // the socket was created successfully   //3
020.  
021. {
022.  
023. protocolFamily = PF_INET6;      //4
024.  
025. else // there was an error creating the IPv6 socket - could be running under iOS 3.x   //5
026.  
027. {      
028.  
029. witap_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&TCPServerAcceptCallBack, &socketCtxt);
030.  
031. if (witap_socket != NULL)
032.  
033. {
034.  
035. protocolFamily = PF_INET;
036.  
037. }
038.  
039. }
040.  
041.  
042.  
043. if (NULL == witap_socket) {     //6
044.  
045. if (error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerNoSocketsAvailable userInfo:nil];
046.  
047. if (witap_socket) CFRelease(witap_socket);
048.  
049. witap_socket = NULL;
050.  
051. return NO;
052.  
053. }
054.  
055.  
056.  
057.  
058.  
059. int yes = 1;  //7
060.  
061. setsockopt(CFSocketGetNative(witap_socket), SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes));
062.  
063.  
064.  
065. // set up the IP endpoint; use port 0, so the kernel will choose an arbitrary port for us, which will be advertised using Bonjour
066.  
067. if (protocolFamily == PF_INET6)  //8
068.  
069. {
070.  
071. struct sockaddr_in6 addr6;
072.  
073. memset(&addr6, 0, sizeof(addr6));
074.  
075. addr6.sin6_len = sizeof(addr6);
076.  
077. addr6.sin6_family = AF_INET6;
078.  
079. addr6.sin6_port = 0;
080.  
081. addr6.sin6_flowinfo = 0;
082.  
083. addr6.sin6_addr = in6addr_any;
084.  
085. NSData *address6 = [NSData dataWithBytes:&addr6 length:sizeof(addr6)];
086.  
087.  
088.  
089. if (kCFSocketSuccess != CFSocketSetAddress(witap_socket, (CFDataRef)address6)) {
090.  
091. if (error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerCouldNotBindToIPv6Address userInfo:nil];
092.  
093. if (witap_socket) CFRelease(witap_socket);
094.  
095. witap_socket = NULL;
096.  
097. return NO;
098.  
099. }
100.  
101.  
102.  
103. // now that the binding was successful, we get the port number
104.  
105. // -- we will need it for the NSNetService
106.  
107. NSData *addr = [(NSData *)CFSocketCopyAddress(witap_socket) autorelease];
108.  
109. memcpy(&addr6, [addr bytes], [addr length]);
110.  
111. self.port = ntohs(addr6.sin6_port);
112.  
113.  
114.  
115. else {              //9
116.  
117. struct sockaddr_in addr4;
118.  
119. memset(&addr4, 0, sizeof(addr4));
120.  
121. addr4.sin_len = sizeof(addr4);
122.  
123. addr4.sin_family = AF_INET;
124.  
125. addr4.sin_port = 0;
126.  
127. addr4.sin_addr.s_addr = htonl(INADDR_ANY);
128.  
129. NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
130.  
131.  
132.  
133. if (kCFSocketSuccess != CFSocketSetAddress(witap_socket, (CFDataRef)address4)) {
134.  
135. if (error) *error = [[NSError alloc] initWithDomain:TCPServerErrorDomain code:kTCPServerCouldNotBindToIPv4Address userInfo:nil];
136.  
137. if (witap_socket) CFRelease(witap_socket);
138.  
139. witap_socket = NULL;
140.  
141. return NO;
142.  
143. }
144.  
145.  
146.  
147. // now that the binding was successful, we get the port number
148.  
149. // -- we will need it for the NSNetService
150.  
151. NSData *addr = [(NSData *)CFSocketCopyAddress(witap_socket) autorelease];
152.  
153. memcpy(&addr4, [addr bytes], [addr length]);
154.  
155. self.port = ntohs(addr4.sin_port);
156.  
157. }
158.  
159.  
160.  
161. // set up the run loop sources for the sockets
162.  
163. CFRunLoopRef cfrl = CFRunLoopGetCurrent();    //10
164.  
165. CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, witap_socket, 0);
166.  
167. CFRunLoopAddSource(cfrl, source, kCFRunLoopCommonModes);
168.  
169. CFRelease(source);
170.  
171.  
172.  
173. return YES;
174.  
175. }

 

   先看一下這個函數的定義,他返回BOOL類型的值以表示指望的操做是否成功;他還有一個NSError**類型的參數,咱們看定義會發現,其實NSError是一個繼承者NSObject的類,NSError**是一個指針的指針,就是說error這個參數是一個指針的指針,那麼*error是一個指針,*error指向一個NSError對象。

      註釋1,咱們定義了一個CFSocketContext結構類型變量socketCtxt,並對這個結構進行初始化。咱們來解釋一下這個結構的定義,這個結構有5個成員。第一個成員是這個結構的版本號,這個必需是0;第二個成員能夠是一個你程序內定義的任何數據的指針,這裏咱們傳入的是self,就是這們這個類自己了,因此咱們的TCPServerAcceptCallBack這個回調方法能夠把它的info參數轉成TCPServer,而且這個參數會被傳入在這個結構內定義的全部回調函數;第3、4、五這三個成員其實就是3個回調函數的指針,通常咱們都設爲NULL,就是不用它們。(若是你要具體瞭解這個結構,請參考官方文檔

      註釋2,這是建立一個socket並把它賦給咱們在TCPServer.h文件裏定義的witap_socket變量,這個CFSocketCreate()方法有7個參數之多,咱們來一個一個解釋吧,你若是想了解的更清楚我建議你仍是查閱官方文檔。第一個是一個內存分配器(咱們前邊已經提到過了);第二個是咱們要建立的socket的協議族(更多這方面的知識你可能須要參考TCP/IP協議),這裏咱們是傳入PF_INET6(代表咱們但願用IPv6協議);第三個參數是socket的類型,咱們這裏是傳入SOCK_STREAM(代表咱們是要數據流服務的,還有一種選擇是數據報服務,這兩種是基於不一樣的協議的,數據流是基於TCP/IP協議的,數據報是基於UDP協議的);第四個參數是咱們要建立的socket所用的具體的協議,這裏咱們傳入IPPROTO_TCP 代表咱們是遵照TCP/IP協議的;第五個是回調事件的類型,就是說當這個類型的事件發生時咱們的回調函數會被調用,咱們這裏傳入kCFSocketAcceptCallBack代表當鏈接成功裏咱們的回調會被觸發。(這裏能夠設置不僅一個回調事件類型,多個不一樣的事件類型用"|"(位或運算符)連起來就能夠了) ;第六個就是咱們的回調函數的地址,當咱們指定的回調事件出現時就調用這個回調函數,咱們傳入咱們的TCPServerAcceptCallBack()回調函數的地址;第七個是一個結構指針,這個結構就是CFSocketContext類型,它保存socket的上下文信息,咱們這裏傳入咱們在註釋1中定義的socketCtxt的地址。(這個CFSocketCreate()函數會拷貝一份這個結構的數據,因此在出了這個create函數以後,這個結構能夠被置爲NULL。)

      註釋3,咱們經過判斷這個witap_socket是否爲空來判斷咱們剛纔執行的socket建立工做有沒有成功。

      註釋4,若是咱們剛纔的建立工做成功了,咱們就把咱們在TCPServer.h文件中定義的protocolFamily設爲PF_INET6。

      註釋5,若是剛纔的建立失敗了,咱們再進行一次socket的建立工做,此次和剛纔不一樣的是,此次咱們把CFSocketCreate函數的協議族參數設爲PF_INET,這表示此次是使用IPv4協議。一樣的,在建立操做完成以後判斷witap_socket的狀態,若是建立成功了就把protocolFamily設爲PF_INET,若是又失敗了就什麼也不作。

      註釋6,若是兩次建立操做都失敗了,若是咱們的參數error這個指針的指針是用效的,咱們就初始化一個NSError,這個NSError的內容就是咱們在TCPServer.h裏定義好的錯誤代碼,和在TCPServer.m裏定義的常量字符串TCPServerErrorDomain,而後咱們把這個NSError對象賦給*error,*error纔是一個指向NSError類型的指針;下面一句是若是這個witap_socket不爲空,就把這個witap_socket釋放了。(我我的認爲這句是多餘的,這句代碼自己就包含在witap_socket是空的if語句裏了,還有必要再判斷一次嗎?),而後按蘋果的邏輯,把這個witap_socket釋放,以後把它設爲NULL,防止野指針。而後返回一個NO,這是告訴咱們的這個函數的調用都,咱們建立socket失敗了。

      註釋7,首先是定義了一個int變量yes,並初始化它的值是1。而後調用setsockopt()方法來設置socket的選項,這個方法的第一個參數要求是一個socket的描述符,這裏是經過CFSocketGetNative()方法來獲得咱們的socket對象針對於這個ios平臺的描述符;第二個是須要設置的選項定義的層次,這裏是SOL_SOCKET(這方面的東西我並不瞭解);第三個參數是咱們要設置的選項的名字,這裏是SO_REUSEADDR。(表示容許重用本地地址和端口,就是說充許綁定已被使用的地址(或端口號),缺省條件下,一個套接口不能與一個已在使用中的本地地址捆綁。但有時會須要「重用」地址。由於每個鏈接都由本地地址和遠端地址的組合惟一肯定,因此只要遠端地址不一樣,兩個套接口與一個地址捆綁並沒有大礙。);第四個參數是一個指針,指向要設置的選項的選項值的緩衝區,這裏是傳入上面申請的int變量yes的地址,就是說咱們把這個選項設爲1;第五個參數是這個選項值數據緩衝區的大小,這裏用sizeof得友yes的數據長度並傳了進去。

      註釋8,若是咱們的protocolFamily是PF_INET6的話,咱們對這個socket進行相應的配置。先是申請一個sockaddr_in6的結構變量addr6,這個結構是一個IPv6協議的地址結構,咱們來看一下它的成員定義:       

 

1. struct sockaddr_in6 {
2. u_char           sin6_len;     
3. u_char           sin6_family;  
4. u_int16m_t       sin6_port;    
5. u_int32m_t       sin6_flowinfo;
6. struct in6_addr  sin6_addr;    
7. }

 

  咱們來看,很明顯這個sin6_len是這個結構的大小;sin6_family成員是指的協議;sin6_port指的是端口;sin6_flowinfo這個在微軟MSDN上的說明只有一句話,IPv6的流信息;sin6_addr這是一個IN6_ADDR的結構,這個結構是真正存儲咱們的地址的。

      (對於這個sin6_flowinfo,我並不懂這些協議上的東西,因此我查閱資料上的解釋是,sin6_flowinfo是與IPv6新增流標和流量類字段類相對應的一個選項,咱們在編程時一般設爲0。)

      如今讓咱們來看看這裏就作了什麼吧,咱們先用memset方法把這個剛申請的結構清零;而後把結構的大小賦給告終構成員sin6_len;把結構的協議族設爲AF_INET6;這裏把這個結構裏的端口號設爲0,那麼在socket進行綁定操做的時候,系統會爲咱們分配一個任意可用的端口;這個sin6_flowinfo置爲0就不說了;把這個地址結構的sin6_addr成員設置爲in6addr_any,這裏更清晰的解釋仍是請你查閱資料,這裏能夠簡單理解爲填上這個值系統會自動爲咱們填上一個可用的本地地址(這個一個可用的本地地址是說,有的機器可能會用多個網卡,會有多個地址);而後申請一個NSData變量address6把咱們的地址結構addr6的信息進行拷貝存儲。

     而後下面,調用CFSocketSetAddress方法把咱們的witap_socket和上面剛設置好的地址address6進行綁定,這其實就是BSDSocket裏的bind同樣的。而後這裏判斷了綁定操做的執行結果,若是綁定失敗的話,就進行相應的清理工做並返回失敗(這些具體代碼咱們以前已經說過了),若是成功的話,就從這個綁定好的socket裏拷貝出實際的地址並存儲在addr6裏,並把在TCPServer.h裏定義的屬性port設爲系統爲這個socket分配的實際的端口號,在後面發佈NSNetService的時候須要用這個端口號。

     註釋9,這個基本上和註釋8是同樣的,略微不一樣的是這個是基於IPv4的操做,小小的不一樣相信只要一看就明白了,我就再也不重複說明了吧呵呵。

     註釋10,這裏申請了一個RunLoop的變量cfrl用來跟蹤當前的RunLoop,經過CFRunLoopGetCurrent()方法獲得當前線程正在運行的RunLoop,而後把它賦給cfrl;而後建立了一個RunLoop的輸入源變量source,這裏經過CFSocketCreateRunLoopSource()方法,這個方法的第一個參數是內存分配器,第二個就是咱們想要作爲輸入源來監聽的socket對象,第三個參數是表明在RunLoop中處理這些輸入源事件時的優先級,數小的話優先級高。而後把這個建立的RunLoop輸入源賦給變量source,接着把這個輸入源source加入到當前RunLoop來進行監測,把輸入源加入到RunLoop是經過CFRunLoopAddSource()這個方法實現的,這個方法的第一個參數就是咱們但願加入的RunLoop,第二個參數是要加入到第一個參數裏的輸入源,這裏是source,最後一個參數就是這個咱們要加入的輸入源要關聯的模式,這裏是傳入的kCFRunLoopCommonModes。(這裏的kCFRunLoopCommonModes須要說明,這個kCFRunLoopCommonModes它並非一個模式,蘋果稱它爲僞模式,它實際上是幾個模式的合集,kCFRunLoopDefaultMode一定是這個KCFRunLoopCommonModes的一個子集。你能夠本身加入一些其它的模式到這個KCFRunLoopCommonModes裏,這個通俗點解釋怎麼說呢,好比說這個KCFRunLoopCommonModes裏有兩個子集,即有兩個模式,咱們假設是模式1和模式2,那麼當咱們把輸入源關聯到模式的時候傳入KCFRunLoopCommonModes的話,這個輸入源就會和這兩個模式,模式1和模式2,都進行關聯,這樣無論咱們的RunLoop是以模式1運行的仍是以模式2運行的,它都會監測咱們的這個輸入源);加入了輸入源以後RunLoop就自動保持了這個輸入源,咱們如今就能夠釋放這個輸入源了。最後返回操做成功。

 最黑暗的已通過去,準備迎接光明

       終於這個巨無霸方法start告一段落了,剩下的都沒有這麼長的了,能夠稍微放鬆一下了,來看看簡單的stop方法:

 

01. - (BOOL)stop {
02.  
03.   [self disableBonjour];   //1
04.  
05. if (witap_socket) {     //2
06.  
07. CFSocketInvalidate(witap_socket);
08.  
09. CFRelease(witap_socket);
10.  
11. witap_socket = NULL;
12.  
13. }       
14.  
15. return YES;
16.  
17. }

 

  看過了start的龐大,再看看這個stop是否是有點小清新的感受哈哈哈哈。首先看看它是一個返回BOOL型變量的方法,不過它在任何狀況下都返回YES,其實這個返回值就沒什麼意義了。

     註釋1,這是調用它的disableBonjour方法來中止它的NSNetService服務的。(後面咱們會講這個方法)

     註釋2,判斷這個用來監聽網絡鏈接的socket是否有效,若是爲真,就先把這個socket設爲無效,再釋放這個sockt資源,並把它置爲NULL。

     讓咱們看一個stop方法裏調用的disableBonjour方法又作了什麼:

 

01. (void) disableBonjour
02.  
03. {
04.  
05. if (self.netService) {
06.  
07. [self.netService stop];
08.  
09. [self.netService removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
10.  
11. self.netService = nil;
12.  
13. }
14.  
15. }

 

  它也是很簡單的,先判斷這個發佈的netService服務是否是有效,若是不是就先中止這個服務,而後把它從RunLoop裏移除使其再也不被監聽(後面會看到NSNetService也是須要加入RunLoop來進行監測的),而後把這個netService置爲NULL。

      下面咱們再來看一個比較關鍵的方法:enableBonjourWithDomain方法:

 

01. (BOOL) enableBonjourWithDomain:(NSString*)domain applicationProtocol:(NSString*)protocol name:(NSString*)name
02.  
03. {
04.  
05.  
06.  
07. if(![domain length])    //1
08.  
09. domain = @""//Will use default Bonjour registration doamins, typically just ".local"
10.  
11. if(![name length])
12.  
13. name = @""//Will use default Bonjour name, e.g. the name assigned to the device in iTunes
14.  
15.  
16.  
17. if(!protocol || ![protocol length] || witap_socket == NULL)   //2
18.  
19. return NO;
20.  
21.  
22.  
23.  
24.  
25. self.netService = [[NSNetService alloc] initWithDomain:domain type:protocol name:name port:self.port];   //3
26.  
27. if(self.netService == nil)
28.  
29. return NO;
30.  
31.  
32.  
33. [self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];  //4
34.  
35. [self.netService publish];
36.  
37. [self.netService setDelegate:self];
38.  
39. return YES;
40.  
41. }

 

  這個方法也用一個BOOL的返回值表示操做的成功或失敗。而且這個方法有三個參數,它們都是咱們用來發布NSNetService服務時要用的參數。第一個參數是發佈服務用的域,第二個參數是咱們要發佈的網絡服務的類型信息,第三個參數是用來表示咱們這個服務的名字。

     註釋1,若是咱們的參數domain和name若是字符長度是0的話,就把它們設爲@「」。(等下會解釋這個@「」在這裏的意義)

     註釋2,若是參數protocol,即服務的協議,若是它不存在,或者它的字符長度爲0,又或者witap_socket爲NULL(也就是說咱們這個用來監聽的socket無效的話),直接返回失敗。

     註釋3,這裏是初始化一個NSNetService服務,並把它賦給在TCPServer.h裏定義的netService屬性。這個NSNetService的初始化方法用到了4個參數,前3個就是咱們如今正在解釋的這個方法的3個參數,最後一個是咱們以前獲得的端口號。第一個參數domain,它表明咱們發佈服務用的域,本地域是用@"local."來表示的,可是當咱們想用本地域的時候,是不用直接傳這個@"local."字符串進去的,咱們只要傳@""進去就好了,系統會本身把它按成本地域來使用的;第二個參數這個網絡服務的類型,這個類型必需包含服務類型和傳輸層信息(傳輸層概念請參考TCP/IP協議),這個服務類型的名字和傳輸層的名字都要有「_」字符做爲前綴。好比這個例子的服務類型的完整的名字實際上是@"_witap._tcp.",看到了吧,它們都有前綴"_",這裏還有一點是要強調的,在這字符串結尾的"."符號是必需的,它表示這個域名字是絕對的;第三個參數是這個服務的名字,這個名字必需是惟一的,若是這個名字是@""的話,系統會自動把設備的名字做爲這個服務的名字;第四個參數就是端口號了,這是咱們發佈這個服務用的。這個端口號必須是在應用程序裏爲這個服務得到的,這裏就是witap_socket在綁定時咱們得到的那個端口號,得到以後賦給了port屬性,因此這裏傳入的是self.port。初始化以後,把這個初始化過的NSNetService賦給.h文件裏定義的netService屬性,接着經過判斷這個netService屬性是否有效來判斷這個NSNetService的初始化是否成功。若是初始化失敗的話,直接返回操做失敗。

      註釋4,對這個netService屬性調用scheduleInRunLoop:forMode:方法,從名字也能看得出來,這是把這個netService加入到當前RunLoop中,並關聯到相應的模式。這部份內容在前面的start方法的註釋10中已經有了詳細的講述,這裏就不囉嗦了。而後對這個netService調用publish方法,這個方法是真正的發佈咱們的服務的。接着又設置這個netService的委託爲這個TCPService類自己,netService是一個NSNetService類,這個類的委託是一個符合NSNetServiceDelegate的通用類型,在TCPService類的interface聲明部分,咱們看到這個類是聲明本身符合NSNetServiceDelegate這個協議的,因此這裏能夠把netService的委託設爲這個TCPService類自己。最後,返回操做成功。

柳暗花明

      到了這一步,這個類剩下的內容就很明瞭了,讓咱們看看NSNetServiceDelegate協議的兩個方法吧:

 

01. (void)netServiceDidPublish:(NSNetService *)sender
02.  
03. {
04.  
05.  
06.  
07. if (self.delegate && [self.delegate respondsToSelector:@selector(serverDidEnableBonjour:withName:)])
08.  
09. [self.delegate serverDidEnableBonjour:self withName:sender.name];
10.  
11. }
12.  
13.  
14.  
15. - (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict
16.  
17. {
18.  
19. if (self.delegate && [self.delegate respondsToSelector:@selector(server:didNotEnableBonjour:)])
20.  
21. [self.delegate server:self didNotEnableBonjour:errorDict];
22.  
23. }


這是NSNetServiceDelegate協議的其中兩個方法,一個會在NSNetService發佈成功時被調用,一個會在發佈失敗時會調用。它們都有一個參數,這個參數是觸發這個回調的NSNetService自己。兩個方法的內容都很簡單。

      發佈成功方法中先判斷self的委託是否有效,若是有效的話它接着判斷這個委託是否是響應serverDidEnableBonjour:withName方法,若是響應的話,就對它的委託調用這個方法。在ios局域網聯機—蘋果官方源碼之WiTap剖析(一)的setup方法的註釋4中,咱們已經解釋過了這裏的self的委託被設置爲AppController了,因此這裏是判斷AppController是否是響應這個serverDidEnableBonjour:withName方法,若是響應就對它調用這個方法。(關於這個被調用的方法咱們在之後講回到AppController時再講)

     發佈失敗方法和這個成功方法基本上同樣,不一樣的只是對應的方法,也再也不重複說明了。

     最後是兩個輔助性的方法: 

 

01. (NSString*) description
02.  
03. {
04.  
05. return [NSString stringWithFormat:@"<%@ = 0x%08X | port %d | netService = %@>", [self class], (long)self, self.port, self.netService];
06.  
07. }
08.  
09.  
10.  
11. + (NSString*) bonjourTypeFromIdentifier:(NSString*)identifier
12. {
13.  
14.  
15.  
16. if (![identifier length])
17.  
18. return nil;   
19.  
20. return [NSString stringWithFormat:@"_%@._tcp.", identifier];
21.  
22. }


description方法是幫助咱們調試程序時方便用的,它把這個類的信息返回給咱們,方便咱們輸出到控制檯進行查看。它把這個TCPService類對象的類名,地址,port屬性,netService屬性都返回了,很是方便。

      bonjourTypeFromIdentifier:這個方法真是一個純粹的輔助方法,它在這個例子中的用途就是經過傳入在AppController.m中定義的宏kGameIdentifier,而後返回一個完整的事實上的NSNetService的初始化方法中用的網絡服務的類型。

黎明即將到來

     至此,這個TCPService類咱們介紹完了,這篇文章就到這裏,後面的內容會在後續文章中繼續講述。

     (能力有限,文中可能會有不對的地方,但願你們指教。謝謝!!)

相關文章
相關標籤/搜索