iOS即時通訊進階 - CocoaAsyncSocket源碼解析(Connect篇)

前言:

CocoaAsyncSocket是谷歌的開發者,基于BSD-Socket寫的一個IM框架,它給Mac和iOS提供了易于使用的、強大的異步套接字庫,向上封裝出簡單易用OC接口。省去了我們面向Socket以及數據流Stream等繁瑣復雜的編程。
本文為一個系列,旨在讓大家了解CocoaAsyncSocket是如何基于底層進行封裝、工作的。

注:文中涉及代碼比較多,建議大家結合源碼一起閱讀比較容易能加深理解。這里有樓主標注好注釋的源碼,有需要的可以作為參照:CocoaAsyncSocket源碼注釋

如果對該框架用法不熟悉的話,可以參考樓主之前這篇文章:iOS即時通訊,從入門到“放棄”?,或者自行查閱。

正文:
首先我們來看看框架的結構圖:

整個庫就這么兩個類,一個基于TCP,一個基于UDP。其中基于TCP的GCDAsyncSocket,大概8000多行代碼。而GCDAsyncUdpSocket稍微少一點,也有5000多行。
所以單純從代碼量上來看,這個庫還是做了很多事的。

順便提一下,之前這個框架還有一個runloop版的,不過因為功能重疊和其它種種原因,后續版本便廢棄了,現在僅有GCD版本。

本系列我們將重點來講GCDAsyncSocket這個類。

我們先來看看這個類的屬性:
@implementation GCDAsyncSocket
{
    //flags,當前正在做操作的標識符
    uint32_t flags;
    uint16_t config;
    
    //代理
    __weak id<GCDAsyncSocketDelegate> delegate;
    //代理回調的queue
    dispatch_queue_t delegateQueue;
    
    //本地IPV4Socket
    int socket4FD;
    //本地IPV6Socket
    int socket6FD;
    //unix域的套接字
    int socketUN;
    //unix域 服務端 url
    NSURL *socketUrl;
    //狀態Index
    int stateIndex;
    
    //本機的IPV4地址
    NSData * connectInterface4;
    //本機的IPV6地址
    NSData * connectInterface6;
    //本機unix域地址
    NSData * connectInterfaceUN;
    
    //這個類的對Socket的操作都在這個queue中,串行
    dispatch_queue_t socketQueue;
    
    dispatch_source_t accept4Source;
    dispatch_source_t accept6Source;
    dispatch_source_t acceptUNSource;
    
    //連接timer,GCD定時器
    dispatch_source_t connectTimer;
    dispatch_source_t readSource;
    dispatch_source_t writeSource;
    dispatch_source_t readTimer;
    dispatch_source_t writeTimer;
   
    //讀寫數據包數組 類似queue,最大限制為5個包
    NSMutableArray *readQueue;
    NSMutableArray *writeQueue;
    
    //當前正在讀寫數據包
    GCDAsyncReadPacket *currentRead;
    GCDAsyncWritePacket *currentWrite;
    //當前socket未獲取完的數據大小
    unsigned long socketFDBytesAvailable;
    
    //全局公用的提前緩沖區
    GCDAsyncSocketPreBuffer *preBuffer;
        
#if TARGET_OS_IPHONE
    CFStreamClientContext streamContext;
    //讀的數據流
    CFReadStreamRef readStream;
    //寫的數據流
    CFWriteStreamRef writeStream;
#endif
    //SSL上下文,用來做SSL認證
    SSLContextRef sslContext;
    
    //全局公用的SSL的提前緩沖區
    GCDAsyncSocketPreBuffer *sslPreBuffer;
    size_t sslWriteCachedLength;
    
    //記錄SSL讀取數據錯誤
    OSStatus sslErrCode;
    //記錄SSL握手的錯誤
    OSStatus lastSSLHandshakeError;
    
    //socket隊列的標識key
    void *IsOnSocketQueueOrTargetQueueKey;
    
    id userData;
    
    //連接備選服務端地址的延時 (另一個IPV4或IPV6)
    NSTimeInterval alternateAddressDelay;
}

這個里定義了一些屬性,可以先簡單看看注釋,這里我們僅僅先暫時列出來,給大家混個眼熟。
在接下來的代碼中,會大量穿插著這些屬性的使用。所以大家不用覺得困惑,具體作用,我們后面會一一講清楚的。

接著我們來看看本文方法一--初始化方法:
//層級調用
- (id)init
{
     return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL];
}

