標籤:ios gcdasyncsocket tcp socket
關於在IOS下使用Socket進行通訊的技術文章也確保很久了,今日又是一個還債的日子,網上雖然很多介紹過AsyncSocket或GCDAsyncSocket的文章,但其實就那麼一兩篇大部分都是轉載,於是我義正言辭、慷慨激昂的批判他們這種不負責任的態度,學習,不是給自己學的,是要和大家分享的。技術的共用有利於整體行業的進步,也可以使自身更深入全面的瞭解。
之前的文章中我們講到過TCP通訊協議,並且也對其進行了較為詳細的介紹和描述,關於TCP通訊的原理此處我們不再贅述,如有需要的看官可自行翻閱本人所寫的《IOS、安卓IM語音交談開發初探部分心得——網路基礎篇》一文。
正如名稱一樣GCDAsyncSocket開源類庫是以蘋果的GCD多任務處理機制完成的一個非同步互動通訊端通訊。使用方法其實並不複雜,主要說的是在使用這個類庫的時候我的一些心得和理解,若有不妥之處望看官指點。首先,每一個GCDAsyncSocket對象(以下簡稱GCDSocket對象)都可以理解為一個socket通訊端,我們的操作都是針對於這個socket執行的各種命令,可以開啟一個連接埠偵聽,同樣也可以串連其他電腦的連接埠進行資料通訊等等等等。首先我們來建立一個socket。當然這之前先要將CGDAsyncSocket的.h檔案及.m檔案加入到我們的項目,並且在需要使用socket串連的地方將.h標頭檔包含,這些廢話我覺得不需要複述了應該(那你還嘚吧嘚的說半天幹嘛啊喂!)。具體代碼如下
GCDAsyncSocket socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
代碼並不複雜,我們只需要給出一個委派物件也就是第一個參數中的self,以及一個委託啟動並執行GCD隊列即可建立一個GCDAsyncSocket,當前代碼中我們是使用靜態全域函數取得的主訊息佇列。當然也可以使用其他方法獲得其他的GCD隊列,比如:dispatch_get_global_queue().
建立了Socket對象我們即可以立即為,當前我們的socket已經進入程式以供操作。但如果你想和伺服器進行通訊,那麼我們還需要和伺服器進行串連。可能有的使用習慣了http協議的人會問,初始化函數中我們為何不直接指定伺服器以及連接埠號碼?其實這些肯定都是需要的,但是你要理解到,你的socket對象功能不只是可以用來串連伺服器,換而言之我們的socket對象一樣可以偵聽某連接埠來等待他人串連,所以在通過通訊端編程使用TCP協議的時候是我們從http協議過度到TCP協議的一個轉變(雖然本文並不會教你如何在IOS上構架伺服器。),但並不是第一個,第一個轉變是要記得,我們要使用的是協議,並非某個類,所以我上述說明中都是說從http協議過度到TCP而不是跟大家說現在我們將從NSURLRequest和NSURLConnection過度到GCDAsyncSocket。
好了接下來我們看看如何串連伺服器。原始碼如下:
NSError *err;
[socket connectToHost:@“192.168.10.111” [email protected]"60000" error:&err];
if (err != nil)
{
NSLog(@”%@”,err);
}
代碼比前面稍微長了一點,不過實質上完全不複雜,我們只是先聲明了一個錯誤資訊的指標,然後使用之前建立的對象調用他的串連方法,第一個參數不難看出是一個IP,第二個參數則是一個連接埠,如果這裡還不理解何為IP和連接埠的話,就先去看看在開頭就提到的我之前寫過的那邊網路基礎篇文章吧…最後一個是出參,如果串連的過程中出現了錯誤,該方法會把這根指標指向一個具體的錯誤資訊,最後我們再判斷一下之前我們建立錯誤資訊的指標是否還是指向空,如果並非指向空那麼代表我們串連的過程中出現了錯誤,將錯誤資訊列印一下吧~不過請切記,此處的錯誤資訊並非你建立串連時所有的錯誤都會在此處得到反映。
說到這裡我們該說一點真正有用的了,GCDAsyncSocket具有一系列完整的委託機制,我們所做的一切處理基本都是非同步處理的狀態,換句話說,串連之後是否串連成功,串連成功要執行什麼懂並非應該寫在此處而應該寫在相應的委託之中,同樣的道理一樣適用於發送、讀取資料等等。也就是說我們在此處讀出的錯誤只是同步執行的代碼處理一些串連時會發生的錯誤,而更多的處理我們應在相對應的委託中進行處理。首先請看下面這個方法:
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
這個方法就是在成功串連伺服器之後的委託方法。關於委託該如何使用我在此處就不贅述了和本文的關係實在不大,不過給諸位看管一個建議,也是我才剛剛糾正的一個編碼錯誤習慣,之前碰到所有委託的地方我都會將直接將當前的類對象設定成委託處理對象,並且遵循委託協議擴充代碼,這麼做的壞處顯而易見,顯示層與邏輯層的混淆是一方面,另一方面是一旦需要使用過多的委託,將造成大量不必要的代碼都堆積在一個類中,並且我們很容易直接在委託方法中直接使用一些類內成員屬性或者甚至是私人成員,而實際上這種做法是很不好的,因為這種最發會使得邏輯出現混亂,處理委託應當是單獨處於背景邏輯,如果需要一些必要的資料傳遞也應該採取屬性偵聽、甚至是通知等方式來實現而並非直接在顯示層中編寫邏輯代碼來實現。使得代碼耦合性大增的同時也使得很多時候在切換操作對象時對委派物件的處理變得複雜,甚至可能完全相同的代碼要難免的複製粘貼。所以我給大家的建議是單獨編寫一個委託類,在每個類中設定一個該類類型的成員指標,將委託設定到專門的委派物件上去處理,這樣不僅效率更高,代碼可讀性更強,更便於維護,同時也更符合物件導向的編程思想。
回到對GCDAsyncSocket使用的講解上來,在這個委託方法中,我們可以取到一個socket對象一個伺服器IP和一個連接埠號碼,你可以處理一切在串連建立之後應該馬上執行的事情,比如與伺服器進行通訊確認串連端以免出現其他人通過IP及連接埠隨意的和你的伺服器通訊,再比如開啟心跳包的發送,讓伺服器一直可以確認你的存在。不管做什麼,都是你和伺服器的編寫者事前約定好的,就像資料轉送格式什麼的,如果沒有當面約定我堅信他也一定要給你出個文檔什麼的,否則你的工作接下來將舉步維艱。但是不管你要在此處都做什麼工作,都要處理哪些事宜,請務必記得,在此處你必須要在函數的最後加上一句:
[socket readDataWithTimeout:-1 tag:0];
這是什嗎?別慌,按照你看到這個函數的第一反應取理解,沒錯他就是讀取資料的方法,兩個參數也略顯簡單,一個逾時時間,如果你設定成-1則認為永不逾時,而第二參數則是區別該次讀取與其他讀取的標誌,通常我們在設計檢視上的控制項時也會有這樣的一個屬性就是tag。如果你做過web開發,那你應該知道Http標籤上的id,如果你做過一些案頭級開發,你的控制項或許有個id或者是index再或者是tag的屬性來區別這些控制項,沒錯此tag和彼tag功效基本一樣。
我們可以這樣理解,socket在開啟之前是一個巨大門,開啟這道門之後(也就是串連之後)就是一個寬敞的通道,通過這條通道所達到的地點就是我們串連的目標伺服器,或者是串連過來的用戶端,兩面都是一樣的。我們現在不論是發送資料還是讀取資料都是往返於這個大門之中的一個個門衛與郵遞員,我們可以把讀取資料的方法看作是門衛,而發送資料的人看做是郵遞員,沒錯伺服器與用戶端都一樣,我們都會派出一個個郵遞員去我們串連的另一端送信,但是如果你沒有命令你的門衛去吧門口郵箱中的信拿過來,那麼你的郵遞員就會假裝看不見郵遞員,然後呼呼睡大覺,好吧看起來這些門衛實在沒什麼責任心不是麼,其實他們也是有苦衷的,因為這是最初設計者給他們的命令,不接到命令絕對不要出門,萬一收到的是金剛葫蘆娃高清全集的種子怎麼辦!好的就這樣,為了避免我們的郵件不被錯過,所以建立串連之後就讓一個門衛跑去門口等著吧~慢著,萬一我需要派出很多個門衛我分不清他們該怎麼辦,其實他們已經被你分配了工號,這個工號就是tag。
現在我們的串連動作算是完整的做完了,接下來我們要做的就只有兩件事,第一個在需要發送資料的時候派出郵遞員,以及當門衛接到訊息的時候在我們的手機端上根據門衛的訊息做出反應。等等,好像少了點什麼,沒錯 少點委託,我們來看一下讀取和寫入的委託,讀取的委託即是門衛接到資訊的報告,寫入的委託就是郵遞員將郵件送完的回複:
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
好的讓我們來看看這些委託中我們都能得到什麼,首先是讀取的委託,是一個socket對象,一個讀取到的資料以及一個“門衛的工號”,嗯,大概也就這些,我們還能要什麼的,沒錯這些足夠了,別抱怨第二個參數的資料類型,要知道其實最開始接到資料的時候只是位元組數組啊,已經給你轉換成NSData對象了你就要學會感恩啊,誰讓你要用通訊端傳輸了,這就是活該的,所以改怎麼讀取轉換解析這些資料你需要好好的和伺服器編寫者溝通。除此之外你還要詳細的瞭解如何將NSData轉換成各種各樣的資料或者檔案如果你還不知道該怎麼做我這裡實在幫不了你,因為我總不能吧多如牛毛的情況都列舉在這一篇文章中吧,要知道我每篇文章的篇幅都夠長了。。。不過也別因此而氣餒百度和Google肯定可以幫到你~
接下來我們再看看“郵遞員的委託”,嗯一個socket的對象,一個tag嗯,沒錯,哎哎,慢著,好像哪裡不太對啊,我來看看,哪裡不對呢,哦對了!發送的資料呢!怎麼沒有!哎也不對。。。明明是我自己發的資料我還要來幹嘛,有了工號我不就知道發送的是什麼了麼。那是哪裡不太對呢。。。哦!是名字!我們的資料轉送來說接受可以是讀取read而發送通常我們應該寫成Send一類的單詞,為何這裡是Write?寫入?沒錯就是寫入,向TCP的通訊流之中寫入資料。TCP通訊協議是一個基於位元組流的運輸層通訊協定,其資料轉送的形式也是以流的形式提現,而我感覺在使用GCDAsyncSocket的過程中我們可以很好的體會到流的概念,首先來說為什麼這種TCP的這種傳輸形式要叫流而不像UDP中的那樣叫做包?流之中又寫入和讀出的概念,我們可以把整個TCP通訊的串連看作為一條無水的河流,當然因為他沒水所以你可以稱它為溝,而向其寫入資料即是向河流注入水,被寫入的資料會向水一樣流向串連的另一端。讀即是從河流中取水,只要讀得動作在繼續,並且河流之中有水,那麼我們就可以不停的取到資料,不論是河流之中有水你確沒有去讀亦或者是你去讀了而河流之中沒有水都會引發看起來完全相同的反應就是沒有資料返回,所以在很多時候我們要處理更多的關於接收資料的邏輯的處理。正如我們目前使用的方法就是一種比較粗暴有效方法——一旦開啟串連讀取的動作就永不停歇。
接下來我們還要記住使用TCP串流資料時的一個關鍵性問題,資料是不會自己分段的。沒錯,就如一次次倒入河流中的水一樣,資料也同樣會向水一樣融合為一個整體,換句話說,資料在TCP中傳輸本身是沒有起始或結尾之分,如果我先向資料流中寫入兩個人的聊天記錄,第一句是“你好”,對方回複了一句“不好”,結果發到了伺服器,伺服器讀取出的資訊是“你好不好”,同樣類似的情況會發生很多,比我舉出的這個例子要常見的多比如我先發了一段音頻,又發了一段圖片,又發了一段文字,最後伺服器接收到了一個帶語音和字母的靜態圖片。實際情況上比我說的要遭的多,因為由於位元組之間並沒有邊界,所以字元、文字、音頻,我們根本無法確定他們各有多長,胡亂截取,只會導致無法編碼解析成圖片、文字及音頻,所以如何界定資料之間的邊界是你開始使用TCP協議之後又一個問題。你可以使用一個固定的位元組數組組合來區分開頭以及結尾,也可以將所有的字串都添加一個特殊的界定字元來區分不同的命令與操作。
如果看到這裡的看官有心使用GCDAsyncSocket去編寫了一個伺服器端,並且使用它來接受用戶端的資料,比如傳輸了一些音頻,圖片等從位元組單位看來將會不小的長串資料的話就會發現,伺服器端接到的程式是一段一段的,沒錯,但我沒有欺騙你,TCP協議並不會區分你發送資料的頭尾,被劃分為段知識GCDAsyncSocket為了保證在並不通常的移動互連網之中一樣可以安全的傳輸資料,於是將你所有寫入到流的資料都一分割為一段一段的內容,所以請正確理解我在上一段開頭所說的“資料是不會自己分段的。”這句話,不要較真哦親~