iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

原文

前言:

CocoaAsyncSocket是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的、强大的异步套接字库,向上封装出简单易用OC接口。省去了我们面向Socket以及数据流Stream等繁琐复杂的编程。

本文为一个系列,旨在让大家了解CocoaAsyncSocket是如何基于底层进行封装、工作的。

注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释

如果对该框架用法不熟悉的话,可以参考楼主之前这篇文章:iOS即时通讯,从入门到“放弃”?,或者自行查阅。

正文:

首先我们来看看框架的结构图:

iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

整个库就这么两个类,一个基于TCP,一个基于UDP。其中基于TCP的GCDAsyncSocket,大概8000多行代码。而GCDAsyncUdpSocket稍微少一点,也有5000多行。

所以单纯从代码量上来看,这个库还是做了很多事的。

顺便提一下,之前这个框架还有一个runloop版的,不过因为功能重叠和其它种种原因,后续版本便废弃了,现在仅有GCD版本。

本系列我们将重点来讲GCDAsyncSocket这个类。

我们先来看看这个类的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@implementation GCDAsyncSocket
{
    //flags,当前正在做操作的标识符
    uint32_t flags;
    uint16_t config;
 
    //代理
    __weak id 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;
}

这个里定义了一些属性,可以先简单看看注释,这里我们仅仅先暂时列出来,给大家混个眼熟。

在接下来的代码中,会大量穿插着这些属性的使用。所以大家不用觉得困惑,具体作用,我们后面会一一讲清楚的。