- (id)initWithSocketQueue:(dispatch_queue_t)sq
{
     return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq];
}

- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
{
     return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL];
}

- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
     if((self = [super init]))
     {
          delegate = aDelegate;
          delegateQueue = dq;
          
         //這個宏是在sdk6.0之后才有的,如果是之前的,則OS_OBJECT_USE_OBJC為0,!0即執行if語句
        //對6.0的適配,如果是6.0以下,則去retain release,6.0之后ARC也管理了GCD
          #if !OS_OBJECT_USE_OBJC
        
          if (dq) dispatch_retain(dq);
          #endif
          
        //創建socket,先都置為 -1
        //本機的ipv4
          socket4FD = SOCKET_NULL;
        //ipv6
          socket6FD = SOCKET_NULL;
        //應該是UnixSocket
          socketUN = SOCKET_NULL;
        //url
          socketUrl = nil;
        //狀態
          stateIndex = 0;
          
          if (sq)
          {
            //如果scoketQueue是global的,則報錯。斷言必須要一個非并行queue。
               NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
                        @"The given socketQueue parameter must not be a concurrent queue.");
               NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
                        @"The given socketQueue parameter must not be a concurrent queue.");
               NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                        @"The given socketQueue parameter must not be a concurrent queue.");
               //拿到scoketQueue
               socketQueue = sq;
            //iOS6之下retain
               #if !OS_OBJECT_USE_OBJC
               dispatch_retain(sq);
               #endif
          }
          else
          {
            //沒有的話創建一個,  名字為:GCDAsyncSocket,串行
               socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
          }
          
          // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
          // From the documentation:
          //
          // > Keys are only compared as pointers and are never dereferenced.
          // > Thus, you can use a pointer to a static variable for a specific subsystem or
          // > any other value that allows you to identify the value uniquely.
          //
          // We're just going to use the memory address of an ivar.
          // Specifically an ivar that is explicitly named for our purpose to make the code more readable.
          //
          // However, it feels tedious (and less readable) to include the "&" all the time:
          // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
          //
          // So we're going to make it so it doesn't matter if we use the '&' or not,
          // by assigning the value of the ivar to the address of the ivar.
          // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
          
        
        //比如原來為   0X123 -> NULL 變成  0X222->0X123->NULL
        //自己的指針等于自己原來的指針,成二級指針了  看了注釋是為了以后省略&,讓代碼更可讀?
          IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
          
        
          void *nonNullUnusedPointer = (__bridge void *)self;
        
        //dispatch_queue_set_specific給當前隊里加一個標識 dispatch_get_specific當前線程取出這個標識,判斷是不是在這個隊列
        //這個key的值其實就是一個一級指針的地址  ,第三個參數把自己傳過去了,上下文對象?第4個參數,為銷毀的時候用的,可以指定一個函數
          dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
          //讀的數組 限制為5
          readQueue = [[NSMutableArray alloc] initWithCapacity:5];
          currentRead = nil;
          
        //寫的數組,限制5
          writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
          currentWrite = nil;
          
        //設置大小為 4kb
          preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
    
#pragma mark alternateAddressDelay??
        //交替地址延時?? wtf
        alternateAddressDelay = 0.3;
     }
     return self;
}

詳細的細節可以看看注釋,這里初始化了一些屬性:

1.代理、以及代理queue的賦值。

2.本機socket的初始化:包括下面3種

//本機的ipv4
socket4FD = SOCKET_NULL;
//ipv6
socket6FD = SOCKET_NULL;
//UnixSocket
socketUN = SOCKET_NULL;

