CocoaAsyncSocket部分功能解析

想要全部了解CocoaAsyncSocket的代码实在是一件太过于繁重的工作=。=毕竟代码量放在那里,尝试着看一下其源码,了解底层socket API的调用,从某个具体的功能开始一个个来看吧

首先,GCDAsyncSocket中定义了几个类,用来包装数据以及缓存等功能

1
@interface GCDAsyncSocketPreBuffer : NSObject

Property

这个类定义了很多属性,先大致看看,后面用到的时候会详细说到。

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
uint32_t flags;
uint16_t config;
// 一个socket实例的delegate
__weak id<GCDAsyncSocketDelegate> delegate;
// 处理回调的queue
dispatch_queue_t delegateQueue;
// 本地的IPV4 socket
int socket4FD;
// 本地的IPV6 socket
int socket6FD;
// unix域的 socket
int socketUN;
// 服务器端 url
NSURL *socketUrl;
int stateIndex;
// 对应的IPV4,IPV6,UNIX地址
NSData * connectInterface4;
NSData * connectInterface6;
NSData * connectInterfaceUN;
// 对这个类的socket操作全部在这个串行队列中进行
dispatch_queue_t socketQueue;
dispatch_source_t accept4Source;
dispatch_source_t accept6Source;
dispatch_source_t acceptUNSource;
dispatch_source_t connectTimer;
dispatch_source_t readSource;
dispatch_source_t writeSource;
dispatch_source_t readTimer;
dispatch_source_t writeTimer;
// read/write数据的数组
NSMutableArray *readQueue;
NSMutableArray *writeQueue;
// 当前正在进行的read/write包
GCDAsyncReadPacket *currentRead;
GCDAsyncWritePacket *currentWrite;
// 当前时刻未获取的数据大小
unsigned long socketFDBytesAvailable;
// 缓冲区,未来得及处理的数据放在这里
GCDAsyncSocketPreBuffer *preBuffer;
#if TARGET_OS_IPHONE
// 读入和写入的数据流
CFStreamClientContext streamContext;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
#endif
// 用来做SSL认证的部分
SSLContextRef sslContext;
GCDAsyncSocketPreBuffer *sslPreBuffer;
size_t sslWriteCachedLength;
OSStatus sslErrCode;
OSStatus lastSSLHandshakeError;
void *IsOnSocketQueueOrTargetQueueKey;
id userData;
// 连接备选服务器的延时
NSTimeInterval alternateAddressDelay;

初始化

