iOS即时通讯之CocoaAsyncSocket源码解析一
申明:本文内容属于转载整理,原文连接
前言:
CocoaAsyncSocket是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的、强大的异步套接字库,向上封装出简单易用OC接口。省去了我们面Socket向Socket以及数据流Stream等繁琐复杂的编程。
本文为一个系列,旨在让大家了解CocoaAsyncSocket是如何基于底层进行封装、工作的。
注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释
如果对该框架用法不熟悉的话,可以参考楼主之前这篇文章:iOS即时通讯,从入门到“放弃”?,或者自行查阅。
首先我们来看看框架的结构图:
整个库就这么两个类,一个基于TCP,一个基于UDP。其中基于TCP的GCDAsyncSocket,大概8000多行代码。而GCDAsyncUdpSocket稍微少一点,也有5000多行。
所以单纯从代码量上来看,这个库还是做了很多事的。
顺便提一下,之前这个框架还有一个runloop版的,不过因为功能重叠和其它种种原因,后续版本便废弃了,现在仅有GCD
版本。
本系列我们将重点来讲GCDAsyncSocket这个类。
1 @implementation GCDAsyncSocket 2 { 3 //flags,当前正在做操作的标识符 4 uint32_t flags; 5 uint16_t config; 6 7 //代理 8 __weak id<GCDAsyncSocketDelegate> delegate; 9 //代理回调的queue 10 dispatch_queue_t delegateQueue; 11 12 //本地IPV4Socket 13 int socket4FD; 14 //本地IPV6Socket 15 int socket6FD; 16 //unix域的套接字 17 int socketUN; 18 //unix域 服务端 url 19 NSURL *socketUrl; 20 //状态Index 21 int stateIndex; 22 23 //本机的IPV4地址 24 NSData * connectInterface4; 25 //本机的IPV6地址 26 NSData * connectInterface6; 27 //本机unix域地址 28 NSData * connectInterfaceUN; 29 30 //这个类的对Socket的操作都在这个queue中,串行 31 dispatch_queue_t socketQueue; 32 33 dispatch_source_t accept4Source; 34 dispatch_source_t accept6Source; 35 dispatch_source_t acceptUNSource; 36 37 //连接timer,GCD定时器 38 dispatch_source_t connectTimer; 39 dispatch_source_t readSource; 40 dispatch_source_t writeSource; 41 dispatch_source_t readTimer; 42 dispatch_source_t writeTimer; 43 44 //读写数据包数组 类似queue,最大限制为5个包 45 NSMutableArray *readQueue; 46 NSMutableArray *writeQueue; 47 48 //当前正在读写数据包 49 GCDAsyncReadPacket *currentRead; 50 GCDAsyncWritePacket *currentWrite; 51 //当前socket未获取完的数据大小 52 unsigned long socketFDBytesAvailable; 53 54 //全局公用的提前缓冲区 55 GCDAsyncSocketPreBuffer *preBuffer; 56 57 #if TARGET_OS_IPHONE 58 CFStreamClientContext streamContext; 59 //读的数据流 60 CFReadStreamRef readStream; 61 //写的数据流 62 CFWriteStreamRef writeStream; 63 #endif 64 //SSL上下文,用来做SSL认证 65 SSLContextRef sslContext; 66 67 //全局公用的SSL的提前缓冲区 68 GCDAsyncSocketPreBuffer *sslPreBuffer; 69 size_t sslWriteCachedLength; 70 71 //记录SSL读取数据错误 72 OSStatus sslErrCode; 73 //记录SSL握手的错误 74 OSStatus lastSSLHandshakeError; 75 76 //socket队列的标识key 77 void *IsOnSocketQueueOrTargetQueueKey; 78 79 id userData; 80 81 //连接备选服务端地址的延时 (另一个IPV4或IPV6) 82 NSTimeInterval alternateAddressDelay; 83 }
这个里定义了一些属性,可以先简单看看注释,这里我们仅仅先暂时列出来,给大家混个眼熟。
在接下来的代码中,会大量穿插着这些属性的使用。所以大家不用觉得困惑,具体作用,我们后面会一一讲清楚的。
接着我们来看看本文方法一--初始化方法:
1 //层级调用 2 - (id)init 3 { 4 return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; 5 } 6 7 - (id)initWithSocketQueue:(dispatch_queue_t)sq 8 { 9 return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; 10 } 11 12 - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq 13 { 14 return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; 15 } 16 17 - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq 18 { 19 if((self = [super init])) 20 { 21 delegate = aDelegate; 22 delegateQueue = dq; 23 24 //这个宏是在sdk6.0之后才有的,如果是之前的,则OS_OBJECT_USE_OBJC为0,!0即执行if语句 25 //对6.0的适配,如果是6.0以下,则去retain release,6.0之后ARC也管理了GCD 26 #if !OS_OBJECT_USE_OBJC 27 28 if (dq) dispatch_retain(dq); 29 #endif 30 31 //创建socket,先都置为 -1 32 //本机的ipv4 33 socket4FD = SOCKET_NULL; 34 //ipv6 35 socket6FD = SOCKET_NULL; 36 //应该是UnixSocket 37 socketUN = SOCKET_NULL; 38 //url 39 socketUrl = nil; 40 //状态 41 stateIndex = 0; 42 43 if (sq) 44 { 45 //如果scoketQueue是global的,则报错。断言必须要一个非并行queue。 46 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), 47 @"The given socketQueue parameter must not be a concurrent queue."); 48 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), 49 @"The given socketQueue parameter must not be a concurrent queue."); 50 NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 51 @"The given socketQueue parameter must not be a concurrent queue."); 52 //拿到scoketQueue 53 socketQueue = sq; 54 //iOS6之下retain 55 #if !OS_OBJECT_USE_OBJC 56 dispatch_retain(sq); 57 #endif 58 } 59 else 60 { 61 //没有的话创建一个, 名字为:GCDAsyncSocket,串行 62 socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); 63 } 64 65 // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. 66 // From the documentation: 67 // 68 // > Keys are only compared as pointers and are never dereferenced. 69 // > Thus, you can use a pointer to a static variable for a specific subsystem or 70 // > any other value that allows you to identify the value uniquely. 71 // 72 // We're just going to use the memory address of an ivar. 73 // Specifically an ivar that is explicitly named for our purpose to make the code more readable. 74 // 75 // However, it feels tedious (and less readable) to include the "&" all the time: 76 // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) 77 // 78 // So we're going to make it so it doesn't matter if we use the '&' or not, 79 // by assigning the value of the ivar to the address of the ivar. 80 // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; 81 82 83 //比如原来为 0X123 -> NULL 变成 0X222->0X123->NULL 84 //自己的指针等于自己原来的指针,成二级指针了 看了注释是为了以后省略&,让代码更可读? 85 IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; 86 87 88 void *nonNullUnusedPointer = (__bridge void *)self; 89 90 //dispatch_queue_set_specific给当前队里加一个标识 dispatch_get_specific当前线程取出这个标识,判断是不是在这个队列 91 //这个key的值其实就是一个一级指针的地址 ,第三个参数把自己传过去了,上下文对象?第4个参数,为销毁的时候用的,可以指定一个函数 92 dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); 93 //读的数组 限制为5 94 readQueue = [[NSMutableArray alloc] initWithCapacity:5]; 95 currentRead = nil; 96 97 //写的数组,限制5 98 writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; 99 currentWrite = nil; 100 101 //设置大小为 4kb 102 preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; 103 104 #pragma mark alternateAddressDelay?? 105 //交替地址延时?? wtf 106 alternateAddressDelay = 0.3; 107 } 108 return self; 109 }
详细的细节可以看看注释,这里初始化了一些属性:
1.代理、以及代理queue的赋值。
2.本机socket的初始化:包括下面3种
//本机的ipv4 socket4FD = SOCKET_NULL; //ipv6 socket6FD = SOCKET_NULL; //UnixSocket socketUN = SOCKET_NULL;
其中值得一提的是第三种:UnixSocket,这个是用于Unix Domin Socket通信用的。
那么什么是Unix Domin Socket呢?
原来它是在socket的框架上发展出一种IPC(进程间通信)机制,虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC 更有效率 :
- 不需要经过网络协议栈
- 不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
基本上它是当今应用于IPC最主流的方式。至于它到底和普通的socket通信实现起来有什么区别,别着急,我们接着往下看。
3.生成了一个socketQueue,这个queue是串行的,接下来我们看代码就会知道它贯穿于这个类的所有地方。所有对socket以及一些内部数据的相关操作,都需要在这个串行queue中进行。这样使得整个类没有加一个锁,就保证了整个类的线程安全。
4.创建了两个读写队列(本质数组),接下来我们所有的读写任务,都会先追加在这个队列最后,然后每次取出队列中最前面的任务,进行处理。
5.创建了一个全局的数据缓冲区:preBuffer,我们所操作的数据,大部分都是要先存入这个preBuffer中,然后再从preBuffer取出进行处理的。
6.初始化了一个交替延时变量:alternateAddressDelay,这个变量先简单的理解下:就是进行另一个服务端地址请求的延时。后面我们一讲到,大家就明白了。
初始化方法就到此为止了。
接着我们有socket了,我们如果是客户端,就需要去connet服务器。
又或者我们是服务端的话,就需要去bind端口,并且accept,等待客户端的连接。(基本上也没有用iOS来做服务端的吧...)
这里我们先作为客户端来看看connect
:
其中和connect相关的方法就这么多,我们一般这么来连接到服务端:
[socket connectToHost:Khost onPort:Kport error:nil];
也就是我们在截图中选中的方法,那我们就从这个方法作为起点,开始讲起吧。
本文方法二--connect总方法
1 /逐级调用 2 - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr 3 { 4 return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; 5 } 6 7 - (BOOL)connectToHost:(NSString *)host 8 onPort:(uint16_t)port 9 withTimeout:(NSTimeInterval)timeout 10 error:(NSError **)errPtr 11 { 12 return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; 13 } 14 15 //多一个inInterface,本机地址 16 - (BOOL)connectToHost:(NSString *)inHost 17 onPort:(uint16_t)port 18 viaInterface:(NSString *)inInterface 19 withTimeout:(NSTimeInterval)timeout 20 error:(NSError **)errPtr 21 { 22 //{} 跟踪当前行为 23 LogTrace(); 24 25 // Just in case immutable objects were passed 26 //拿到host ,copy防止值被修改 27 NSString *host = [inHost copy]; 28 //interface?接口? 29 NSString *interface = [inInterface copy]; 30 31 //声明两个__block的 32 __block BOOL result = NO; 33 //error信息 34 __block NSError *preConnectErr = nil; 35 36 //gcdBlock ,都包裹在自动释放池中 37 dispatch_block_t block = ^{ @autoreleasepool { 38 39 // Check for problems with host parameter 40 41 if ([host length] == 0) 42 { 43 NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; 44 preConnectErr = [self badParamError:msg]; 45 46 //其实就是return,大牛的代码真是充满逼格 47 return_from_block; 48 } 49 50 // Run through standard pre-connect checks 51 //一个前置的检查,如果没通过返回,这个检查里,如果interface有值,则会将本机的IPV4 IPV6的 address设置上。 52 if (![self preConnectWithInterface:interface error:&preConnectErr]) 53 { 54 return_from_block; 55 } 56 57 // We've made it past all the checks. 58 // It's time to start the connection process. 59 //flags 做或等运算。 flags标识为开始Socket连接 60 flags |= kSocketStarted; 61 62 //又是一个{}? 只是为了标记么? 63 LogVerbose(@"Dispatching DNS lookup..."); 64 65 // It's possible that the given host parameter is actually a NSMutableString. 66 //很可能给我们的服务端的参数是一个可变字符串 67 // So we want to copy it now, within this block that will be executed synchronously. 68 //所以我们需要copy,在Block里同步的执行 69 // This way the asynchronous lookup block below doesn't have to worry about it changing. 70 //这种基于Block的异步查找,不需要担心它被改变 71 72 //copy,防止改变 73 NSString *hostCpy = [host copy]; 74 75 //拿到状态 76 int aStateIndex = stateIndex; 77 __weak GCDAsyncSocket *weakSelf = self; 78 79 //全局Queue 80 dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 81 //异步执行 82 dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { 83 //忽视循环引用 84 #pragma clang diagnostic push 85 #pragma clang diagnostic warning "-Wimplicit-retain-self" 86 87 //查找错误 88 NSError *lookupErr = nil; 89 //server地址数组(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in类型) 90 NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; 91 92 //strongSelf 93 __strong GCDAsyncSocket *strongSelf = weakSelf; 94 95 //完整Block安全形态,在加个if 96 if (strongSelf == nil) return_from_block; 97 98 //如果有错 99 if (lookupErr) 100 { 101 //用cocketQueue 102 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { 103 //一些错误处理,清空一些数据等等 104 [strongSelf lookup:aStateIndex didFail:lookupErr]; 105 }}); 106 } 107 //正常 108 else 109 { 110 111 NSData *address4 = nil; 112 NSData *address6 = nil; 113 //遍历地址数组 114 for (NSData *address in addresses) 115 { 116 //判断address4不为空,且address为IPV4 117 if (!address4 && [[self class] isIPv4Address:address]) 118 { 119 address4 = address; 120 } 121 //判断address6不为空,且address为IPV6 122 else if (!address6 && [[self class] isIPv6Address:address]) 123 { 124 address6 = address; 125 } 126 } 127 //异步去发起连接 128 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { 129 130 [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; 131 }}); 132 } 133 134 #pragma clang diagnostic pop 135 }}); 136 137 138 //开启连接超时 139 [self startConnectTimeout:timeout]; 140 141 result = YES; 142 }}; 143 //在socketQueue中执行这个Block 144 if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) 145 block(); 146 //否则同步的调起这个queue去执行 147 else 148 dispatch_sync(socketQueue, block); 149 150 //如果有错误,赋值错误 151 if (errPtr) *errPtr = preConnectErr; 152 //把连接是否成功的result返回 153 return result; 154 }
这个方法非常长,它主要做了以下几件事:
-
首先我们需要说一下的是,整个类大量的会出现
LogTrace()
类似这样的宏,我们点进去发现它的本质只是一个{},什么事都没做。原来这些宏是为了追踪当前执行的流程用的,它被定义在一个大的
#if #else
中:
#ifndef GCDAsyncSocketLoggingEnabled #define GCDAsyncSocketLoggingEnabled 0 #endif #if GCDAsyncSocketLoggingEnabled // Logging Enabled - See log level below // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack // // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" #define LogAsync YES #define LogContext GCDAsyncSocketLoggingContext #define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) #define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) #ifndef GCDAsyncSocketLogLevel #define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE #endif // Log levels : off, error, warn, info, verbose static const int logLevel = GCDAsyncSocketLogLevel; #else // Logging Disabled #define LogError(frmt, ...) {} #define LogWarn(frmt, ...) {} #define LogInfo(frmt, ...) {} #define LogVerbose(frmt, ...) {} #define LogCError(frmt, ...) {} #define LogCWarn(frmt, ...) {} #define LogCInfo(frmt, ...) {} #define LogCVerbose(frmt, ...) {} #define LogTrace() {} #define LogCTrace(frmt, ...) {} #endif
而此时因为GCDAsyncSocketLoggingEnabled默认为0,所以仅仅是一个{}。当标记为1时,这些宏就可以用来输出我们当前的业务流程,极大的方便了我们的调试过程。
- 接着我们回到正题上,我们定义了一个
Block
,所有的连接操作都被包裹在这个Block
中。我们做了如下判断:
//在socketQueue中执行这个Block if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); //否则同步的调起这个queue去执行 else dispatch_sync(socketQueue, block);
保证这个连接操作一定是在我们的socketQueue中,而且还是以串行同步的形式去执行,规避了线程安全的问题。
-
接着把Block中连接过程产生的错误进行赋值,并且把连接的结果返回出去
//如果有错误,赋值错误 if (errPtr) *errPtr = preConnectErr; //把连接是否成功的result返回 return result;
接着来看这个方法声明的Block内部,也就是进行连接的真正主题操作,这个连接过程将会调用许多函数,一环扣一环,我会尽可能用最清晰、详尽的语言来描述...
1.这个Block首先做了一些错误的判断,并调用了一些错误生成的方法。类似:
1 if ([host length] == 0) 2 { 3 NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; 4 preConnectErr = [self badParamError:msg]; 5 6 //其实就是return,大牛的代码真是充满逼格 7 return_from_block; 8 } 9 //用该字符串生成一个错误,错误的域名,错误的参数 10 - (NSError *)badParamError:(NSString *)errMsg 11 { 12 NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; 13 14 return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; 15 }
2.接着做了一个前置的错误检查:
if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; }
这个检查方法,如果没通过返回NO。并且如果interface有值,则会将本机的IPV4 IPV6的 address设置上。即我们之前提到的这两个属性:
//本机的IPV4地址 NSData * connectInterface4; //本机的IPV6地址 NSData * connectInterface6;
我们来看看这个前置检查方法:
本文方法三--前置检查方法
1 //在连接之前的接口检查,一般我们传nil interface本机的IP 端口等等 2 - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr 3 { 4 //先断言,如果当前的queue不是初始化quueue,直接报错 5 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 6 7 //无代理 8 if (delegate == nil) // Must have delegate set 9 { 10 if (errPtr) 11 { 12 NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; 13 *errPtr = [self badConfigError:msg]; 14 } 15 return NO; 16 } 17 //没有代理queue 18 if (delegateQueue == NULL) // Must have delegate queue set 19 { 20 if (errPtr) 21 { 22 NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; 23 *errPtr = [self badConfigError:msg]; 24 } 25 return NO; 26 } 27 28 //当前不是非连接状态 29 if (![self isDisconnected]) // Must be disconnected 30 { 31 if (errPtr) 32 { 33 NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; 34 *errPtr = [self badConfigError:msg]; 35 } 36 return NO; 37 } 38 39 //判断是否支持IPV4 IPV6 &位与运算,因为枚举是用 左位移<<运算定义的,所以可以用来判断 config包不包含某个枚举。因为一个值可能包含好几个枚举值,所以这时候不能用==来判断,只能用&来判断 40 BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; 41 BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; 42 43 //是否都不支持 44 if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled 45 { 46 if (errPtr) 47 { 48 NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; 49 *errPtr = [self badConfigError:msg]; 50 } 51 return NO; 52 } 53 54 //如果有interface,本机地址 55 if (interface) 56 { 57 NSMutableData *interface4 = nil; 58 NSMutableData *interface6 = nil; 59 60 //得到本机的IPV4 IPV6地址 61 [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; 62 63 //如果两者都为nil 64 if ((interface4 == nil) && (interface6 == nil)) 65 { 66 if (errPtr) 67 { 68 NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; 69 *errPtr = [self badParamError:msg]; 70 } 71 return NO; 72 } 73 74 if (isIPv4Disabled && (interface6 == nil)) 75 { 76 if (errPtr) 77 { 78 NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; 79 *errPtr = [self badParamError:msg]; 80 } 81 return NO; 82 } 83 84 if (isIPv6Disabled && (interface4 == nil)) 85 { 86 if (errPtr) 87 { 88 NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; 89 *errPtr = [self badParamError:msg]; 90 } 91 return NO; 92 } 93 //如果都没问题,则赋值 94 connectInterface4 = interface4; 95 connectInterface6 = interface6; 96 } 97 98 // Clear queues (spurious read/write requests post disconnect) 99 //清除queue(假的读写请求 ,提交断开连接) 100 //读写Queue清除 101 [readQueue removeAllObjects]; 102 [writeQueue removeAllObjects]; 103 104 return YES; 105 }
又是非常长的一个方法,但是这个方法还是非常好读的。
- 主要是对连接前的一个属性参数的判断,如果不齐全的话,则填充错误指针,并且返回NO。
-
在这里如果我们interface这个参数不为空话,我们会额外多执行一些操作。
首先来讲讲这个参数是什么,简单来讲,这个就是我们设置的本机IP+端口号。照理来说我们是不需要去设置这个参数的,默认的为localhost(127.0.0.1)本机地址。而端口号会在本机中取一个空闲可用的端口。
而我们一旦设置了这个参数,就会强制本地IP和端口为我们指定的。其实这样设置反而不好,其实大家也能想明白,这里端口号如果我们写死,万一被其他进程给占用了。那么肯定是无法连接成功的。
所以就有了我们做IM的时候,一般是不会去指定客户端bind某一个端口。而是用系统自动去选择。 -
我们最后清空了当前读写queue中,所有的任务。
至于有interface,我们所做的额外操作是什么呢,我们接下来看看这个方法:
本文方法四--本地地址绑定方法
1 - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr 2 address6:(NSMutableData **)interfaceAddr6Ptr 3 fromDescription:(NSString *)interfaceDescription 4 port:(uint16_t)port 5 { 6 NSMutableData *addr4 = nil; 7 NSMutableData *addr6 = nil; 8 9 NSString *interface = nil; 10 11 //先用:分割 12 NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; 13 if ([components count] > 0) 14 { 15 NSString *temp = [components objectAtIndex:0]; 16 if ([temp length] > 0) 17 { 18 interface = temp; 19 } 20 } 21 if ([components count] > 1 && port == 0) 22 { 23 //拿到port strtol函数,将一个字符串,根据base参数转成长整型,如base值为10则采用10进制,若base值为16则采用16进制 24 long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); 25 //UINT16_MAX,65535最大端口号 26 if (portL > 0 && portL <= UINT16_MAX) 27 { 28 port = (uint16_t)portL; 29 } 30 } 31 32 //为空则自己创建一个 0x00000000 ,全是0 ,为线路地址 33 //如果端口为0 通常用于分析操作系统。这一方法能够工作是因为在一些系统中“0”是无效端口,当你试图使用通常的闭合端口连接它时将产生不同的结果。一种典型的扫描,使用IP地址为0.0.0.0,设置ACK位并在以太网层广播。 34 if (interface == nil) 35 { 36 37 struct sockaddr_in sockaddr4; 38 39 //memset作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 40 41 //memset(void *s,int ch,size_t n);函数,第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小) 42 memset(&sockaddr4, 0, sizeof(sockaddr4)); 43 //结构体长度 44 sockaddr4.sin_len = sizeof(sockaddr4); 45 //addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。 46 sockaddr4.sin_family = AF_INET; 47 //端口号 htons将主机字节顺序转换成网络字节顺序 16位 48 sockaddr4.sin_port = htons(port); 49 //htonl ,将INADDR_ANY:0.0.0.0,不确定地址,或者任意地址 htonl 32位。 也是转为网络字节序 50 51 //ipv4 32位 4个字节 INADDR_ANY,0x00000000 (16进制,一个0代表4位,8个0就是32位) = 4个字节的 52 sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); 53 struct sockaddr_in6 sockaddr6; 54 memset(&sockaddr6, 0, sizeof(sockaddr6)); 55 56 sockaddr6.sin6_len = sizeof(sockaddr6); 57 //ipv6 58 sockaddr6.sin6_family = AF_INET6; 59 //port 60 sockaddr6.sin6_port = htons(port); 61 62 //共128位 63 sockaddr6.sin6_addr = in6addr_any; 64 65 //把这两个结构体转成data 66 addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; 67 addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; 68 } 69 //如果localhost、loopback 回环地址,虚拟地址,路由器工作它就存在。一般用来标识路由器 70 //这两种的话就赋值为127.0.0.1,端口为port 71 else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) 72 { 73 // LOOPBACK address 74 75 //ipv4 76 struct sockaddr_in sockaddr4; 77 memset(&sockaddr4, 0, sizeof(sockaddr4)); 78 79 sockaddr4.sin_len = sizeof(sockaddr4); 80 sockaddr4.sin_family = AF_INET; 81 sockaddr4.sin_port = htons(port); 82 83 //#define INADDR_LOOPBACK (u_int32_t)0x7f000001 84 //7f000001->1111111 00000000 00000000 00000001->127.0.0.1 85 sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 86 87 //ipv6 88 struct sockaddr_in6 sockaddr6; 89 memset(&sockaddr6, 0, sizeof(sockaddr6)); 90 91 sockaddr6.sin6_len = sizeof(sockaddr6); 92 sockaddr6.sin6_family = AF_INET6; 93 sockaddr6.sin6_port = htons(port); 94 95 sockaddr6.sin6_addr = in6addr_loopback; 96 //赋值 97 addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; 98 addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; 99 } 100 //非localhost、loopback,去获取本机IP,看和传进来Interface是同名或者同IP,相同才给赋端口号,把数据封装进Data。否则为nil 101 else 102 { 103 //转成cString 104 const char *iface = [interface UTF8String]; 105 106 //定义结构体指针,这个指针是本地IP 107 struct ifaddrs *addrs; 108 const struct ifaddrs *cursor; 109 110 //获取到本机IP,为0说明成功了 111 if ((getifaddrs(&addrs) == 0)) 112 { 113 //赋值 114 cursor = addrs; 115 //如果IP不为空,则循环链表去设置 116 while (cursor != NULL) 117 { 118 //如果 addr4 IPV4地址为空,而且地址类型为IPV4 119 if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) 120 { 121 // IPv4 122 123 struct sockaddr_in nativeAddr4; 124 //memcpy内存copy函数,把src开始到size的字节数copy到 dest中 125 memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); 126 127 //比较两个字符串是否相同,本机的IP名,和接口interface是否相同 128 if (strcmp(cursor->ifa_name, iface) == 0) 129 { 130 // Name match 131 //相同则赋值 port 132 nativeAddr4.sin_port = htons(port); 133 //用data封号IPV4地址 134 addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; 135 } 136 //本机IP名和interface不相同 137 else 138 { 139 //声明一个IP 16位的数组 140 char ip[INET_ADDRSTRLEN]; 141 142 //这里是转成了10进制。。(因为获取到的是二进制IP) 143 const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); 144 145 //如果conversion不为空,说明转换成功而且 ,比较转换后的IP,和interface是否相同 146 if ((conversion != NULL) && (strcmp(ip, iface) == 0)) 147 { 148 // IP match 149 //相同则赋值 port 150 nativeAddr4.sin_port = htons(port); 151 152 addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; 153 } 154 } 155 } 156 //IPV6 一样 157 else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) 158 { 159 // IPv6 160 161 struct sockaddr_in6 nativeAddr6; 162 memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); 163 164 if (strcmp(cursor->ifa_name, iface) == 0) 165 { 166 // Name match 167 168 nativeAddr6.sin6_port = htons(port); 169 170 addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; 171 } 172 else 173 { 174 char ip[INET6_ADDRSTRLEN]; 175 176 const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); 177 178 if ((conversion != NULL) && (strcmp(ip, iface) == 0)) 179 { 180 // IP match 181 182 nativeAddr6.sin6_port = htons(port); 183 184 addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; 185 } 186 } 187 } 188 189 //指向链表下一个addr 190 cursor = cursor->ifa_next; 191 } 192 //和getifaddrs对应,释放这部分内存 193 freeifaddrs(addrs); 194 } 195 } 196 //如果这两个二级指针存在,则取成一级指针,把addr4赋值给它 197 if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; 198 if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; 199
这个方法中,主要是大量的socket相关的函数的调用,会显得比较难读一点,其实简单来讲就做了这么一件事:
把interface变成进行socket操作所需要的地址结构体,然后把地址结构体包裹在NSMuttableData中。
这里,为了让大家能更容易理解,我把这个方法涉及到的socket
相关函数以及宏(按照调用顺序)都列出来:
//拿到port strtol函数,将一个字符串,根据base参数转成长整型, //如base值为10则采用10进制,若base值为16则采用16进制 long strtol(const char *__str, char **__endptr, int __base); //作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 //第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小) memset(void *s,int ch,size_t n); //最大端口号 #define UINT16_MAX 65535 //作用是把主机字节序转化为网络字节序 htons() //参数16位 htonl() //参数32位 //获取占用内存大小 sizeof() //比较两个指针,是否相同 相同返回0 int strcmp(const char *__s1, const char *__s2) //内存copu函数,把src开始到len的字节数copy到 dest中 memcpy(dest, src, len) //inet_pton和inet_ntop这2个IP地址转换函数,可以在将IP地址在“点分十进制”和“二进制整数”之间转换 //参数socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt); //得到本机地址 extern int getifaddrs(struct ifaddrs **); //释放本机地址 extern void freeifaddrs(struct ifaddrs *);
还有一些用到的作为参数的结构体:
//socket通信用的 IPV4地址结构体 struct sockaddr_in { __uint8_t sin_len; //整个结构体大小 sa_family_t sin_family; //协议族,IPV4?IPV6 in_port_t sin_port; //端口 struct in_addr sin_addr; //IP地址 char sin_zero[8]; //空的占位符,为了和其他地址结构体保持一致大小,方便转化 }; //IPV6地址结构体,和上面的类似 struct sockaddr_in6 { __uint8_t sin6_len; /* length of this struct(sa_family_t) */ sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */ in_port_t sin6_port; /* Transport layer port # (in_port_t) */ __uint32_t sin6_flowinfo; /* IP6 flow information */ struct in6_addr sin6_addr; /* IP6 address */ __uint32_t sin6_scope_id; /* scope zone index */ }; //用来获取本机IP的参数结构体 struct ifaddrs { //指向链表的下一个成员 struct ifaddrs *ifa_next; //接口名称 char *ifa_name; //接口标识位(比如当IFF_BROADCAST或IFF_POINTOPOINT设置到此标识位时,影响联合体变量ifu_broadaddr存储广播地址或ifu_dstaddr记录点对点地址) unsigned int ifa_flags; //接口地址 struct sockaddr *ifa_addr; //存储该接口的子网掩码; struct sockaddr *ifa_netmask; //点对点的地址 struct sockaddr *ifa_dstaddr; //ifa_data存储了该接口协议族的特殊信息,它通常是NULL(一般不关注他)。 void *ifa_data; };
这一段内容算是比较枯涩了,但是也是了解socket编程必经之路。
这里提到了网络字节序和主机字节序。我们创建socket之前,必须把port和host这些参数转化为网络字节序。那么为什么要这么做呢?
不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
这样如果我们到网络中,就无法得知互相的字节序是什么了,所以我们就必须统一一套排序,这样网络字节序就有它存在的必要了。
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关。从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
大家感兴趣可以到这篇文章中去看看:网络字节序与主机字节序。
除此之外比较重要的就是这几个地址结构体了。它定义了我们当前socket的地址信息。包括IP、Port、长度、协议族等等。当然socket中标识为地址的结构体不止这3种,等我们后续代码来补充。
大家了解了我们上述说的知识点,这个方法也就不难度了。这个方法主要是做了本机IPV4
和IPV6
地址的创建和绑定。当然这里分了几种情况:
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:
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];
-
第二步是做一些错误判断,并且把地址信息赋值到address6和address6中去,然后异步调用回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
进行了划分:
- 如果
host
为localhost
或者loopback
,则按照我们之前绑定本机地址那一套生成地址的方式,去生成IPV4和IPV6的地址,并且用NSData包裹住这个地址结构体,装在NSMutableArray中。 -
不是本机地址,那么我们就需要根据host和port去创建地址了,这里用到的是这么一个函数:
-
int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );
这个函数主要的作用是:根据hostname(IP),service(port),去获取地址信息,并且把地址信息传递到result中。
而hints这个参数可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,如果填了,其实它就是一个配置参数,返回的地址信息会和这个配置参数的内容有关,如下例: -
举例来说:指定的服务既可支持TCP也可支持UDP,所以调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
这里我们可以看到result和hints这两个参数指针指向的都是一个addrinfo的结构体,这是我们继上面以来看到的第4种地址结构体了。它的定义如下:
-
struct addrinfo { int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */ int ai_family; /* PF_xxx */ int ai_socktype; /* SOCK_xxx */ int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */ socklen_t ai_addrlen; /* length of ai_addr */ char *ai_canonname; /* canonical name for hostname */ struct sockaddr *ai_addr; /* binary address */ struct addrinfo *ai_next; /* next structure in linked list */ };
-
我们可以看到它其中包括了一个IPV4的结构体地址
ai_addr
,还有一个指向下一个同类型数据节点的指针ai_next
。
其他参数和之前的地址结构体一些参数作用类似,大家可以对着注释很好理解,或者仍有疑惑可以看看这篇:
socket编程之addrinfo结构体与getaddrinfo函数
这里讲讲ai_next
这个指针,因为我们是去获取server
端的地址,所以很可能有不止一个地址,比如IPV4、IPV6,又或者我们之前所说的一个服务器有多个网卡,这时候可能就会有多个地址。这些地址就会用ai_next
指针串联起来,形成一个单链表。然后我们拿到这个地址链表,去遍历它,对应取出IPV4、IPV6的地址,封装成NSData并装到数组中去。
-
如果中间有错误,赋值错误,返回地址数组,理清楚这几个结构体与函数,这个方法还是相当容易读的,具体的细节可以看看注释。
接着我们回到本文方法二,就要用这个地址数组去做连接了。
//异步去发起连接 dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }});
-
这里调用了我们本文方法六--开始连接的方法1
1 //连接的最终方法 1 2 - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 3 { 4 LogTrace(); 5 6 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 7 //至少有一个server地址 8 NSAssert(address4 || address6, @"Expected at least one valid address"); 9 10 //如果状态不一致,说明断开连接 11 if (aStateIndex != stateIndex) 12 { 13 LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); 14 15 // The connect operation has been cancelled. 16 // That is, socket was disconnected, or connection has already timed out. 17 return; 18 } 19 20 // Check for problems 21 //分开判断。 22 BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; 23 BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; 24 25 if (isIPv4Disabled && (address6 == nil)) 26 { 27 NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; 28 29 [self closeWithError:[self otherError:msg]]; 30 return; 31 } 32 33 if (isIPv6Disabled && (address4 == nil)) 34 { 35 NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; 36 37 [self closeWithError:[self otherError:msg]]; 38 return; 39 } 40 41 // Start the normal connection process 42 43 NSError *err = nil; 44 //调用连接方法,如果失败,则错误返回 45 if (![self connectWithAddress4:address4 address6:address6 error:&err]) 46 { 47 [self closeWithError:err]; 48 } 49 }
这个方法也比较简单,基本上就是做了一些错误的判断。比如:
- 判断在不在这个
socket
队列。 - 判断传过来的
aStateIndex
和属性stateIndex
是不是同一个值。说到这个值,不得不提的是大神用的框架,在容错处理上,做的真不是一般的严谨。从这个stateIndex
上就能略见一二。
这个aStateIndex
是我们之前调用方法,用属性传过来的,所以按道理说,是肯定一样的。但是就怕在调用过程中,这个值发生了改变,这时候整个socket配置也就完全不一样了,有可能我们已经置空地址、销毁socket、断开连接等等...等我们后面再来看这个属性stateIndex
在什么地方会发生改变。 - 判断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
1 //连接最终方法 2。用两个Server地址去连接,失败返回NO,并填充error 2 - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr 3 { 4 LogTrace(); 5 6 NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); 7 8 //输出一些东西? 9 LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); 10 LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); 11 12 // Determine socket type 13 14 //判断是否倾向于IPV6 15 BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; 16 17 // Create and bind the sockets 18 19 //如果有IPV4地址,创建IPV4 Socket 20 if (address4) 21 { 22 LogVerbose(@"Creating IPv4 socket"); 23 24 socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; 25 } 26 //如果有IPV6地址,创建IPV6 Socket 27 if (address6) 28 { 29 LogVerbose(@"Creating IPv6 socket"); 30 31 socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; 32 } 33 34 //如果都为空,直接返回 35 if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) 36 { 37 return NO; 38 } 39 40 //主选socketFD,备选alternateSocketFD 41 int socketFD, alternateSocketFD; 42 //主选地址和备选地址 43 NSData *address, *alternateAddress; 44 45 //IPV6 46 if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL) 47 { 48 socketFD = socket6FD; 49 alternateSocketFD = socket4FD; 50 address = address6; 51 alternateAddress = address4; 52 } 53 //主选IPV4 54 else 55 { 56 socketFD = socket4FD; 57 alternateSocketFD = socket6FD; 58 address = address4; 59 alternateAddress = address6; 60 } 61 //拿到当前状态 62 int aStateIndex = stateIndex; 63 //用socket和address去连接 64 [self connectSocket:socketFD address:address stateIndex:aStateIndex]; 65 66 //如果有备选地址 67 if (alternateAddress) 68 { 69 //延迟去连接备选的地址 70 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ 71 [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; 72 }); 73 } 74 75 return YES; 76 }
这个方法也仅仅是连接中过渡的一个方法,做的事也非常简单:
- 就是拿到IPV4和IPV6地址,先去创建对应的socket,注意这个socket是本机客户端的,和server端没有关系。这里服务端的IPV4和IPV6地址仅仅是用来判断是否需要去创建对应的本机Socket。这里去创建socket会带上我们之前生成的本地地址信息
connectInterface4
或者connectInterface6
。 - 根据我们的config配置,得到主选连接和备选连接。 然后先去连接主选连接地址,在用我们一开始初始化中设置的属性
alternateAddressDelay
,就是这个备选连接延时的属性,去延时连接备选地址(当然如果主选地址在此时已经连接成功,会再次连接导致socket错误,并且关闭)。
这两步分别调用了各自的方法去实现,接下来我们先来看创建本机Socket的方法:
本文方法八--创建Socket:
1 //创建Socket 2 - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr 3 { 4 //创建socket,用的SOCK_STREAM TCP流 5 int socketFD = socket(family, SOCK_STREAM, 0); 6 //如果创建失败 7 if (socketFD == SOCKET_NULL) 8 { 9 if (errPtr) 10 *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; 11 12 return socketFD; 13 } 14 15 //和connectInterface绑定 16 if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) 17 { 18 //绑定失败,直接关闭返回 19 [self closeSocket:socketFD]; 20 21 return SOCKET_NULL; 22 } 23 24 // Prevent SIGPIPE signals 25 //防止终止进程的信号? 26 int nosigpipe = 1; 27 //SO_NOSIGPIPE是为了避免网络错误,而导致进程退出。用这个来避免系统发送signal 28 setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); 29 30 return socketFD; 31 }
这个方法做了这么几件事:
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);
2、其实这个函数在之前那篇IM文章中也讲过了,大家参考参考注释看看就可以了,这里如果返回值为-1,说明创建失败。
3、去绑定我们之前创建的本地地址,它调用了另外一个方法来实现。
4、最后我们调用了如下函数:
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
5、那么这个函数是做什么用的呢?简单来说,它就是给我们的socket加一些额外的设置项,来配置socket
的一些行为。它还有许多的用法,具体可以参考这篇文章:setsockopt函数
而这里的目的是为了来避免网络错误而出现的进程退出的情况,调用了这行函数,网络错误后,系统不再发送进程退出的信号。
关于这个进程退出的错误可以参考这篇文章:Mac OSX下SO_NOSIGPIPE的怪异表现
总结,本文未完,续下节 :iOS即时通讯之CocoaAsyncSocket源码解析二