其中值得一提的是第三種:UnixSocket,這個是用于Unix Domin Socket通信用的。
那么什么是Unix Domain Socket呢?
原來它是在socket的框架上發展出一種IPC(進程間通信)機制,雖然網絡socket也可用于同一臺主機的進程間通訊(通過loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC 更有效率 :

  • 不需要經過網絡協議棧
  • 不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程。這是因為,IPC機制本質上是可靠的通訊,而網絡協議是為不可靠的通訊設計的。UNIX Domain Socket也提供面向流和面向數據包兩種API接口,類似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不會丟失也不會順序錯亂。

基本上它是當今應用于IPC最主流的方式。至于它到底和普通的socket通信實現起來有什么區別,別著急,我們接著往下看。

3.生成了一個socketQueue,這個queue是串行的,接下來我們看代碼就會知道它貫穿于這個類的所有地方。所有對socket以及一些內部數據的相關操作,都需要在這個串行queue中進行。這樣使得整個類沒有加一個鎖,就保證了整個類的線程安全。

4.創建了兩個讀寫隊列(本質數組),接下來我們所有的讀寫任務,都會先追加在這個隊列最后,然后每次取出隊列中最前面的任務,進行處理。

5.創建了一個全局的數據緩沖區:preBuffer,我們所操作的數據,大部分都是要先存入這個preBuffer中,然后再從preBuffer取出進行處理的。

6.初始化了一個交替延時變量:alternateAddressDelay,這個變量先簡單的理解下:就是進行另一個服務端地址請求的延時。后面我們一講到,大家就明白了。

初始化方法就到此為止了。

分割圖.png

接著我們有socket了,我們如果是客戶端,就需要去connect服務器。

又或者我們是服務端的話,就需要去bind端口,并且accept,等待客戶端的連接。(基本上也沒有用iOS來做服務端的吧...)

這里我們先作為客戶端來看看connect
  • connect.png

其中和connect相關的方法就這么多,我們一般這么來連接到服務端:

[socket connectToHost:Khost onPort:Kport error:nil];

也就是我們在截圖中選中的方法,那我們就從這個方法作為起點,開始講起吧。

本文方法二--connect總方法
/逐級調用
- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
{
    return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr];
}

- (BOOL)connectToHost:(NSString *)host
               onPort:(uint16_t)port
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
    return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr];
}

//多一個inInterface,本機地址
- (BOOL)connectToHost:(NSString *)inHost
               onPort:(uint16_t)port
         viaInterface:(NSString *)inInterface
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
    //{} 跟蹤當前行為
    LogTrace();
    
    // Just in case immutable objects were passed
    //拿到host ,copy防止值被修改
    NSString *host = [inHost copy];
    //interface?接口?
    NSString *interface = [inInterface copy];
    
    //聲明兩個__block的
    __block BOOL result = NO;
    //error信息
    __block NSError *preConnectErr = nil;
    
    //gcdBlock ,都包裹在自動釋放池中
    dispatch_block_t block = ^{ @autoreleasepool {
        
        // Check for problems with host parameter
        
        if ([host length] == 0)
        {
            NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
            preConnectErr = [self badParamError:msg];
            
            //其實就是return,大牛的代碼真是充滿逼格
            return_from_block;
        }
        
        // Run through standard pre-connect checks
        //一個前置的檢查,如果沒通過返回,這個檢查里,如果interface有值,則會將本機的IPV4 IPV6的 address設置上。
        if (![self preConnectWithInterface:interface error:&preConnectErr])
        {
            return_from_block;
        }
        
        // We've made it past all the checks.
        // It's time to start the connection process.
        //flags 做或等運算。 flags標識為開始Socket連接
        flags |= kSocketStarted;
        
        //又是一個{}? 只是為了標記么?
        LogVerbose(@"Dispatching DNS lookup...");
        
        // It's possible that the given host parameter is actually a NSMutableString.
        //很可能給我們的服務端的參數是一個可變字符串
        // So we want to copy it now, within this block that will be executed synchronously.
        //所以我們需要copy,在Block里同步的執行
        // This way the asynchronous lookup block below doesn't have to worry about it changing.
        //這種基于Block的異步查找,不需要擔心它被改變
        
        //copy,防止改變
        NSString *hostCpy = [host copy];
        
        //拿到狀態
        int aStateIndex = stateIndex;
        __weak GCDAsyncSocket *weakSelf = self;
        
        //全局Queue
        dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //異步執行
        dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
            //忽視循環引用
        #pragma clang diagnostic push
        #pragma clang diagnostic warning "-Wimplicit-retain-self"
            
            //查找錯誤
            NSError *lookupErr = nil;
            //server地址數組(包含IPV4 IPV6的地址  sockaddr_in6、sockaddr_in類型)
            NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
            
            //strongSelf
            __strong GCDAsyncSocket *strongSelf = weakSelf;
            
            //完整Block安全形態,在加個if
            if (strongSelf == nil) return_from_block;
            
            //如果有錯
            if (lookupErr)
            {
                //用cocketQueue
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    //一些錯誤處理,清空一些數據等等
                    [strongSelf lookup:aStateIndex didFail:lookupErr];
                }});
            }
            //正常
            else
            {
                
                NSData *address4 = nil;
                NSData *address6 = nil;
                //遍歷地址數組
                for (NSData *address in addresses)
                {
                    //判斷address4為空,且address為IPV4
                    if (!address4 && [[self class] isIPv4Address:address])
                    {
                        address4 = address;
                    }
                    //判斷address6為空,且address為IPV6
                    else if (!address6 && [[self class] isIPv6Address:address])
                    {
                        address6 = address;
                    }
                }
                //異步去發起連接
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    
                    [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
                }});
            }
            
        #pragma clang diagnostic pop
        }});
        
        
        //開啟連接超時
        [self startConnectTimeout:timeout];
        
        result = YES;
    }};
    //在socketQueue中執行這個Block
    if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
        block();
    //否則同步的調起這個queue去執行
    else
        dispatch_sync(socketQueue, block);
    
    //如果有錯誤,賦值錯誤
    if (errPtr) *errPtr = preConnectErr;
    //把連接是否成功的result返回
    return result;
}

這個方法非常長,它主要做了以下幾件事:

  • 首先我們需要說一下的是,整個類大量的會出現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.這個Block首先做了一些錯誤的判斷,并調用了一些錯誤生成的方法。類似:
if ([host length] == 0)
{
     NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
     preConnectErr = [self badParamError:msg];
     
  //其實就是return,大牛的代碼真是充滿逼格
     return_from_block;
}
//用該字符串生成一個錯誤,錯誤的域名,錯誤的參數
- (NSError *)badParamError:(NSString *)errMsg
{
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
    
    return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
}
2.接著做了一個前置的錯誤檢查:
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
     return_from_block;
}

這個檢查方法,如果沒通過返回NO。并且如果interface有值,則會將本機的IPV4 IPV6的 address設置上。即我們之前提到的這兩個屬性:

  //本機的IPV4地址
NSData * connectInterface4;
//本機的IPV6地址
NSData * connectInterface6;

我們來看看這個前置檢查方法:

本文方法三--前置檢查方法
//在連接之前的接口檢查,一般我們傳nil  interface本機的IP 端口等等
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
    //先斷言,如果當前的queue不是初始化quueue,直接報錯
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    //無代理
    if (delegate == nil) // Must have delegate set
    {
        if (errPtr)
        {
            NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
            *errPtr = [self badConfigError:msg];
        }
        return NO;
    }
    //沒有代理queue
    if (delegateQueue == NULL) // Must have delegate queue set
    {
        if (errPtr)
        {
            NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
            *errPtr = [self badConfigError:msg];
        }
        return NO;
    }
    
    //當前不是非連接狀態
    if (![self isDisconnected]) // Must be disconnected
    {
        if (errPtr)
        {
            NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
            *errPtr = [self badConfigError:msg];
        }
        return NO;
    }
    
    //判斷是否支持IPV4 IPV6  &位與運算,因為枚舉是用  左位移<<運算定義的,所以可以用來判斷 config包不包含某個枚舉。因為一個值可能包含好幾個枚舉值,所以這時候不能用==來判斷,只能用&來判斷
    BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
    BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
    
    //是否都不支持
    if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
    {
        if (errPtr)
        {
            NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
            *errPtr = [self badConfigError:msg];
        }
        return NO;
    }
    
    //如果有interface,本機地址
    if (interface)
    {
        NSMutableData *interface4 = nil;
        NSMutableData *interface6 = nil;
        
        //得到本機的IPV4 IPV6地址
        [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
        
        //如果兩者都為nil
        if ((interface4 == nil) && (interface6 == nil))
        {
            if (errPtr)
            {
                NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
                *errPtr = [self badParamError:msg];
            }
            return NO;
        }
        
        if (isIPv4Disabled && (interface6 == nil))
        {
            if (errPtr)
            {
                NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
                *errPtr = [self badParamError:msg];
            }
            return NO;
        }
        
        if (isIPv6Disabled && (interface4 == nil))
        {
            if (errPtr)
            {
                NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
                *errPtr = [self badParamError:msg];
            }
            return NO;
        }
        //如果都沒問題,則賦值
        connectInterface4 = interface4;
        connectInterface6 = interface6;
    }
    
    // Clear queues (spurious read/write requests post disconnect)
    //清除queue(假的讀寫請求 ,提交斷開連接)
    //讀寫Queue清除
    [readQueue removeAllObjects];
    [writeQueue removeAllObjects];
    
    return YES;
}

