GCDAsyncSocket類庫,IOS下TCP通訊使用心得


關於在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” onPort@"60000" error:&err];

if (err != nil)

{

NSLog(@”%@”,err);

}

代碼比前面稍微長了一點,不過實質上完全不復雜,我們只是先聲明了一個錯誤信息的指針,然后使用之前創建的對象調用他的連接方法,第一個參數不難看出是一個IP,第二個參數則是一個端口,如果這里還不理解何為IP和端口的話,就先去看看在開頭就提到的我之前寫過的那邊網絡基礎篇文章吧…最后一個是出參,如果連接的過程中出現了錯誤,該方法會把這根指針指向一個具體的錯誤信息,最后我們再判斷一下之前我們創建錯誤信息的指針是否還是指向空,如果並非指向空那么代表我們連接的過程中出現了錯誤,將錯誤信息打印一下吧~不過請切記,此處的錯誤信息並非你創建連接時所有的錯誤都會在此處得到反映。

說到這里我們該說一點真正有用的了,GCDAsyncSocket具有一系列完整的委托機制,我們所做的一切處理基本都是異步處理的狀態,換句話說,連接之后是否連接成功,連接成功要執行什么懂並非應該寫在此處而應該寫在相應的委托之中,同樣的道理一樣適用於發送、讀取數據等等。也就是說我們在此處讀出的錯誤只是同步執行的代碼處理一些連接時會發生的錯誤,而更多的處理我們應在相對應的委托中進行處理。首先請看下面這個方法:

-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString Many people who are exposed to marijuana need to casino online get quick THC antioxidants due to their employer. *)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轉換成各種各樣的數據或者文件如果你還不知道該怎么做我這里實在幫不了你,因為我總不能吧多如牛毛的情況都列舉在這一篇文章中吧,要知道我每篇文章的篇幅都夠長了。。。不過也別因此而氣餒百度和谷歌肯定可以幫到你~

接下來我們再看看“郵遞員的委托”,嗯一個socket的對象,一個tag嗯,沒錯,哎哎,慢着,好像哪里不太對啊,我來看看,哪里不對呢,哦對了!發送的數據呢!怎么沒有!哎也不對。。。明明是我自己發的數據我還要來干嘛,有了工號我不就知道發送的是什么了么。那是哪里不太對呢。。。哦!是名字!我們的數據傳輸來說接受可以是讀取read而發送通常我們應該寫成Send一類的單詞,為何這里是Write?寫入?沒錯就是寫入,向TCP的通訊流之中寫入數據。TCP通訊協議是一個基於字節流的運輸層通信協議,其數據傳輸的形式也是以流的形式提現,而我感覺在使用GCDAsyncSocket的過程中我們可以很好的體會到流的概念,首先來說為什么這種TCP的這種傳輸形式要叫流而不像UDP中的那樣叫做包?流之中又寫入和讀出的概念,我們可以把整個TCP通訊的連接看作為一條無水的河流,當然因為他沒水所以你可以稱它為溝,而向其寫入數據即是向河流注入水,被寫入的數據會向水一樣流向連接的另一端。讀即是從河流中取水,只要讀得動作在繼續,並且河流之中有水,那么我們就可以不停的取到數據,不論是河流之中有水你確沒有去讀亦或者是你去讀了而河流之中沒有水都會引發看起來完全相同的反應就是沒有數據返回,所以在很多時候我們要處理更多的關於接收數據的邏輯的處理。正如我們目前使用的方法就是一種比較粗暴有效的方法——一旦開啟連接讀取的動作就永不停歇。

接下來我們還要記住使用TCP流式傳輸數據時的一個關鍵性問題,數據是不會自己分段的。沒錯,就如一次次倒入河流中的水一樣,數據也同樣會向水一樣融合為一個整體,換句話說,數據在TCP中傳輸本身是沒有起始或結尾之分,如果我先向數據流中寫入兩個人的聊天記錄,第一句是“你好”,對方回復了一句“不好”,結果發到了服務器,服務器讀取出的信息是“你好不好”,同樣類似的情況會發生很多,比我舉出的這個例子要常見的多比如我先發了一段音頻,又發了一段圖片,又發了一段文字,最后服務器接收到了一個帶語音和字母的靜態圖片。實際情況上比我說的要遭的多,因為由於字節之間並沒有邊界,所以字符、文字、音頻,我們根本無法確定他們各有多長,胡亂截取,只會導致無法編碼解析成圖片、文字及音頻,所以如何界定數據之間的邊界是你開始使用TCP協議之后又一個問題。你可以使用一個固定的字節數組組合來區分開頭以及結尾,也可以將所有的字符串都添加一個特殊的界定字符來區分不同的命令與操作。

如果看到這里的看官有心使用GCDAsyncSocket去編寫了一個服務器端,並且使用它來接受客戶端的數據,比如傳輸了一些音頻,圖片等從字節單位看來將會不小的長串數據的話就會發現,服務器端接到的程序是一段一段的,沒錯,但我沒有欺騙你,TCP協議並不會區分你發送數據的頭尾,被划分為段知識GCDAsyncSocket為了保證在並不通常的移動互聯網之中一樣可以安全的傳輸數據,於是將你所有寫入到流的數據都一分割為一段一段的內容,所以請正確理解我在上一段開頭所說的“數據是不會自己分段的。”這句話,不要較真哦親~

寫到這里,GCDAsyncSocket的基本操作及其核心思想就全部寫完了,對於思想部分皆為筆者本人個人理解,若有缺少或意見不同之處,歡迎交換意見相互學習,感謝您的閱讀。

 

 

此條目是由 水德星君 發表在 iOSMac 分類目錄的。將固定鏈接加入收藏夾。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM