上一篇亂說了一陣socket,這篇要說說怎麼幹活了。畢竟用過的起來才行。 我的項目裡面使用的是CocoaAsyncSocket,這個是對CFSocket的封裝。如果你覺得自己可以實現封裝或者直接用原生的,我可以告訴你,很累;關鍵是等你弄出來,項目可能都要交了。這個庫,支持TCP和UDP;有GCD和R
上一篇亂說了一陣socket,這篇要說說怎麼幹活了。畢竟用的起來才行。
我的項目裡面使用的是CocoaAsyncSocket,這個是對CFSocket的封裝。如果你覺得自己可以實現封裝或者直接用原生的,我可以告訴你,很累;關鍵是等你弄出來,項目可能都要交了。這個庫,支持TCP和UDP;有GCD和RunLoop兩種選擇。UDP相比TCP的話,可靠性低一點,一般用來傳輸視頻,少個一兩幀沒有什麼影響。這裡我就說一下TCP的使用,當然為了發揮Apple設備的牛X性能,我用GCD。
建立socket 單例
先說一點,要保持長連接。我們需要建一個全局的單例。標準單例模式寫法⬇️
1 + (instancetype)ShareBaseClient { 2 static SocketClient * iSocketCilent; 3 static dispatch_once_t onceToken; 4 dispatch_once(&onceToken, ^{ 5 iSocketCilent = [[SocketClient alloc]init]; 6 }); 7 return iSocketCilent; 8 }
1 #define USE_SECURE_CONNECTION 0 // 是否需要使用安全連接 2 #define USE_CFSTREAM_FOR_TLS 0 // Use old-school CFStream style technique 3 #define MANUALLY_EVALUATE_TRUST 1 // 是否需要人工驗證 4 5 6 - (id)init { 7 self = [super init]; 8 // Start the socket stuff 最後的那個是你的要放在哪個線程裡面操作 果斷全局 9 asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; 10 NSError *error = nil; 11 uint16_t port = DefalutPORT;// 預設的埠號 12 // 連接是否成功 13 // WWW_HOST 連接的地址 14 // 50秒 是我設置的 超時時間 15 if (![asyncSocket connectToHost:WWW_HOST onPort:port withTimeout:50.0f error:&error]){ 16 17 NSLog(@"Unable to connect to due to invalid configuration: %@", error); 18 19 }else{ 20 NSLog(@"Connecting to \"%@\" on port %hu...", WWW_HOST, port); 21 } 22 { 23 #if USE_SECURE_CONNECTION 24 25 #if USE_CFSTREAM_FOR_TLS 26 { 27 // Use old-school CFStream style technique 28 29 NSDictionary *options = @{ 30 GCDAsyncSocketUseCFStreamForTLS : @(YES), 31 GCDAsyncSocketSSLPeerName : CERT_HOST 32 }; 33 34 DDLogVerbose(@"Requesting StartTLS with options:\n%@", options); 35 [asyncSocket startTLS:options]; 36 } 37 #elif MANUALLY_EVALUATE_TRUST 38 { 39 // Use socket:didReceiveTrust:completionHandler: delegate method for manual trust evaluation 40 41 NSDictionary *options = @{ 42 GCDAsyncSocketManuallyEvaluateTrust : @(YES), 43 GCDAsyncSocketSSLPeerName : CERT_HOST 44 }; 45 46 DDLogVerbose(@"Requesting StartTLS with options:\n%@", options); 47 [asyncSocket startTLS:options]; 48 } 49 #else 50 { 51 // Use default trust evaluation, and provide basic security parameters 52 53 NSDictionary *options = @{ 54 GCDAsyncSocketSSLPeerName : CERT_HOST 55 }; 56 57 DDLogVerbose(@"Requesting StartTLS with options:\n%@", options); 58 [asyncSocket startTLS:options]; 59 } 60 #endif 61 #endif 62 } 63 return self; 64 }
接受報文
如果你連接成功了,那你就可以開始收發報文了。我這裡用的是delegate模式,所以我們要實現一下幾個比較重要的代理:
連接上的第一步,我們要準備讀數據。我用一個簡單的枚舉標識了每次讀取數據的區域,一般報文都是先讀報頭,再去解析正文的。所以連接上之後,要首先接受報頭。
1 #define READ_HEADER_LINE_BY_LINE 0 2 3 typedef NS_ENUM(long, ReadTagType){ //讀取數據的類型 4 ReadTagTypehead = 1,// 報頭 5 ReadTagTypebody = 2 // 主體 6 }; 7 8 - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port 9 { 10 NSLog(@"socket:didConnectToHost:%@ port:%hu", host, port); 11 12 // 是否一行一行的讀數據,我這裡設置的是 0 13 #if READ_HEADER_LINE_BY_LINE 14 // Now we tell the socket to read the first line of the http response header. 15 // As per the http protocol, we know each header line is terminated with a CRLF (carriage return, line feed). 16 [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1.0 tag:0]; 17 18 #else 19 // Now we tell the socket to read the full header for the http response. 20 // As per the http protocol, we know the header is terminated with two CRLF's (carriage return, line feed). 21 // sizeof(protocol_head) 一個報文頭的長度 22 // ReadTagTypehead 先讀的是報文的頭的 23 // -1代表沒有超時時間 24 [asyncSocket readDataToLength:sizeof(protocol_head) withTimeout:-1 tag:ReadTagTypehead]; 25 26 #endif 27 }
開始讀取後,有數據的話會進入這個代理。
1 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { 2 3 switch (tag) { 4 case ReadTagTypehead:// 先 讀取 head部分 5 { 6 // 拿到之後,我們處理一下這段頭 7 // 獲得head裡面的命令CMD 和 下一段正文的長度BodyLength 8 // 把這些參數傳到一個方法裡面處理一下(萬一是空頭呢。) 9 10 [self willReadBody:CMD size: BodyLength data:data]; 11 } 12 break; 13 case ReadTagTypebody:// 再 讀取 body部分 14 { 15 // 加上這一段,我們取得了完整的數據了,可以給需要的地方發過去了 16 [self haveReadData:data]; 17 // 然後我們去讀下一個報文,還是先讀報文頭 18 [asyncSocket readDataToLength:sizeof(protocol_head) withTimeout:-1 tag:ReadTagTypehead]; 19 } 20 break; 21 default: 22 break; 23 } 24 } 25 26 // 處理用的方法 下麵兩個是我寫的私有方法 不是CocoaAsyncSocket的代理方法 27 // 準備讀取body的數據 CMD需要記錄下來的 28 - (void)willReadBody:(int)CMD size:(long)size data:(NSData *)data{ 29 gCMD = CMD; 30 alldata = [[NSMutableData alloc]initWithData: data]; 31 if(size == 0) {// 如果是空頭,就丟掉這段數據,繼續讀下一個頭 32 [self haveReadData:nil]; 33 [asyncSocket readDataToLength:sizeof(protocol_head) withTimeout:-1 tag:ReadTagTypehead]; 34 }else {// 如果不是的話,就去讀下一段 報文的正文 35 [asyncSocket readDataToLength:dataBufferSize withTimeout:-1 tag:ReadTagTypebody]; 36 } 37 } 38 39 // 讀取完數據了 要回調代理 40 - (void)haveReadData:(NSData *)data { 41 // 把頭和正文拼接起來 構成完整的數據 42 if(data && data.length>0) 43 [alldata appendData:data]; 44 45 // NSLog(@"receiveData: %@",alldata); 46 if(alldata.length>0) { 47 // 這裡去告訴 你需要處理數據的地方 讓他去處理數據 48 } 49 alldata = nil; 50 }
到上面這一步的話,你的接受數據就算完成了。
發送報文
[asyncSocket writeData:data withTimeout:-1.0 tag:0];
就一行,不用懷疑。把你的信息轉為NSData後 調用這個方法就好了。成功的話,會進入這個代理。
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag;
socket 斷開
當然,socket也有斷開的時候,所以你需要處理這個代理。這裡面你可以發個通知,讓你的application斷線重連一下。PS:移動網路和Wi-Fi切換,socket會斷開哦。
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
附加:
當我們連續收到錯誤報文的時候,我們需要主動斷開socket。
// 先去掉代理 再斷開連接 self.asyncSocket.delegate = nil; [self.asyncSocket disconnect];