又是非常長的一個方法,但是這個方法還是非常好讀的。

  • 主要是對連接前的一個屬性參數的判斷,如果不齊全的話,則填充錯誤指針,并且返回NO。

  • 在這里如果我們interface這個參數不為空話,我們會額外多執行一些操作。
    首先來講講這個參數是什么,簡單來講,這個就是我們設置的本機IP+端口號。照理來說我們是不需要去設置這個參數的,默認的為localhost(127.0.0.1)本機地址。而端口號會在本機中取一個空閑可用的端口。
    而我們一旦設置了這個參數,就會強制本地IP和端口為我們指定的。其實這樣設置反而不好,其實大家也能想明白,這里端口號如果我們寫死,萬一被其他進程給占用了。那么肯定是無法連接成功的。
    所以就有了我們做IM的時候,一般是不會去指定客戶端bind某一個端口。而是用系統自動去選擇。

  • 我們最后清空了當前讀寫queue中,所有的任務。

至于有interface,我們所做的額外操作是什么呢,我們接下來看看這個方法:

本文方法四--本地地址綁定方法
- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
                    address6:(NSMutableData **)interfaceAddr6Ptr
             fromDescription:(NSString *)interfaceDescription
                        port:(uint16_t)port
{
    NSMutableData *addr4 = nil;
    NSMutableData *addr6 = nil;
    
    NSString *interface = nil;
    
    //先用:分割
    NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
    if ([components count] > 0)
    {
        NSString *temp = [components objectAtIndex:0];
        if ([temp length] > 0)
        {
            interface = temp;
        }
    }
    if ([components count] > 1 && port == 0)
    {
        //拿到port strtol函數,將一個字符串,根據base參數轉成長整型,如base值為10則采用10進制,若base值為16則采用16進制
        long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
        //UINT16_MAX,65535最大端口號
        if (portL > 0 && portL <= UINT16_MAX)
        {
            port = (uint16_t)portL;
        }
    }
    
    //為空則自己創建一個 0x00000000 ,全是0 ,為線路地址
    //如果端口為0 通常用于分析操作系統。這一方法能夠工作是因為在一些系統中“0”是無效端口,當你試圖使用通常的閉合端口連接它時將產生不同的結果。一種典型的掃描,使用IP地址為0.0.0.0,設置ACK位并在以太網層廣播。
    if (interface == nil)
    {
        
        struct sockaddr_in sockaddr4;
        
        //memset作用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操作的一種最快方法
        
        //memset(void *s,int ch,size_t n);函數,第一個參數為指針地址,第二個為設置值,第三個為連續設置的長度(大小)
        memset(&sockaddr4, 0, sizeof(sockaddr4));
        //結構體長度
        sockaddr4.sin_len         = sizeof(sockaddr4);
        //addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
        sockaddr4.sin_family      = AF_INET;
        //端口號 htons將主機字節順序轉換成網絡字節順序  16位
        sockaddr4.sin_port        = htons(port);
        //htonl ,將INADDR_ANY:0.0.0.0,不確定地址,或者任意地址  htonl 32位。 也是轉為網絡字節序
      
        //ipv4 32位  4個字節    INADDR_ANY,0x00000000 (16進制,一個0代表4位,8個0就是32位) =   4個字節的
        sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
        struct sockaddr_in6 sockaddr6;
        memset(&sockaddr6, 0, sizeof(sockaddr6));
        
        sockaddr6.sin6_len       = sizeof(sockaddr6);
        //ipv6
        sockaddr6.sin6_family    = AF_INET6;
        //port
        sockaddr6.sin6_port      = htons(port);
        
        //共128位
        sockaddr6.sin6_addr      = in6addr_any;
        
        //把這兩個結構體轉成data
        addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
        addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
    }
    //如果localhost、loopback 回環地址,虛擬地址,路由器工作它就存在。一般用來標識路由器
    //這兩種的話就賦值為127.0.0.1,端口為port
    else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"])
    {
        // LOOPBACK address
        
        //ipv4
        struct sockaddr_in sockaddr4;
        memset(&sockaddr4, 0, sizeof(sockaddr4));
        
        sockaddr4.sin_len         = sizeof(sockaddr4);
        sockaddr4.sin_family      = AF_INET;
        sockaddr4.sin_port        = htons(port);
        
        //#define   INADDR_LOOPBACK     (u_int32_t)0x7f000001
        //7f000001->1111111 00000000 00000000 00000001->127.0.0.1
        sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
        
        //ipv6
        struct sockaddr_in6 sockaddr6;
        memset(&sockaddr6, 0, sizeof(sockaddr6));
        
        sockaddr6.sin6_len       = sizeof(sockaddr6);
        sockaddr6.sin6_family    = AF_INET6;
        sockaddr6.sin6_port      = htons(port);

        sockaddr6.sin6_addr      = in6addr_loopback;
        //賦值
        addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
        addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
    }
    //非localhost、loopback,去獲取本機IP,看和傳進來Interface是同名或者同IP,相同才給賦端口號,把數據封裝進Data。否則為nil
    else
    {
        //轉成cString
        const char *iface = [interface UTF8String];
    
        //定義結構體指針,這個指針是本地IP
        struct ifaddrs *addrs;
        const struct ifaddrs *cursor;
        
        //獲取到本機IP,為0說明成功了
        if ((getifaddrs(&addrs) == 0))
        {
            //賦值
            cursor = addrs;
            //如果IP不為空,則循環鏈表去設置
            while (cursor != NULL)
            {
                //如果 addr4 IPV4地址為空,而且地址類型為IPV4
                if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
                {
                    // IPv4
                    
                    struct sockaddr_in nativeAddr4;
                    //memcpy內存copy函數,把src開始到size的字節數copy到 dest中
                    memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));
                    
                    //比較兩個字符串是否相同,本機的IP名,和接口interface是否相同
                    if (strcmp(cursor->ifa_name, iface) == 0)
                    {
                        // Name match
                        //相同則賦值 port
                        nativeAddr4.sin_port = htons(port);
                        //用data封號IPV4地址
                        addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
                    }
                    //本機IP名和interface不相同
                    else
                    {
                        //聲明一個IP 16位的數組
                        char ip[INET_ADDRSTRLEN];
                     
                        //這里是轉成了10進制。。(因為獲取到的是二進制IP)
                        const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));
                        
                        //如果conversion不為空,說明轉換成功而且 ,比較轉換后的IP,和interface是否相同
                        if ((conversion != NULL) && (strcmp(ip, iface) == 0))
                        {
                            // IP match
                            //相同則賦值 port
                            nativeAddr4.sin_port = htons(port);
                            
                            addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
                        }
                    }
                }
                //IPV6 一樣
                else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6))
                {
                    // IPv6
                    
                    struct sockaddr_in6 nativeAddr6;
                    memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6));
                    
                    if (strcmp(cursor->ifa_name, iface) == 0)
                    {
                        // Name match
                        
                        nativeAddr6.sin6_port = htons(port);
                        
                        addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
                    }
                    else
                    {
                        char ip[INET6_ADDRSTRLEN];
                        
                        const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip));
                        
                        if ((conversion != NULL) && (strcmp(ip, iface) == 0))
                        {
                            // IP match
                            
                            nativeAddr6.sin6_port = htons(port);
                            
                            addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
                        }
                    }
                }
                
                //指向鏈表下一個addr
                cursor = cursor->ifa_next;
            }
            //和getifaddrs對應,釋放這部分內存
            freeifaddrs(addrs);
        }
    }
    //如果這兩個二級指針存在,則取成一級指針,把addr4賦值給它
    if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
    if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;