接着我们来看看本文方法一--初始化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//层级调用
- (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种

1
2
3
4
5
6
//本机的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,这个变量先简单的理解下:就是进行另一个服务端地址请求的延时。后面我们一讲到,大家就明白了。

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

iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

分割图.png

接着我们有socket了,我们如果是客户端,就需要去connect服务器。

又或者我们是服务端的话,就需要去bind端口,并且accept,等待客户端的连接。(基本上也没有用iOS来做服务端的吧...)

这里我们先作为客户端来看看connect:

iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

connect.png

其中和connect相关的方法就这么多,我们一般这么来连接到服务端:

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

也就是我们在截图中选中的方法,那我们就从这个方法作为起点,开始讲起吧。

本文方法二--connect总方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/逐级调用
- (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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif
#if GCDAsyncSocketLoggingEnabled
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// 
// 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中。我们做了如下判断:

1
2
3
4
5
6
  //在socketQueue中执行这个Block
  if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
      block();
  //否则同步的调起这个queue去执行
  else
      dispatch_sync(socketQueue, block);

保证这个连接操作一定是在我们的socketQueue中,而且还是以串行同步的形式去执行,规避了线程安全的问题。

  • 接着把Block中连接过程产生的错误进行赋值,并且把连接的结果返回出去

1
2
3
4
//如果有错误,赋值错误
  if (errPtr) *errPtr = preConnectErr;
  //把连接是否成功的result返回
  return result;

接着来看这个方法声明的Block内部,也就是进行连接的真正主题操作,这个连接过程将会调用许多函数,一环扣一环,我会尽可能用最清晰、详尽的语言来描述...

1.这个Block首先做了一些错误的判断,并调用了一些错误生成的方法。类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.接着做了一个前置的错误检查:

1
2
3
4
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
     return_from_block;
}

这个检查方法,如果没通过返回NO。并且如果interface有值,则会将本机的IPV4 IPV6的 address设置上。即我们之前提到的这两个属性:

1
2
3
4
  //本机的IPV4地址
NSData * connectInterface4;
//本机的IPV6地址
NSData * connectInterface6;

我们来看看这个前置检查方法:

本文方法三--前置检查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//在连接之前的接口检查,一般我们传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,我们所做的额外操作是什么呢,我们接下来看看这个方法:

本文方法四--本地地址绑定方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
- (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 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相关函数以及宏(按照调用顺序)都列出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//拿到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 *);

还有一些用到的作为参数的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//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地址的创建和绑定。当然这里分了几种情况:

1.interface为空的,我们作为客户端不会出现这种情况。注意之前我们是这个参数不为空才会调入这个方法的。

而这个一般是用于做服务端监听用的,这里的处理是给本机地址绑定0地址(任意地址)。那么这里这么做作用是什么呢?引用一个应用场景来说明:

如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。

2.如果interface为localhost或者loopback则把IP设置为127.0.0.1,这里localhost我们大家都知道。那么什么是loopback呢?

loopback地址叫做回环地址,他不是一个物理接口上的地址,他是一个虚拟的一个地址,只要路由器在工作,这个地址就存在.它是路由器的唯一标识。

更详细的内容可以看看百科:loopback

3.如果是一个其他的地址,我们会去使用getifaddrs()函数得到本机地址。然后去对比本机名或者本机IP。有一个能相同,我们就认为该地址有效,就进行IPV4和IPV6绑定。否则什么都不做。

至此这个本机地址绑定我们就做完了,我们前面也说过,一般我们作为客户端,是不需要做这一步的。如果我们不绑定,系统会自己绑定本机IP,并且选择一个空闲可用的端口。所以这个方法是iOS用来作为服务端调用的。

方法三--前置检查、方法四--本机地址绑定都说完了,我们继续接着之前的方法二往下看:

之前讲到第3点了:

3.这里把flag标记为kSocketStarted:

1
flags |= kSocketStarted;

源码中大量的运用了3个位运算符:分别是或(|)、与(&)、取反(~)、运算符。 运用这个标记的好处也很明显,可以很简单的标记当前的状态,并且因为flags所指向的枚举值是用左位移的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的地址数组:

1
2
//server地址数组(包含IPV4 IPV6的地址  sockaddr_in6、sockaddr_in类型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];

第二步是做一些错误判断,并且把地址信息赋值到address4和address6中去,然后异步调用回socketQueue去用另一个方法去发起连接:

1
2
3
4
5
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
 
   [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});

在这个方法中我们可以看到作者这里把创建server地址这些费时的逻辑操作放在了异步线程中并发进行。然后得到数据之后又回到了我们的socketQueue发起下一步的连接。

然后这里又是两个很大块的分支,首先我们来看看server地址的获取

本文方法五--创建服务端server地址数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//根据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. 如果host为localhost或者loopback,则按照我们之前绑定本机地址那一套生成地址的方式,去生成IPV4和IPV6的地址,并且用NSData包裹住这个地址结构体,装在NSMutableArray中。

  2. 不是本机地址,那么我们就需要根据host和port去创建地址了,这里用到的是这么一个函数:

1
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种地址结构体了。它的定义如下:

1
2
3
4
5
6
7
8
9
10
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并装到数组中去。

3.如果中间有错误,赋值错误,返回地址数组,理清楚这几个结构体与函数,这个方法还是相当容易读的,具体的细节可以看看注释。

接着我们回到本文方法二,就要用这个地址数组去做连接了。

1
2
3
4
5
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
 
     [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});

这里调用了我们本文方法六--开始连接的方法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//连接的最终方法 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中是需要哪种配置,它的参数对应了一个枚举:

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//连接最终方法 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//创建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:

1
2
3
4
5
//创建一个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,说明创建失败。

2.去绑定我们之前创建的本地地址,它调用了另外一个方法来实现。

3.最后我们调用了如下函数:

1
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));

那么这个函数是做什么用的呢?简单来说,它就是给我们的socket加一些额外的设置项,来配置socket的一些行为。它还有许多的用法,具体可以参考这篇文章:setsockopt函数

而这里的目的是为了来避免网络错误而出现的进程退出的情况,调用了这行函数,网络错误后,系统不再发送进程退出的信号。

关于这个进程退出的错误可以参考这篇文章:Mac OSX下SO_NOSIGPIPE的怪异表现

未完总结:

connect篇还没有完结,奈何篇幅问题,只能断在这里。下一个方法将是socket本地绑定的方法。再下面就是我们最终的连接方法了,历经九九八十一难,马上就要取到真经了...(然而这仅仅是一个开始...)

下一篇将会承接这一篇的内容继续讲,包括最终连接、连接完成后的source和流的处理。

我们还会去讲讲iOS作为服务端的accpet建立连接的流程。

除此之外还有 unix domin socket(进程间通信)的连接。

最近总感觉很浮躁,贴一句一直都很喜欢的话:

上善若水。水善利万物而不争

iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)