下面是初始化一个socket的方法,如果初始化的时候不赋予需要的delegate和delegateQueue,那么在最终调用的方法中也会默认为其赋值。

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
- (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<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
if((self = [super init]))
{
delegate = aDelegate;
delegateQueue = dq;
// 对6.0的适配,如果6.0以上是不需要GCD调用MRC的
#if !OS_OBJECT_USE_OBJC
if (dq) dispatch_retain(dq);
#endif
// 各个变量初始化,先全部设定为无效
socket4FD = SOCKET_NULL;
socket6FD = SOCKET_NULL;
socketUN = SOCKET_NULL;
socketUrl = nil;
stateIndex = 0;
if (sq)
{
// socketQueue不能是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.");
socketQueue = sq;
#if !OS_OBJECT_USE_OBJC
dispatch_retain(sq);
#endif
}
else
{
// 没有sq就创建一个串行队列
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;
// 这里的注释大概就是把一个指针变成二级指针,可能是为了省略&操作符,不是很明白这个意义
IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
void *nonNullUnusedPointer = (__bridge void *)self;
// 当前队列里设置标识符,有对应的取出的函数
dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
// read/write队列的最大容量都是5个
readQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentRead = nil;
writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentWrite = nil;
// preBuffer设置为4KB
preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
alternateAddressDelay = 0.3;
}
return self;
}

基本作用都写在注释里了,大部分也就是一些参数初始化,并分配delegate和delegateQueue。需要注意的一点是,socket初始化中有三个整型变量分别是socket4FD,socket6FD和socketUN,它们初始化的时候都设置为SOCKET_NULL。前两者代表socket对IPV4,IPV6类型的地址的解析,后面的UnixSocket是一种从socket框架下发展出来的进程间通信机制,也就是说在同一个主机上进程间通信,使用UNIX Domain Socket是更有效率的。
因为它不需要经过网络协议栈,本质是默认可靠通信的基础下设计的,那么自然在本机上就会比其他方法快很多。
初始化的时候会给socket分配一个socketQueue(如果没有赋值则使用默认生成方法),这个queue是串行队列,由于它负责所有对socket和其内部数据的操作,所以必须让它是串行的,才能保证整个实例运行时的安全性。
后面的比较好理解,一个是read/write的数组,每次有新的读写任务就追加到数组后面,等待何时的时间执行;而preBuffer用来保存操作的数据,比如从服务器端接收来的数据,都是先缓存在这里,然后到一定的时机才取出来和先前读取的数据拼凑在一起。

以上是初始化的部分

Connect

真的不会有人拿iOS来做服务器吧=。=所以这里反正只考虑从客户端的角度来解析各个部分的方法,那么客户端连接的最开始当然就是connect指令了:

connect指令

上面是所有和connect功能的方法,我们从调用的角度来一步步看
连接服务器的方法是:

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

最后实际上是调用这个方法:

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
- (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
// copy保证不会再被修改
NSString *host = [inHost copy];
NSString *interface = [inInterface copy];
// 两个__block变量,方便在block中调用
__block BOOL result = NO;
__block NSError *preConnectErr = nil;
// 放在autoreleasepool中,由系统来决定什么时候释放资源
dispatch_block_t block = ^{ @autoreleasepool {
// Check for problems with host parameter
// 对host进行检测
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地址设置好,有问题就直接return了
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.
// 上面都是参数合法性的判断,现在后面的才是真正的connect流程
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.
// This way the asynchronous lookup block below doesn't have to worry about it changing.
// 翻译一下就是很可能服务端的参数是一个可变字符串,所以这里采取copy的方法,在这个block中同步执行,下面采取的异步查找就不会出现字符串改变的情况了
NSString *hostCpy = [host copy];
// 获取状态stateIndex
int aStateIndex = stateIndex;
__weak GCDAsyncSocket *weakSelf = self;
// 获取全局队列,并且异步执行操作
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"
// 获取server的地址数据,包含IPV4,IPV6
NSError *lookupErr = nil;
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
// strong self,由于是异步执行,有可能面临执行时self = nil的问题,所以这里触发循环引用保证执行的时候self一定存在,而当block执行完了之后放入autoreleasepool,会在该释放的时候自然释放的,不会有问题
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
if (lookupErr)
{
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
// 如果有错误,那么就进行一些数据清空等操作
[strongSelf lookup:aStateIndex didFail:lookupErr];
}});
}
else
{
// 正常情况
NSData *address4 = nil;
NSData *address6 = nil;
// 遍历刚才获取的address数组
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
}});
// 设置超时时间,这个方法创建一个系统时钟,由GCDAsyncSocket本身来管理,如果超时了就调用方法终端dispatch queue中的操作,并以connect失败收尾
[self startConnectTimeout:timeout];
result = YES;
}};
// 在自己的socketqueue中执行block操作
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
if (errPtr) *errPtr = preConnectErr;
return result;
}

基本上就是定义了一个block,然后交给socketQueue中串行执行,具体的关键方法的作用已经注释在代码中,但是我们还是看一看一些方法细节上的实现:

接口检查

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
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
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;
}
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;
}
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;
}
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
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)
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
return YES;
}

代码上都有作者的注释,每一段的功能其实很清晰,先判断delegate和delegateQueue的合法性,然后判断是不是在非连接状态(已经connect就没有调用前置判断的意义了),是否支持IPV4,IPV6的位运算,如果interface存在,得到本机的IPV4,IPV6地址并且赋值目标变量,最后清空queue中的读写请求。
这个interface参数并不是一定会有的,也可能传入的是nil,那么后面的一些操作就直接跳过。inrerface的含义是本机的IP+端口号,本来是默认的localhost并且由系统自取一个空闲的端口,但是也可以用户自己设置,不过=。=这样反倒是有可能导致被另歪线程占用的端口被调用,容易出事。一般情况下都是直接选择让系统自己决定调用的端口号的。

不过万一需要自己设定的情况下,后面会调用什么操作呢?会调用一个方法通过这个interface的描述来得到IPV4,IPV6地址,并且以oc能够识别的方式包装起来。注意这里interfaceAddre4Ptr和interfaceAddr6Ptr其实算是返回值,作用和引用参数一样。

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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
address6:(NSMutableData **)interfaceAddr6Ptr
fromDescription:(NSString *)interfaceDescription
port:(uint16_t)port
{
NSMutableData *addr4 = nil;
NSMutableData *addr6 = nil;
NSString *interface = nil;
// 按“:”来拆分字符串description
NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
if ([components count] > 0)
{
NSString *temp = [components objectAtIndex:0];
if ([temp length] > 0)
{
interface = temp;
}
}
// 调用strtol函数来转换字符串,根据base参数来决定转换后成为long型变量component的位制
if ([components count] > 1 && port == 0)
{
long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
// 端口号最大是65535
if (portL > 0 && portL <= UINT16_MAX)
{
port = (uint16_t)portL;
}
}
// 如果interface为空,创建一个0x00000000
if (interface == nil)
{
// ANY address
// memset就不用说了吧,往内存中赋值,这里把结构体sockaddr4置空
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
// 分别是结构体长度,addressFanily(这里是IPV4),端口号和32位IPV4地址
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
// 和上面一样,这里是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_any;
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
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
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是同名的才能给端口号
else
{
const char *iface = [interface UTF8String];
// 结构体指针指向本地IP
struct ifaddrs *addrs;
const struct ifaddrs *cursor;
// 获取本地IP存放到addrs变量中,0代表成功
if ((getifaddrs(&addrs) == 0))
{
cursor = addrs;
while (cursor != NULL)
{
if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
{
// IPv4
struct sockaddr_in nativeAddr4;
memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));
// 取出描述字节,如果字符串相同则赋值给port
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
nativeAddr4.sin_port = htons(port);
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
// 如果本地IP和interface的描述不相同,转换十进制再比较一遍
else
{
char ip[INET_ADDRSTRLEN];
const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
nativeAddr4.sin_port = htons(port);
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
}
}
// 上面是IPV4,这里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)];
}
}
}
cursor = cursor->ifa_next;
}
freeifaddrs(addrs);
}
}
// 把IPV4,IPV6地址赋值给参数
if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;
}

这个方法里主要都是C语言的函数调用,毕竟socket的API这么底层,不能用OC直接操控了。这些结构体重要的成员变量基本也就看名字就明白是啥意思了,这里就不再赘述。简单藐视这个方法就是在各种情况下顺利获取IPV4,IPV6地址。

有一些网络知识可能需要提及一下:
客户端不会出现interface为空的情况,一般是在服务器端将本机地址绑定为0,用来侦听自己各个端口上是否在接受数据;loopback是一个虚拟地址,是路由器的标志;极端情况下如果interface的地址是其他地址,需要和本机地址或者IP进行对比,如果都不相同,其实是啥都不干的。

回过头来接着看connectToHost方法,里面还有一个重要操作:

*addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];```
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
获取服务器端的地址数组,然后在socketQueue中异步调用``` [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 ```来发起连接。
具体的获取服务器端的地址数据也是和上面一个方法一样,多是调用底层socketAPI,代码太长这里就不全部复制粘贴了,说一下重要的部分:
```objective-c
+ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr{
/* 如果是localhost或loopback
正常创建sockaddr_in,sockaddr_in6两种机构体实例来存放数据,
参数也是默认的方法调用 */
//如果不是
NSString *portStr = [NSString stringWithFormat:@"%hu", port];
// 这里是三个addrinfo,是一种sockaddr结构体的链表
struct addrinfo hints, *res, *res0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// 获取地址信息
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
if (gai_error)
{
error = [self gaiError:gai_error];
}
else
{
// 正确获取之后遍历res0,最后把IPV4/IPV6的总数统计出来并创建对应大小的数组
NSUInteger capacity = 0;
for (res = res0; res; res = res->ai_next)
{
if (res->ai_family == AF_INET || res->ai_family == AF_INET6) {
capacity++;
}
}
addresses = [NSMutableArray arrayWithCapacity:capacity];
// 重新遍历,如果是IPV4地址则加入数组中,如果是IPV6,拿到port进行转换后加入数组中
for (res = res0; res; res = res->ai_next)
{
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;
in_port_t *portPtr = &sockaddr->sin6_port;
if ((portPtr != NULL) && (*portPtr == 0)) {
*portPtr = htons(port);
}
NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
[addresses addObject:address6];
}
}
freeaddrinfo(res0);
if ([addresses count] == 0)
{
error = [self gaiError:EAI_FAIL];
}
}
}
}

如果host为localhost或者loopback,则和先前本机地址的方法一样生成IPV4/IPV6地址,并保存在数组中,如果不是,则根据host和port来创建地址。调用的是C的方法

getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );``` 其中hostname代表ip,service代表port,然后把地址信息传递到result中,hints参数是一个配置参数可以为空。addrinfo是一个链表结构体,其next指向链表中的下一个节点,遍历这个链表可以得到所有的IPV4,IPV6地址,并保存在NSData变量中并装入数组里。
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
然后是刚才说到的放入到socketQueue中执行的最终的连接方法:
```objective-c
- (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;
// 如果IPV4禁用而且IPV6地址为空,报错
if (isIPv4Disabled && (address6 == nil))
{
NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
[self closeWithError:[self otherError:msg]];
return;
}
// 如果IPV6禁用,IPV4地址为空,禁用
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];
}
}

这个很通俗易懂,反正就是对地址进行判断,如果合适就直接连接。唯一需要说明的是

stateIndex```,前者是用参数传递过来的,而stateIndex是用属性传递过来的,有可能中途状态发生了变化,比如断开了连接等,那么这样stateIndex就会发生改变,和前者不能相等了,直接中断操作。然后调用另一个更深层的连接方法;
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
```objective-c
// 最终进行连接的第二层方法,调用两个server地址(IPV4,IPV6)去连接
- (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连接socket4FD
if (address4)
{
LogVerbose(@"Creating IPv4 socket");
socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr];
}
// 同样,创建IPV6连接socket6FD
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和主副address
int socketFD, alternateSocketFD;
NSData *address, *alternateAddress;
// 偏好IPV6的情况下主socketFD和主地址全部选IPV6相关
if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL)
{
socketFD = socket6FD;
alternateSocketFD = socket4FD;
address = address6;
alternateAddress = address4;
}
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;
}

结果我们可以看到,这个也不是建立连接最主要的部分,也只是起到了从IPV4,IPV6地址获取到socketFD的过渡功能。这里创建的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
// 正式连接的第三步:创建socket
- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr
{
/* 底层真正创建socket的函数,返回的socket句柄就是整型变量,第一个参数family表示到底是IPV4还是IPV6,第二个参数表示socket类型,到底是数据流还是数据报文(stream或者gram),最后一个参数是协议,0为系统默认,如果是stream则是TCP,如果是gram则是UDP */
int socketFD = socket(family, SOCK_STREAM, 0);
if (socketFD == SOCKET_NULL)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
return socketFD;
}
// 创建成功的话和本地地址绑定,绑定失败就关闭socket
if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
{
[self closeSocket:socketFD];
return SOCKET_NULL;
}
// Prevent SIGPIPE signals
int nosigpipe = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
return socketFD;
}

可以看到创建socket也是调用的iOS底层的socket接口,很简单易懂,有一点要提一下的就是这个方法最后的SIGPIPE:
简单描述一下就是SIGPIPE就是在一个socket通信中,如果一端向另一个已经关闭的端口发送两次write,第二次的write将会生成SIGPIPE信号,直接结束进程。一般不想直接遇到这种情况就结束进程,那么就得想办法不生成SIGPIPE信号或者忽视SIGPIPE信号。
iOS中可以很方便地处理这个SIGPIPE问题,就是如这个方法最后调用的setsockpot函数,可以直接更改socket的设置,我们看到这里设定了SO_NOSIGPIPE,意味着不会触发SIGPIPE信号量,这样就不会中途结束进程了。具体的socket通信中信号的问题请参考更详细的资料。
总结一下这个方法做了三件事:1.创建socket 2.将socket和本地地址绑定 3.禁用SIGPIPE信号量防止进程中断
到了这里,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
31
32
33
34
35
// 绑定一个socket到本地地址
- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr
{
// Bind the socket to the desired interface (if needed)
if (connectInterface)
{
LogVerbose(@"Binding socket...");
// 需要绑定的port得大于0
if ([[self class] portFromAddress:connectInterface] > 0)
{
// Since we're going to be binding to a specific port,
// we should turn on reuseaddr to allow us to override sockets in time_wait.
int reuseOn = 1;
// 这里的设定是,即使socket调用了close也不会马上清除这个socket线程,而是会在一定的时间内等待是不是可以再次重用
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
}
const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes];
// 绑定出错的处理
int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
if (result != 0)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in bind() function"];
return NO;
}
}
return YES;
}

可以看到这个方法也挺简单易懂,就是调用socket自己的bind函数,三个参数分别为socket,绑定地址以及地址大小。有多的一点就是设置了socket的复用。
上面进行了这么多的准备工作,我们拿到了本机地址,通过IPV4/IPV6地址建立了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
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
// 真的是最终的连接方法:
- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
{
// If there already is a socket connected, we close socketFD and return
if (self.isConnected)
{
[self closeSocket:socketFD];
return;
}
// Start the connection process in a background queue
__weak GCDAsyncSocket *weakSelf = self;
// 全局queue
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
// 调用connect函数,它会阻塞线程,所以这里是异步线程中执行,客户端向服务器发送连接请求
int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
// 如果self还存在的话。。
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
// 在socketQueue中开启异步线程执行
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
// 如果状态是已经连接,就关闭直接返回
if (strongSelf.isConnected)
{
[strongSelf closeSocket:socketFD];
return_from_block;
}
// result == 0说明连接成功,-1是失败
if (result == 0)
{
// 由于我们是用ipv4,ipv6两个方案来创建socket,一旦创建成功,就把另一个多余的给关闭
[self closeUnusedSocket:socketFD];
// 调用连接成功后的后续操作
[strongSelf didConnect:aStateIndex];
}
else
{
// 连接失败,各种错误回报
[strongSelf closeSocket:socketFD];
// If there are no more sockets trying to connect, we inform the error to the delegate
if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL)
{
NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"];
[strongSelf didNotConnect:aStateIndex error:error];
}
}
}});
#pragma clang diagnostic pop
});
LogVerbose(@"Connecting...");
}

其实也就是调用了socket的connect函数,这个函数式阻塞的,会让其执行的线程一直等待到有结果,所以肯定要使用全局并发队列,然后再新的线程中进行操作,得到结果后回到socketQueue进行后续的操作。
后面其实还有一些关于连接成功后source,stream的初始化操作,但是限于篇幅就不在这里展开说明了。后面说到I/O的内容的时候再来细致讨论吧。
总体而言CocoaAsyncSocket这个框架是在iOS自带的socket API的基础上做了数据包装和通信错误验证的各种操作,其主要的操作基本上都在socketQueue中执行,根据不同情况选择是同步还是异步线程执行。
后续能说的还很多,这里说了这么多只是包装过的socket中connect的部分操作,解读起来已经非常耗时了,不得不佩服大神的技术水平。所以说学习大神的代码真是让人受益匪浅。

参考

1.涂耀辉 iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)

2.CocoaAsyncSocket源码

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器