這個方法中,主要是大量的socket相關的函數的調用,會顯得比較難讀一點,其實簡單來講就做了這么一件事:
interface變成進行socket操作所需要的地址結構體,然后把地址結構體包裹在NSMutableData中。

這里,為了讓大家能更容易理解,我把這個方法涉及到的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種,等我們后續代碼來補充。

大家了解了我們上述說的知識點,這個方法也就不難度了。這個方法主要是做了本機IPV4IPV6地址的創建和綁定。當然這里分了幾種情況:

  1. interface為空的,我們作為客戶端不會出現這種情況。注意之前我們是這個參數不為空才會調入這個方法的。
    而這個一般是用于做服務端監聽用的,這里的處理是給本機地址綁定0地址(任意地址)。那么這里這么做作用是什么呢?引用一個應用場景來說明:

如果你的服務器有多個網卡(每個網卡上有不同的IP地址),而你的服務(不管是在udp端口上偵聽,還是在tcp端口上偵聽),出于某種原因:可能是你的服務器操作系統可能隨時增減IP地址,也有可能是為了省去確定服務器上有什么網絡端口(網卡)的麻煩 —— 可以要在調用bind()的時候,告訴操作系統:“我需要在 yyyy 端口上偵聽,所有發送到服務器的這個端口,不管是哪個網卡/哪個IP地址接收到的數據,都是我處理的。”這時候,服務器程序則在0.0.0.0這個地址上進行偵聽。

  1. 如果interfacelocalhost或者loopback則把IP設置為127.0.0.1,這里localhost我們大家都知道。那么什么是loopback呢?
    loopback地址叫做回環地址,他不是一個物理接口上的地址,他是一個虛擬的一個地址,只要路由器在工作,這個地址就存在.它是路由器的唯一標識。
    更詳細的內容可以看看百科:loopback

  2. 如果是一個其他的地址,我們會去使用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_in6、sockaddr_in類型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
  • 第二步是做一些錯誤判斷,并且把地址信息賦值到address4address6中去,然后異步調用回socketQueue去用另一個方法去發起連接:
