申明:本文內容屬於轉載整理,原文鏈接html
CocoaAsyncSocket是谷歌的開發者,基於BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易於使用的、強大的異步套接字庫,向上封裝出簡單易用OC接口。省去了咱們面Socket向Socket以及數據流Stream等繁瑣複雜的編程。git
本文爲一個系列,旨在讓你們瞭解CocoaAsyncSocket是如何基於底層進行封裝、工做的。github
注:文中涉及代碼比較多,建議你們結合源碼一塊兒閱讀比較容易能加深理解。這裏有樓主標註好註釋的源碼,有須要的能夠做爲參照:CocoaAsyncSocket源碼註釋編程
若是對該框架用法不熟悉的話,能夠參考樓主以前這篇文章:iOS即時通信,從入門到「放棄」?,或者自行查閱。數組
整個庫就這麼兩個類,一個基於TCP,一個基於UDP。其中基於TCP的GCDAsyncSocket,大概8000多行代碼。而GCDAsyncUdpSocket稍微少一點,也有5000多行。
因此單純從代碼量上來看,這個庫仍是作了不少事的。緩存
順便提一下,以前這個框架還有一個runloop版的,不過由於功能重疊和其它種種緣由,後續版本便廢棄了,如今僅有GCD
版本。安全
本系列咱們將重點來說GCDAsyncSocket這個類。服務器
1 @implementation GCDAsyncSocket 2 { 3 //flags,當前正在作操做的標識符 4 uint32_t flags; 5 uint16_t config; 6 7 //代理 8 __weak id<GCDAsyncSocketDelegate> delegate; 9 //代理回調的queue 10 dispatch_queue_t delegateQueue; 11 12 //本地IPV4Socket 13 int socket4FD; 14 //本地IPV6Socket 15 int socket6FD; 16 //unix域的套接字 17 int socketUN; 18 //unix域 服務端 url 19 NSURL *socketUrl; 20 //狀態Index 21 int stateIndex; 22 23 //本機的IPV4地址 24 NSData * connectInterface4; 25 //本機的IPV6地址 26 NSData * connectInterface6; 27 //本機unix域地址 28 NSData * connectInterfaceUN; 29 30 //這個類的對Socket的操做都在這個queue中,串行 31 dispatch_queue_t socketQueue; 32 33 dispatch_source_t accept4Source; 34 dispatch_source_t accept6Source; 35 dispatch_source_t acceptUNSource; 36 37 //鏈接timer,GCD定時器 38 dispatch_source_t connectTimer; 39 dispatch_source_t readSource; 40 dispatch_source_t writeSource; 41 dispatch_source_t readTimer; 42 dispatch_source_t writeTimer; 43 44 //讀寫數據包數組 相似queue,最大限制爲5個包 45 NSMutableArray *readQueue; 46 NSMutableArray *writeQueue; 47 48 //當前正在讀寫數據包 49 GCDAsyncReadPacket *currentRead; 50 GCDAsyncWritePacket *currentWrite; 51 //當前socket未獲取完的數據大小 52 unsigned long socketFDBytesAvailable; 53 54 //全局公用的提早緩衝區 55 GCDAsyncSocketPreBuffer *preBuffer; 56 57 #if TARGET_OS_IPHONE 58 CFStreamClientContext streamContext; 59 //讀的數據流 60 CFReadStreamRef readStream; 61 //寫的數據流 62 CFWriteStreamRef writeStream; 63 #endif 64 //SSL上下文,用來作SSL認證 65 SSLContextRef sslContext; 66 67 //全局公用的SSL的提早緩衝區 68 GCDAsyncSocketPreBuffer *sslPreBuffer; 69 size_t sslWriteCachedLength; 70 71 //記錄SSL讀取數據錯誤 72 OSStatus sslErrCode; 73 //記錄SSL握手的錯誤 74 OSStatus lastSSLHandshakeError; 75 76 //socket隊列的標識key 77 void *IsOnSocketQueueOrTargetQueueKey; 78 79 id userData; 80 81 //鏈接備選服務端地址的延時 (另外一個IPV4或IPV6) 82 NSTimeInterval alternateAddressDelay; 83 }
這個裏定義了一些屬性,能夠先簡單看看註釋,這裏咱們僅僅先暫時列出來,給你們混個眼熟。
在接下來的代碼中,會大量穿插着這些屬性的使用。因此你們不用以爲困惑,具體做用,咱們後面會一一講清楚的。網絡
1 //層級調用 2 - (id)init 3 { 4 return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; 5 } 6 7 - (id)initWithSocketQueue:(dispatch_queue_t)sq 8 { 9 return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; 10 } 11 12 - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq 13 { 14 return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; 15 } 16 17 - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq 18 { 19 if((self = [super init])) 20 { 21 delegate = aDelegate; 22 delegateQueue = dq; 23 24 //這個宏是在sdk6.0以後纔有的,若是是以前的,則OS_OBJECT_USE_OBJC爲0,!0即執行if語句 25 //對6.0的適配,若是是6.0如下,則去retain release,6.0以後ARC也管理了GCD 26 #if !OS_OBJECT_USE_OBJC 27 28 if (dq) dispatch_retain(dq); 29 #endif 30 31 //建立socket,先都置爲 -1 32 //本機的ipv4 33 socket4FD = SOCKET_NULL; 34 //ipv6 35 socket6FD = SOCKET_NULL; 36 //應該是UnixSocket 37 socketUN = SOCKET_NULL; 38 //url 39 socketUrl = nil; 40 //狀態 41 stateIndex = 0; 42 43 if (sq) 44 { 45 //若是scoketQueue是global的,則報錯。斷言必需要一個非並行queue。 46 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), 47 @"The given socketQueue parameter must not be a concurrent queue."); 48 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), 49 @"The given socketQueue parameter must not be a concurrent queue."); 50 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 51 @"The given socketQueue parameter must not be a concurrent queue."); 52 //拿到scoketQueue 53 socketQueue = sq; 54 //iOS6之下retain 55 #if !OS_OBJECT_USE_OBJC 56 dispatch_retain(sq); 57 #endif 58 } 59 else 60 { 61 //沒有的話建立一個, 名字爲:GCDAsyncSocket,串行 62 socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); 63 } 64 65 // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. 66 // From the documentation: 67 // 68 // > Keys are only compared as pointers and are never dereferenced. 69 // > Thus, you can use a pointer to a static variable for a specific subsystem or 70 // > any other value that allows you to identify the value uniquely. 71 // 72 // We're just going to use the memory address of an ivar. 73 // Specifically an ivar that is explicitly named for our purpose to make the code more readable. 74 // 75 // However, it feels tedious (and less readable) to include the "&" all the time: 76 // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) 77 // 78 // So we're going to make it so it doesn't matter if we use the '&' or not, 79 // by assigning the value of the ivar to the address of the ivar. 80 // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; 81 82 83 //好比原來爲 0X123 -> NULL 變成 0X222->0X123->NULL 84 //本身的指針等於本身原來的指針,成二級指針了 看了註釋是爲了之後省略&,讓代碼更可讀? 85 IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; 86 87 88 void *nonNullUnusedPointer = (__bridge void *)self; 89 90 //dispatch_queue_set_specific給當前隊里加一個標識 dispatch_get_specific當前線程取出這個標識,判斷是否是在這個隊列 91 //這個key的值其實就是一個一級指針的地址 ,第三個參數把本身傳過去了,上下文對象?第4個參數,爲銷燬的時候用的,能夠指定一個函數 92 dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); 93 //讀的數組 限制爲5 94 readQueue = [[NSMutableArray alloc] initWithCapacity:5]; 95 currentRead = nil; 96 97 //寫的數組,限制5 98 writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; 99 currentWrite = nil; 100 101 //設置大小爲 4kb 102 preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; 103 104 #pragma mark alternateAddressDelay?? 105 //交替地址延時?? wtf 106 alternateAddressDelay = 0.3; 107 } 108 return self; 109 }
詳細的細節能夠看看註釋,這裏初始化了一些屬性:併發
1.代理、以及代理queue的賦值。
2.本機socket的初始化:包括下面3種
//本機的ipv4 socket4FD = SOCKET_NULL; //ipv6 socket6FD = SOCKET_NULL; //UnixSocket socketUN = SOCKET_NULL;
其中值得一提的是第三種:UnixSocket,這個是用於Unix Domin Socket通訊用的。
那麼什麼是Unix Domin Socket呢?
原來它是在socket的框架上發展出一種IPC(進程間通訊)機制,雖然網絡socket也可用於同一臺主機的進程間通信(經過loopback地址127.0.0.1),可是UNIX Domain Socket用於IPC 更有效率 :
基本上它是當今應用於IPC最主流的方式。至於它到底和普通的socket通訊實現起來有什麼區別,彆着急,咱們接着往下看。
3.生成了一個socketQueue,這個queue是串行的,接下來咱們看代碼就會知道它貫穿於這個類的全部地方。全部對socket以及一些內部數據的相關操做,都須要在這個串行queue中進行。這樣使得整個類沒有加一個鎖,就保證了整個類的線程安全。
4.建立了兩個讀寫隊列(本質數組),接下來咱們全部的讀寫任務,都會先追加在這個隊列最後,而後每次取出隊列中最前面的任務,進行處理。
5.建立了一個全局的數據緩衝區:preBuffer,咱們所操做的數據,大部分都是要先存入這個preBuffer中,而後再從preBuffer取出進行處理的。
6.初始化了一個交替延時變量:alternateAddressDelay,這個變量先簡單的理解下:就是進行另外一個服務端地址請求的延時。後面咱們一講到,你們就明白了。
初始化方法就到此爲止了。
接着咱們有socket了,咱們若是是客戶端,就須要去connet服務器。
又或者咱們是服務端的話,就須要去bind端口,而且accept,等待客戶端的鏈接。(基本上也沒有用iOS來作服務端的吧...)
connect
: 其中和connect相關的方法就這麼多,咱們通常這麼來鏈接到服務端:
[socket connectToHost:Khost onPort:Kport error:nil];
也就是咱們在截圖中選中的方法,那咱們就從這個方法做爲起點,開始講起吧。
1 /逐級調用 2 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr 3 { 4 return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; 5 } 6 7 - (BOOL)connectToHost:(NSString *)host 8 onPort:(uint16_t)port 9 withTimeout:(NSTimeInterval)timeout 10 error:(NSError **)errPtr 11 { 12 return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; 13 } 14 15 //多一個inInterface,本機地址 16 - (BOOL)connectToHost:(NSString *)inHost 17 onPort:(uint16_t)port 18 viaInterface:(NSString *)inInterface 19 withTimeout:(NSTimeInterval)timeout 20 error:(NSError **)errPtr 21 { 22 //{} 跟蹤當前行爲 23 LogTrace(); 24 25 // Just in case immutable objects were passed 26 //拿到host ,copy防止值被修改 27 NSString *host = [inHost copy]; 28 //interface?接口? 29 NSString *interface = [inInterface copy]; 30 31 //聲明兩個__block的 32 __block BOOL result = NO; 33 //error信息 34 __block NSError *preConnectErr = nil; 35 36 //gcdBlock ,都包裹在自動釋放池中 37 dispatch_block_t block = ^{ @autoreleasepool { 38 39 // Check for problems with host parameter 40 41 if ([host length] == 0) 42 { 43 NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; 44 preConnectErr = [self badParamError:msg]; 45 46 //其實就是return,大牛的代碼真是充滿逼格 47 return_from_block; 48 } 49 50 // Run through standard pre-connect checks 51 //一個前置的檢查,若是沒經過返回,這個檢查裏,若是interface有值,則會將本機的IPV4 IPV6的 address設置上。 52 if (![self preConnectWithInterface:interface error:&preConnectErr]) 53 { 54 return_from_block; 55 } 56 57 // We've made it past all the checks. 58 // It's time to start the connection process. 59 //flags 作或等運算。 flags標識爲開始Socket鏈接 60 flags |= kSocketStarted; 61 62 //又是一個{}? 只是爲了標記麼? 63 LogVerbose(@"Dispatching DNS lookup..."); 64 65 // It's possible that the given host parameter is actually a NSMutableString. 66 //極可能給咱們的服務端的參數是一個可變字符串 67 // So we want to copy it now, within this block that will be executed synchronously. 68 //因此咱們須要copy,在Block裏同步的執行 69 // This way the asynchronous lookup block below doesn't have to worry about it changing. 70 //這種基於Block的異步查找,不須要擔憂它被改變 71 72 //copy,防止改變 73 NSString *hostCpy = [host copy]; 74 75 //拿到狀態 76 int aStateIndex = stateIndex; 77 __weak GCDAsyncSocket *weakSelf = self; 78 79 //全局Queue 80 dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 81 //異步執行 82 dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { 83 //忽視循環引用 84 #pragma clang diagnostic push 85 #pragma clang diagnostic warning "-Wimplicit-retain-self" 86 87 //查找錯誤 88 NSError *lookupErr = nil; 89 //server地址數組(包含IPV4 IPV6的地址 sockaddr_in六、sockaddr_in類型) 90 NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; 91 92 //strongSelf 93 __strong GCDAsyncSocket *strongSelf = weakSelf; 94 95 //完整Block安全形態,在加個if 96 if (strongSelf == nil) return_from_block; 97 98 //若是有錯 99 if (lookupErr) 100 { 101 //用cocketQueue 102 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { 103 //一些錯誤處理,清空一些數據等等 104 [strongSelf lookup:aStateIndex didFail:lookupErr]; 105 }}); 106 } 107 //正常 108 else 109 { 110 111 NSData *address4 = nil; 112 NSData *address6 = nil; 113 //遍歷地址數組 114 for (NSData *address in addresses) 115 { 116 //判斷address4不爲空,且address爲IPV4 117 if (!address4 && [[self class] isIPv4Address:address]) 118 { 119 address4 = address; 120 } 121 //判斷address6不爲空,且address爲IPV6 122 else if (!address6 && [[self class] isIPv6Address:address]) 123 { 124 address6 = address; 125 } 126 } 127 //異步去發起鏈接 128 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { 129 130 [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; 131 }}); 132 } 133 134 #pragma clang diagnostic pop 135 }}); 136 137 138 //開啓鏈接超時 139 [self startConnectTimeout:timeout]; 140 141 result = YES; 142 }}; 143 //在socketQueue中執行這個Block 144 if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) 145 block(); 146 //不然同步的調起這個queue去執行 147 else 148 dispatch_sync(socketQueue, block); 149 150 //若是有錯誤,賦值錯誤 151 if (errPtr) *errPtr = preConnectErr; 152 //把鏈接是否成功的result返回 153 return result; 154 }
這個方法很是長,它主要作了如下幾件事:
首先咱們須要說一下的是,整個類大量的會出現LogTrace()
相似這樣的宏,咱們點進去發現它的本質只是一個{},什麼事都沒作。
原來這些宏是爲了追蹤當前執行的流程用的,它被定義在一個大的#if #else
中:
#ifndef GCDAsyncSocketLoggingEnabled #define GCDAsyncSocketLoggingEnabled 0 #endif #if GCDAsyncSocketLoggingEnabled // Logging Enabled - See log level below // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack // // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" #define LogAsync YES #define LogContext GCDAsyncSocketLoggingContext #define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) #define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) #ifndef GCDAsyncSocketLogLevel #define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE #endif // Log levels : off, error, warn, info, verbose static const int logLevel = GCDAsyncSocketLogLevel; #else // Logging Disabled #define LogError(frmt, ...) {} #define LogWarn(frmt, ...) {} #define LogInfo(frmt, ...) {} #define LogVerbose(frmt, ...) {} #define LogCError(frmt, ...) {} #define LogCWarn(frmt, ...) {} #define LogCInfo(frmt, ...) {} #define LogCVerbose(frmt, ...) {} #define LogTrace() {} #define LogCTrace(frmt, ...) {} #endif
而此時由於GCDAsyncSocketLoggingEnabled默認爲0,因此僅僅是一個{}。當標記爲1時,這些宏就能夠用來輸出咱們當前的業務流程,極大的方便了咱們的調試過程。
Block
,全部的鏈接操做都被包裹在這個Block
中。咱們作了以下判斷://在socketQueue中執行這個Block if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); //不然同步的調起這個queue去執行 else dispatch_sync(socketQueue, block);
保證這個鏈接操做必定是在咱們的socketQueue中,並且仍是以串行同步的形式去執行,規避了線程安全的問題。
接着把Block中鏈接過程產生的錯誤進行賦值,而且把鏈接的結果返回出去
//若是有錯誤,賦值錯誤 if (errPtr) *errPtr = preConnectErr; //把鏈接是否成功的result返回 return result;
接着來看這個方法聲明的Block內部,也就是進行鏈接的真正主題操做,這個鏈接過程將會調用許多函數,一環扣一環,我會盡量用最清晰、詳盡的語言來描述...
1 if ([host length] == 0) 2 { 3 NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; 4 preConnectErr = [self badParamError:msg]; 5 6 //其實就是return,大牛的代碼真是充滿逼格 7 return_from_block; 8 } 9 //用該字符串生成一個錯誤,錯誤的域名,錯誤的參數 10 - (NSError *)badParamError:(NSString *)errMsg 11 { 12 NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; 13 14 return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; 15 }
if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; }
這個檢查方法,若是沒經過返回NO。而且若是interface有值,則會將本機的IPV4 IPV6的 address設置上。即咱們以前提到的這兩個屬性:
//本機的IPV4地址 NSData * connectInterface4; //本機的IPV6地址 NSData * connectInterface6;
咱們來看看這個前置檢查方法:
1 //在鏈接以前的接口檢查,通常咱們傳nil interface本機的IP 端口等等 2 - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr 3 { 4 //先斷言,若是當前的queue不是初始化quueue,直接報錯 5 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 6 7 //無代理 8 if (delegate == nil) // Must have delegate set 9 { 10 if (errPtr) 11 { 12 NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; 13 *errPtr = [self badConfigError:msg]; 14 } 15 return NO; 16 } 17 //沒有代理queue 18 if (delegateQueue == NULL) // Must have delegate queue set 19 { 20 if (errPtr) 21 { 22 NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; 23 *errPtr = [self badConfigError:msg]; 24 } 25 return NO; 26 } 27 28 //當前不是非鏈接狀態 29 if (![self isDisconnected]) // Must be disconnected 30 { 31 if (errPtr) 32 { 33 NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; 34 *errPtr = [self badConfigError:msg]; 35 } 36 return NO; 37 } 38 39 //判斷是否支持IPV4 IPV6 &位與運算,由於枚舉是用 左位移<<運算定義的,因此能夠用來判斷 config包不包含某個枚舉。由於一個值可能包含好幾個枚舉值,因此這時候不能用==來判斷,只能用&來判斷 40 BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; 41 BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; 42 43 //是否都不支持 44 if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled 45 { 46 if (errPtr) 47 { 48 NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; 49 *errPtr = [self badConfigError:msg]; 50 } 51 return NO; 52 } 53 54 //若是有interface,本機地址 55 if (interface) 56 { 57 NSMutableData *interface4 = nil; 58 NSMutableData *interface6 = nil; 59 60 //獲得本機的IPV4 IPV6地址 61 [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; 62 63 //若是二者都爲nil 64 if ((interface4 == nil) && (interface6 == nil)) 65 { 66 if (errPtr) 67 { 68 NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; 69 *errPtr = [self badParamError:msg]; 70 } 71 return NO; 72 } 73 74 if (isIPv4Disabled && (interface6 == nil)) 75 { 76 if (errPtr) 77 { 78 NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; 79 *errPtr = [self badParamError:msg]; 80 } 81 return NO; 82 } 83 84 if (isIPv6Disabled && (interface4 == nil)) 85 { 86 if (errPtr) 87 { 88 NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; 89 *errPtr = [self badParamError:msg]; 90 } 91 return NO; 92 } 93 //若是都沒問題,則賦值 94 connectInterface4 = interface4; 95 connectInterface6 = interface6; 96 } 97 98 // Clear queues (spurious read/write requests post disconnect) 99 //清除queue(假的讀寫請求 ,提交斷開鏈接) 100 //讀寫Queue清除 101 [readQueue removeAllObjects]; 102 [writeQueue removeAllObjects]; 103 104 return YES; 105 }
又是很是長的一個方法,可是這個方法仍是很是好讀的。
在這裏若是咱們interface這個參數不爲空話,咱們會額外多執行一些操做。
首先來說講這個參數是什麼,簡單來說,這個就是咱們設置的本機IP+端口號。照理來講咱們是不須要去設置這個參數的,默認的爲localhost(127.0.0.1)本機地址。而端口號會在本機中取一個空閒可用的端口。
而咱們一旦設置了這個參數,就會強制本地IP和端口爲咱們指定的。其實這樣設置反而很差,其實你們也能想明白,這裏端口號若是咱們寫死,萬一被其餘進程給佔用了。那麼確定是沒法鏈接成功的。
因此就有了咱們作IM的時候,通常是不會去指定客戶端bind某一個端口。而是用系統自動去選擇。
咱們最後清空了當前讀寫queue中,全部的任務。
至於有interface,咱們所作的額外操做是什麼呢,咱們接下來看看這個方法:
1 - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr 2 address6:(NSMutableData **)interfaceAddr6Ptr 3 fromDescription:(NSString *)interfaceDescription 4 port:(uint16_t)port 5 { 6 NSMutableData *addr4 = nil; 7 NSMutableData *addr6 = nil; 8 9 NSString *interface = nil; 10 11 //先用:分割 12 NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; 13 if ([components count] > 0) 14 { 15 NSString *temp = [components objectAtIndex:0]; 16 if ([temp length] > 0) 17 { 18 interface = temp; 19 } 20 } 21 if ([components count] > 1 && port == 0) 22 { 23 //拿到port strtol函數,將一個字符串,根據base參數轉成長整型,如base值爲10則採用10進制,若base值爲16則採用16進制 24 long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); 25 //UINT16_MAX,65535最大端口號 26 if (portL > 0 && portL <= UINT16_MAX) 27 { 28 port = (uint16_t)portL; 29 } 30 } 31 32 //爲空則本身建立一個 0x00000000 ,全是0 ,爲線路地址 33 //若是端口爲0 一般用於分析操做系統。這一方法可以工做是由於在一些系統中「0」是無效端口,當你試圖使用一般的閉合端口鏈接它時將產生不一樣的結果。一種典型的掃描,使用IP地址爲0.0.0.0,設置ACK位並在以太網層廣播。 34 if (interface == nil) 35 { 36 37 struct sockaddr_in sockaddr4; 38 39 //memset做用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操做的一種最快方法 40 41 //memset(void *s,int ch,size_t n);函數,第一個參數爲指針地址,第二個爲設置值,第三個爲連續設置的長度(大小) 42 memset(&sockaddr4, 0, sizeof(sockaddr4)); 43 //結構體長度 44 sockaddr4.sin_len = sizeof(sockaddr4); 45 //addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。 46 sockaddr4.sin_family = AF_INET; 47 //端口號 htons將主機字節順序轉換成網絡字節順序 16位 48 sockaddr4.sin_port = htons(port); 49 //htonl ,將INADDR_ANY:0.0.0.0,不肯定地址,或者任意地址 htonl 32位。 也是轉爲網絡字節序 50 51 //ipv4 32位 4個字節 INADDR_ANY,0x00000000 (16進制,一個0表明4位,8個0就是32位) = 4個字節的 52 sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); 53 struct sockaddr_in6 sockaddr6; 54 memset(&sockaddr6, 0, sizeof(sockaddr6)); 55 56 sockaddr6.sin6_len = sizeof(sockaddr6); 57 //ipv6 58 sockaddr6.sin6_family = AF_INET6; 59 //port 60 sockaddr6.sin6_port = htons(port); 61 62 //共128位 63 sockaddr6.sin6_addr = in6addr_any; 64 65 //把這兩個結構體轉成data 66 addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; 67 addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; 68 } 69 //若是localhost、loopback 迴環地址,虛擬地址,路由器工做它就存在。通常用來標識路由器 70 //這兩種的話就賦值爲127.0.0.1,端口爲port 71 else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) 72 { 73 // LOOPBACK address 74 75 //ipv4 76 struct sockaddr_in sockaddr4; 77 memset(&sockaddr4, 0, sizeof(sockaddr4)); 78 79 sockaddr4.sin_len = sizeof(sockaddr4); 80 sockaddr4.sin_family = AF_INET; 81 sockaddr4.sin_port = htons(port); 82 83 //#define INADDR_LOOPBACK (u_int32_t)0x7f000001 84 //7f000001->1111111 00000000 00000000 00000001->127.0.0.1 85 sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 86 87 //ipv6 88 struct sockaddr_in6 sockaddr6; 89 memset(&sockaddr6, 0, sizeof(sockaddr6)); 90 91 sockaddr6.sin6_len = sizeof(sockaddr6); 92 sockaddr6.sin6_family = AF_INET6; 93 sockaddr6.sin6_port = htons(port); 94 95 sockaddr6.sin6_addr = in6addr_loopback; 96 //賦值 97 addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; 98 addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; 99 } 100 //非localhost、loopback,去獲取本機IP,看和傳進來Interface是同名或者同IP,相同纔給賦端口號,把數據封裝進Data。不然爲nil 101 else 102 { 103 //轉成cString 104 const char *iface = [interface UTF8String]; 105 106 //定義結構體指針,這個指針是本地IP 107 struct ifaddrs *addrs; 108 const struct ifaddrs *cursor; 109 110 //獲取到本機IP,爲0說明成功了 111 if ((getifaddrs(&addrs) == 0)) 112 { 113 //賦值 114 cursor = addrs; 115 //若是IP不爲空,則循環鏈表去設置 116 while (cursor != NULL) 117 { 118 //若是 addr4 IPV4地址爲空,並且地址類型爲IPV4 119 if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) 120 { 121 // IPv4 122 123 struct sockaddr_in nativeAddr4; 124 //memcpy內存copy函數,把src開始到size的字節數copy到 dest中 125 memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); 126 127 //比較兩個字符串是否相同,本機的IP名,和接口interface是否相同 128 if (strcmp(cursor->ifa_name, iface) == 0) 129 { 130 // Name match 131 //相同則賦值 port 132 nativeAddr4.sin_port = htons(port); 133 //用data封號IPV4地址 134 addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; 135 } 136 //本機IP名和interface不相同 137 else 138 { 139 //聲明一個IP 16位的數組 140 char ip[INET_ADDRSTRLEN]; 141 142 //這裏是轉成了10進制。。(由於獲取到的是二進制IP) 143 const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); 144 145 //若是conversion不爲空,說明轉換成功並且 ,比較轉換後的IP,和interface是否相同 146 if ((conversion != NULL) && (strcmp(ip, iface) == 0)) 147 { 148 // IP match 149 //相同則賦值 port 150 nativeAddr4.sin_port = htons(port); 151 152 addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; 153 } 154 } 155 } 156 //IPV6 同樣 157 else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) 158 { 159 // IPv6 160 161 struct sockaddr_in6 nativeAddr6; 162 memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); 163 164 if (strcmp(cursor->ifa_name, iface) == 0) 165 { 166 // Name match 167 168 nativeAddr6.sin6_port = htons(port); 169 170 addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; 171 } 172 else 173 { 174 char ip[INET6_ADDRSTRLEN]; 175 176 const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); 177 178 if ((conversion != NULL) && (strcmp(ip, iface) == 0)) 179 { 180 // IP match 181 182 nativeAddr6.sin6_port = htons(port); 183 184 addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; 185 } 186 } 187 } 188 189 //指向鏈表下一個addr 190 cursor = cursor->ifa_next; 191 } 192 //和getifaddrs對應,釋放這部份內存 193 freeifaddrs(addrs); 194 } 195 } 196 //若是這兩個二級指針存在,則取成一級指針,把addr4賦值給它 197 if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; 198 if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; 199
這個方法中,主要是大量的socket相關的函數的調用,會顯得比較難讀一點,其實簡單來說就作了這麼一件事:
把interface變成進行socket操做所須要的地址結構體,而後把地址結構體包裹在NSMuttableData中。
這裏,爲了讓你們能更容易理解,我把這個方法涉及到的socket
相關函數以及宏(按照調用順序)都列出來:
//拿到port strtol函數,將一個字符串,根據base參數轉成長整型, //如base值爲10則採用10進制,若base值爲16則採用16進制 long strtol(const char *__str, char **__endptr, int __base); //做用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操做的一種最快方法 //第一個參數爲指針地址,第二個爲設置值,第三個爲連續設置的長度(大小) memset(void *s,int ch,size_t n); //最大端口號 #define UINT16_MAX 65535 //做用是把主機字節序轉化爲網絡字節序 htons() //參數16位 htonl() //參數32位 //獲取佔用內存大小 sizeof() //比較兩個指針,是否相同 相同返回0 int strcmp(const char *__s1, const char *__s2) //內存copu函數,把src開始到len的字節數copy到 dest中 memcpy(dest, src, len) //inet_pton和inet_ntop這2個IP地址轉換函數,能夠在將IP地址在「點分十進制」和「二進制整數」之間轉換 //參數socklen_t cnt,他是所指向緩存區dst的大小,避免溢出,若是緩存區過小沒法存儲地址的值,則返回一個空指針,並將errno置爲ENOSPC const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt); //獲得本機地址 extern int getifaddrs(struct ifaddrs **); //釋放本機地址 extern void freeifaddrs(struct ifaddrs *);
還有一些用到的做爲參數的結構體:
//socket通訊用的 IPV4地址結構體 struct sockaddr_in { __uint8_t sin_len; //整個結構體大小 sa_family_t sin_family; //協議族,IPV4?IPV6 in_port_t sin_port; //端口 struct in_addr sin_addr; //IP地址 char sin_zero[8]; //空的佔位符,爲了和其餘地址結構體保持一致大小,方便轉化 }; //IPV6地址結構體,和上面的相似 struct sockaddr_in6 { __uint8_t sin6_len; /* length of this struct(sa_family_t) */ sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */ in_port_t sin6_port; /* Transport layer port # (in_port_t) */ __uint32_t sin6_flowinfo; /* IP6 flow information */ struct in6_addr sin6_addr; /* IP6 address */ __uint32_t sin6_scope_id; /* scope zone index */ }; //用來獲取本機IP的參數結構體 struct ifaddrs { //指向鏈表的下一個成員 struct ifaddrs *ifa_next; //接口名稱 char *ifa_name; //接口標識位(好比當IFF_BROADCAST或IFF_POINTOPOINT設置到此標識位時,影響聯合體變量ifu_broadaddr存儲廣播地址或ifu_dstaddr記錄點對點地址) unsigned int ifa_flags; //接口地址 struct sockaddr *ifa_addr; //存儲該接口的子網掩碼; struct sockaddr *ifa_netmask; //點對點的地址 struct sockaddr *ifa_dstaddr; //ifa_data存儲了該接口協議族的特殊信息,它一般是NULL(通常不關注他)。 void *ifa_data; };
這一段內容算是比較枯澀了,可是也是瞭解socket編程必經之路。
這裏提到了網絡字節序和主機字節序。咱們建立socket以前,必須把port和host這些參數轉化爲網絡字節序。那麼爲何要這麼作呢?
不一樣的CPU有不一樣的字節序類型 這些字節序是指整數在內存中保存的順序 這個叫作主機序
最多見的有兩種
1. Little endian:將低序字節存儲在起始地址
2. Big endian:將高序字節存儲在起始地址
這樣若是咱們到網絡中,就沒法得知互相的字節序是什麼了,因此咱們就必須統一一套排序,這樣網絡字節序就有它存在的必要了。
網絡字節順序是TCP/IP中規定好的一種數據表示格式,它與具體的CPU類型、操做系統等無關。從而能夠保證數據在不一樣主機之間傳輸時可以被正確解釋。網絡字節順序採用big endian排序方式。
你們感興趣能夠到這篇文章中去看看:網絡字節序與主機字節序。
除此以外比較重要的就是這幾個地址結構體了。它定義了咱們當前socket的地址信息。包括IP、Port、長度、協議族等等。固然socket中標識爲地址的結構體不止這3種,等咱們後續代碼來補充。
你們瞭解了咱們上述說的知識點,這個方法也就不難度了。這個方法主要是作了本機IPV4
和IPV6
地址的建立和綁定。固然這裏分了幾種狀況:
一、interface爲空的,咱們做爲客戶端不會出現這種狀況。注意以前咱們是這個參數不爲空纔會調入這個方法的。
而這個通常是用於作服務端監聽用的,這裏的處理是給本機地址綁定0地址(任意地址)。那麼這裏這麼作做用是什麼呢?引用一個應用場景來講明:
若是你的服務器有多個網卡(每一個網卡上有不一樣的IP地址),而你的服務(無論是在udp端口上偵聽,仍是在tcp端口上偵聽),出於某種緣由:多是你的服務器操做系統可能隨 時增減IP地址,也有多是爲了省去肯定服務器上有什麼網絡端口(網卡)的麻煩 —— 能夠要在調用bind()的時候,告訴操做系統:「我須要在 yyyy 端口上偵聽,全部發送到 服務器的這個端口,無論是哪一個網卡/哪一個IP地址接收到的數據,都是我處理的。」這時候,服務器程序則在0.0.0.0這個地址上進行偵聽。
二、若是interface爲localhost或者loopback則把IP設置爲127.0.0.1
,這裏localhost咱們你們都知道。那麼什麼是loopback呢?
loopback地址叫作迴環地址,他不是一個物理接口上的地址,他是一個虛擬的一個地址,只要路由器在工做,這個地址就存在.它是路由器的惟一標識。
更詳細的內容能夠看看百科:loopback
三、若是是一個其餘的地址,咱們會去使用getifaddrs()函數獲得本機地址。而後去對比本機名或者本機IP。有一個能相同,咱們就認爲該地址有效,就進行IPV4和IPV6綁定。不然什麼都不作。
至此這個本機地址綁定咱們就作完了,咱們前面也說過,通常咱們做爲客戶端,是不須要作這一步的。若是咱們不綁定,系統會本身綁定本機IP,而且選擇一個空閒可用的端口。因此這個方法是iOS用來做爲服務端調用的。
以前講到第3點了:
3.這裏把flag標記爲kSocketStarted:
flags |= kSocketStarted;
源碼中大量的運用了3個位運算符:分別是或(|)、與(&)、取反(~)、運算符。 運用這個標記的好處也很明顯,能夠很簡單的標記當前的狀態,而且由於flags所指向的枚舉值是用左位移的方式:
enum GCDAsyncSocketFlags { kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) kConnected = 1 << 1, // If set, the socket is connected kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. kReadSourceSuspended = 1 << 8, // If set, the read source is suspended kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained kDealloc = 1 << 16, // If set, the socket is being deallocated #if TARGET_OS_IPHONE kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available #endif };
因此flags
能夠經過|
的方式複合橫跨多個狀態,而且運算也很是輕量級,好處不少,全部的狀態標記的意義能夠在註釋中清晰的看出,這裏把狀態標記爲socket
已經開始鏈接了。
4.而後咱們調用了一個全局queue,異步的調用鏈接,這裏又作了兩件事:
server
的地址數組:
//server地址數組(包含IPV4 IPV6的地址 sockaddr_in六、sockaddr_in類型) NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
第二步是作一些錯誤判斷,而且把地址信息賦值到address6和address6中去,而後異步調用回socketQueue去用另外一個方法去發起鏈接:
//異步去發起鏈接 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }});
在這個方法中咱們能夠看到做者這裏把建立server地址這些費時的邏輯操做放在了異步線程中併發進行。而後獲得數據以後又回到了咱們的socketQueue發起下一步的鏈接。
而後這裏又是兩個很大塊的分支,首先咱們來看看server地址的獲取:
//根據host、port + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); NSMutableArray *addresses = nil; NSError *error = nil; //若是Host是這localhost或者loopback if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); //佔位置0 memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); //ipv6 struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; //兩個添加進數組 addresses = [NSMutableArray arrayWithCapacity:2]; [addresses addObject:address4]; [addresses addObject:address6]; } else { //拿到port String NSString *portStr = [NSString stringWithFormat:@"%hu", port]; //定義三個addrInfo 是一個sockaddr結構的鏈表而不是一個地址清單 struct addrinfo hints, *res, *res0; //初始化爲0 memset(&hints, 0, sizeof(hints)); //至關於 AF_UNSPEC ,返回的是適用於指定主機名和服務名且適合任何協議族的地址。 hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; //根據host port,去獲取地址信息。 int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); //出錯 if (gai_error) { //獲取到錯誤 error = [self gaiError:gai_error]; } //正確獲取到addrInfo else { // NSUInteger capacity = 0; //遍歷 res0 for (res = res0; res; res = res->ai_next) { //若是有IPV4 IPV6的,capacity+1 if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { capacity++; } } //生成一個地址數組,數組爲capacity大小 addresses = [NSMutableArray arrayWithCapacity:capacity]; //再去遍歷,爲何不一次遍歷完,僅僅是爲了限制數組的大小? for (res = res0; res; res = res->ai_next) { //IPV4 if (res->ai_family == AF_INET) { // Found IPv4 address. // Wrap the native address structure, and add to results. //加到數組中 NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address4]; } else if (res->ai_family == AF_INET6) { // Fixes connection issues with IPv6 // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 // Found IPv6 address. // Wrap the native address structure, and add to results. //強轉 struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr; //拿到port in_port_t *portPtr = &sockaddr->sin6_port; //若是Port爲0 if ((portPtr != NULL) && (*portPtr == 0)) { //賦值,用傳進來的port *portPtr = htons(port); } //添加到數組 NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address6]; } } //對應getaddrinfo 釋放內存 freeaddrinfo(res0); //若是地址裏一個沒有,報錯 EAI_FAIL:名字解析中不可恢復的失敗 if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } //賦值錯誤 if (errPtr) *errPtr = error; //返回地址 return addresses; }
這個方法根據host
進行了劃分:
host
爲localhost
或者loopback
,則按照咱們以前綁定本機地址那一套生成地址的方式,去生成IPV4和IPV6的地址,而且用NSData包裹住這個地址結構體,裝在NSMutableArray中。不是本機地址,那麼咱們就須要根據host和port去建立地址了,這裏用到的是這麼一個函數:
int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );
這個函數主要的做用是:根據hostname(IP),service(port),去獲取地址信息,而且把地址信息傳遞到result中。
而hints這個參數能夠是一個空指針,也能夠是一個指向某個addrinfo結構體的指針,若是填了,其實它就是一個配置參數,返回的地址信息會和這個配置參數的內容有關,以下例:
舉例來講:指定的服務既可支持TCP也可支持UDP,因此調用者能夠把hints結構中的ai_socktype成員設置成SOCK_DGRAM使得返回的僅僅是適用於數據報套接口的信息。
這裏咱們能夠看到result和hints這兩個參數指針指向的都是一個addrinfo的結構體,這是咱們繼上面以來看到的第4種地址結構體了。它的定義以下:
struct addrinfo { int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */ int ai_family; /* PF_xxx */ int ai_socktype; /* SOCK_xxx */ int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */ socklen_t ai_addrlen; /* length of ai_addr */ char *ai_canonname; /* canonical name for hostname */ struct sockaddr *ai_addr; /* binary address */ struct addrinfo *ai_next; /* next structure in linked list */ };
咱們能夠看到它其中包括了一個IPV4的結構體地址ai_addr
,還有一個指向下一個同類型數據節點的指針ai_next
。
其餘參數和以前的地址結構體一些參數做用相似,你們能夠對着註釋很好理解,或者仍有疑惑能夠看看這篇:
socket編程之addrinfo結構體與getaddrinfo函數
這裏講講ai_next
這個指針,由於咱們是去獲取server
端的地址,因此極可能有不止一個地址,好比IPV四、IPV6,又或者咱們以前所說的一個服務器有多個網卡,這時候可能就會有多個地址。這些地址就會用ai_next
指針串聯起來,造成一個單鏈表。
而後咱們拿到這個地址鏈表,去遍歷它,對應取出IPV四、IPV6的地址,封裝成NSData並裝到數組中去。
若是中間有錯誤,賦值錯誤,返回地址數組,理清楚這幾個結構體與函數,這個方法仍是至關容易讀的,具體的細節能夠看看註釋。
接着咱們回到本文方法二,就要用這個地址數組去作鏈接了。
//異步去發起鏈接 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }});
1 //鏈接的最終方法 1 2 - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 3 { 4 LogTrace(); 5 6 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 7 //至少有一個server地址 8 NSAssert(address4 || address6, @"Expected at least one valid address"); 9 10 //若是狀態不一致,說明斷開鏈接 11 if (aStateIndex != stateIndex) 12 { 13 LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); 14 15 // The connect operation has been cancelled. 16 // That is, socket was disconnected, or connection has already timed out. 17 return; 18 } 19 20 // Check for problems 21 //分開判斷。 22 BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; 23 BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; 24 25 if (isIPv4Disabled && (address6 == nil)) 26 { 27 NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; 28 29 [self closeWithError:[self otherError:msg]]; 30 return; 31 } 32 33 if (isIPv6Disabled && (address4 == nil)) 34 { 35 NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; 36 37 [self closeWithError:[self otherError:msg]]; 38 return; 39 } 40 41 // Start the normal connection process 42 43 NSError *err = nil; 44 //調用鏈接方法,若是失敗,則錯誤返回 45 if (![self connectWithAddress4:address4 address6:address6 error:&err]) 46 { 47 [self closeWithError:err]; 48 } 49 }
這個方法也比較簡單,基本上就是作了一些錯誤的判斷。好比:
socket
隊列。aStateIndex
和屬性stateIndex
是否是同一個值。說到這個值,不得不提的是大神用的框架,在容錯處理上,作的真不是通常的嚴謹。從這個stateIndex
上就能略見一二。aStateIndex
是咱們以前調用方法,用屬性傳過來的,因此按道理說,是確定同樣的。可是就怕在調用過程當中,這個值發生了改變,這時候整個socket配置也就徹底不同了,有可能咱們已經置空地址、銷燬socket、斷開鏈接等等...等咱們後面再來看這個屬性stateIndex
在什麼地方會發生改變。enum GCDAsyncSocketConfig { kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes };
前3個你們很好理解,無非就是用IPV4仍是IPV6。
而第4個官方註釋意思是,咱們即便關閉讀的流,也會保持Socket開啓。至於具體是什麼意思,咱們先不在這裏討論,等後文再說。
1 //鏈接最終方法 2。用兩個Server地址去鏈接,失敗返回NO,並填充error 2 - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr 3 { 4 LogTrace(); 5 6 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 7 8 //輸出一些東西? 9 LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); 10 LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); 11 12 // Determine socket type 13 14 //判斷是否傾向於IPV6 15 BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; 16 17 // Create and bind the sockets 18 19 //若是有IPV4地址,建立IPV4 Socket 20 if (address4) 21 { 22 LogVerbose(@"Creating IPv4 socket"); 23 24 socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; 25 } 26 //若是有IPV6地址,建立IPV6 Socket 27 if (address6) 28 { 29 LogVerbose(@"Creating IPv6 socket"); 30 31 socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; 32 } 33 34 //若是都爲空,直接返回 35 if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) 36 { 37 return NO; 38 } 39 40 //主選socketFD,備選alternateSocketFD 41 int socketFD, alternateSocketFD; 42 //主選地址和備選地址 43 NSData *address, *alternateAddress; 44 45 //IPV6 46 if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL) 47 { 48 socketFD = socket6FD; 49 alternateSocketFD = socket4FD; 50 address = address6; 51 alternateAddress = address4; 52 } 53 //主選IPV4 54 else 55 { 56 socketFD = socket4FD; 57 alternateSocketFD = socket6FD; 58 address = address4; 59 alternateAddress = address6; 60 } 61 //拿到當前狀態 62 int aStateIndex = stateIndex; 63 //用socket和address去鏈接 64 [self connectSocket:socketFD address:address stateIndex:aStateIndex]; 65 66 //若是有備選地址 67 if (alternateAddress) 68 { 69 //延遲去鏈接備選的地址 70 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ 71 [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; 72 }); 73 } 74 75 return YES; 76 }
這個方法也僅僅是鏈接中過渡的一個方法,作的事也很是簡單:
connectInterface4
或者connectInterface6
。alternateAddressDelay
,就是這個備選鏈接延時的屬性,去延時鏈接備選地址(固然若是主選地址在此時已經鏈接成功,會再次鏈接致使socket錯誤,而且關閉)。這兩步分別調用了各自的方法去實現,接下來咱們先來看建立本機Socket的方法:
1 //建立Socket 2 - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr 3 { 4 //建立socket,用的SOCK_STREAM TCP流 5 int socketFD = socket(family, SOCK_STREAM, 0); 6 //若是建立失敗 7 if (socketFD == SOCKET_NULL) 8 { 9 if (errPtr) 10 *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; 11 12 return socketFD; 13 } 14 15 //和connectInterface綁定 16 if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) 17 { 18 //綁定失敗,直接關閉返回 19 [self closeSocket:socketFD]; 20 21 return SOCKET_NULL; 22 } 23 24 // Prevent SIGPIPE signals 25 //防止終止進程的信號? 26 int nosigpipe = 1; 27 //SO_NOSIGPIPE是爲了不網絡錯誤,而致使進程退出。用這個來避免系統發送signal 28 setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); 29 30 return socketFD; 31 }
這個方法作了這麼幾件事:
一、建立了一個socket:
//建立一個socket,返回值爲Int。(注scoket其實就是Int類型) //第一個參數addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。 //第二個參數 type 表示 socket 的類型,一般是流stream(SOCK_STREAM) 或數據報文datagram(SOCK_DGRAM) //第三個參數 protocol 參數一般設置爲0,以便讓系統自動爲選擇咱們合適的協議,對於 stream socket 來講會是 TCP 協議(IPPROTO_TCP),而對於 datagram來講會是 UDP 協議(IPPROTO_UDP)。 int socketFD = socket(family, SOCK_STREAM, 0);
二、其實這個函數在以前那篇IM文章中也講過了,你們參考參考註釋看看就能夠了,這裏若是返回值爲-1,說明建立失敗。
三、去綁定咱們以前建立的本地地址,它調用了另一個方法來實現。
四、最後咱們調用了以下函數:
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
五、那麼這個函數是作什麼用的呢?簡單來講,它就是給咱們的socket加一些額外的設置項,來配置socket
的一些行爲。它還有許多的用法,具體能夠參考這篇文章:setsockopt函數
而這裏的目的是爲了來避免網絡錯誤而出現的進程退出的狀況,調用了這行函數,網絡錯誤後,系統再也不發送進程退出的信號。
關於這個進程退出的錯誤能夠參考這篇文章:Mac OSX下SO_NOSIGPIPE的怪異表現
總結,本文未完,續下節 :iOS即時通信之CocoaAsyncSocket源碼解析二