//異步去發起連接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
     
     [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});

在這個方法中我們可以看到作者這里把創建server地址這些費時的邏輯操作放在了異步線程中并發進行。然后得到數據之后又回到了我們的socketQueue發起下一步的連接。

然后這里又是兩個很大塊的分支,首先我們來看看server地址的獲取:

本文方法五--創建服務端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進行了劃分:

  1. 如果hostlocalhost或者loopback,則按照我們之前綁定本機地址那一套生成地址的方式,去生成IPV4和IPV6的地址,并且用NSData包裹住這個地址結構體,裝在NSMutableArray中。
  2. 不是本機地址,那么我們就需要根據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端的地址,所以很可能有不止一個地址,比如IPV4、IPV6,又或者我們之前所說的一個服務器有多個網卡,這時候可能就會有多個地址。這些地址就會用ai_next指針串聯起來,形成一個單鏈表。

然后我們拿到這個地址鏈表,去遍歷它,對應取出IPV4、IPV6的地址,封裝成NSData并裝到數組中去。

  1. 如果中間有錯誤,賦值錯誤,返回地址數組,理清楚這幾個結構體與函數,這個方法還是相當容易讀的,具體的細節可以看看注釋。

接著我們回到本文方法二,就要用這個地址數組去做連接了。

//異步去發起連接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
     
     [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
這里調用了我們本文方法六--開始連接的方法1
//連接的最終方法 1
- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
{
     LogTrace();
     
     NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    //至少有一個server地址
     NSAssert(address4 || address6, @"Expected at least one valid address");
     
    //如果狀態不一致,說明斷開連接
     if (aStateIndex != stateIndex)
     {
          LogInfo(@"Ignoring lookupDidSucceed, already disconnected");
          
          // The connect operation has been cancelled.
          // That is, socket was disconnected, or connection has already timed out.
          return;
     }
     
     // Check for problems
     //分開判斷。
     BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
     BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
     
     if (isIPv4Disabled && (address6 == nil))
     {
          NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
          
          [self closeWithError:[self otherError:msg]];
          return;
     }
     
     if (isIPv6Disabled && (address4 == nil))
     {
          NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address.";
          
          [self closeWithError:[self otherError:msg]];
          return;
     }
     
     // Start the normal connection process
     
     NSError *err = nil;
    //調用連接方法,如果失敗,則錯誤返回
     if (![self connectWithAddress4:address4 address6:address6 error:&err])
     {
          [self closeWithError:err];
     }
}

這個方法也比較簡單,基本上就是做了一些錯誤的判斷。比如:

  1. 判斷在不在這個socket隊列。
  2. 判斷傳過來的aStateIndex和屬性stateIndex是不是同一個值。說到這個值,不得不提的是大神用的框架,在容錯處理上,做的真不是一般的嚴謹。從這個stateIndex上就能略見一二。
    這個aStateIndex是我們之前調用方法,用屬性傳過來的,所以按道理說,是肯定一樣的。但是就怕在調用過程中,這個值發生了改變,這時候整個socket配置也就完全不一樣了,有可能我們已經置空地址、銷毀socket、斷開連接等等...等我們后面再來看這個屬性stateIndex在什么地方會發生改變。
  3. 判斷config中是需要哪種配置,它的參數對應了一個枚舉:
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開啟。至于具體是什么意思,我們先不在這里討論,等后文再說。

這里調用了我們本文方法七--開始連接的方法2
//連接最終方法 2。用兩個Server地址去連接,失敗返回NO,并填充error
- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
{
    LogTrace();
    
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    //輸出一些東西?
    LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]);
    LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]);
    
    // Determine socket type
    
    //判斷是否傾向于IPV6
    BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
    
    // Create and bind the sockets
    
    //如果有IPV4地址,創建IPV4 Socket
    if (address4)
    {
        LogVerbose(@"Creating IPv4 socket");
        
        socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr];
    }
    //如果有IPV6地址,創建IPV6 Socket
    if (address6)
    {
        LogVerbose(@"Creating IPv6 socket");
        
        socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr];
    }
    
    //如果都為空,直接返回
    if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL)
    {
        return NO;
    }
    
    //主選socketFD,備選alternateSocketFD
    int socketFD, alternateSocketFD;
    //主選地址和備選地址
    NSData *address, *alternateAddress;
    
    //IPV6
    if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL)
    {
        socketFD = socket6FD;
        alternateSocketFD = socket4FD;
        address = address6;
        alternateAddress = address4;
    }
    //主選IPV4
    else
    {
        socketFD = socket4FD;
        alternateSocketFD = socket6FD;
        address = address4;
        alternateAddress = address6;
    }
    //拿到當前狀態
    int aStateIndex = stateIndex;
    //用socket和address去連接
    [self connectSocket:socketFD address:address stateIndex:aStateIndex];
    
    //如果有備選地址
    if (alternateAddress)
    {
        //延遲去連接備選的地址
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{
            [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex];
        });
    }
    
    return YES;
}

這個方法也僅僅是連接中過渡的一個方法,做的事也非常簡單:

  1. 就是拿到IPV4和IPV6地址,先去創建對應的socket,注意這個socket是本機客戶端的,和server端沒有關系。這里服務端的IPV4和IPV6地址僅僅是用來判斷是否需要去創建對應的本機Socket。這里去創建socket會帶上我們之前生成的本地地址信息connectInterface4或者connectInterface6
  2. 根據我們的config配置,得到主選連接和備選連接。 然后先去連接主選連接地址,在用我們一開始初始化中設置的屬性alternateAddressDelay,就是這個備選連接延時的屬性,去延時連接備選地址(當然如果主選地址在此時已經連接成功,會再次連接導致socket錯誤,并且關閉)。

這兩步分別調用了各自的方法去實現,接下來我們先來看創建本機Socket的方法:

本文方法八--創建Socket:
//創建Socket
- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr
{
    //創建socket,用的SOCK_STREAM TCP流
    int socketFD = socket(family, SOCK_STREAM, 0);
    //如果創建失敗
    if (socketFD == SOCKET_NULL)
    {
        if (errPtr)
            *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
        
        return socketFD;
    }
    
    //和connectInterface綁定
    if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
    {
        //綁定失敗,直接關閉返回
        [self closeSocket:socketFD];
        
        return SOCKET_NULL;
    }
    
    // Prevent SIGPIPE signals
    //防止終止進程的信號?
    int nosigpipe = 1;
    //SO_NOSIGPIPE是為了避免網絡錯誤,而導致進程退出。用這個來避免系統發送signal
    setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
    
    return socketFD;
}

這個方法做了這么幾件事:

  1. 創建了一個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,說明創建失敗。

  1. 去綁定我們之前創建的本地地址,它調用了另外一個方法來實現。
  2. 最后我們調用了如下函數?:
   setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));

那么這個函數是做什么用的呢?簡單來說,它就是給我們的socket加一些額外的設置項,來配置socket的一些行為。它還有許多的用法,具體可以參考這篇文章:setsockopt函數

而這里的目的是為了來避免網絡錯誤而出現的進程退出的情況,調用了這行函數,網絡錯誤后,系統不再發送進程退出的信號。
關于這個進程退出的錯誤可以參考這篇文章:Mac OSX下SO_NOSIGPIPE的怪異表現

未完總結:

connect篇還沒有完結,奈何篇幅問題,只能斷在這里。下一個方法將是socket本地綁定的方法。再下面就是我們最終的連接方法了,歷經九九八十一難,馬上就要取到真經了...(然而這僅僅是一個開始...)
下一篇將會承接這一篇的內容繼續講,包括最終連接、連接完成后的source和流的處理。
我們還會去講講iOS作為服務端的accpet建立連接的流程。
除此之外還有 unix domin socket(進程間通信)的連接。

最近總感覺很浮躁,貼一句一直都很喜歡的話:
上善若水。水善利萬物而不爭

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容