CocoaAsyncSocket源码分析---Connect (二)
这里我们先作为客户端来看看connect
:
其中和connect相关的方法就这么多,我们一般这么来连接到服务端:
[socket connectToHost:Khost onPort:Kport error:nil];
也就是我们在截图中选中的方法,那我们就从这个方法作为起点,开始讲起吧。
本文方法二--connect总方法
/逐级调用
- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr];
}
- (BOOL)connectToHost:(NSString *)host
onPort:(uint16_t)port
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr];
}
//多一个inInterface,本机地址
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
//{} 跟踪当前行为
LogTrace();
// Just in case immutable objects were passed
//拿到host ,copy防止值被修改
NSString *host = [inHost copy];
//interface?接口?
NSString *interface = [inInterface copy];
//声明两个__block的
__block BOOL result = NO;
//error信息
__block NSError *preConnectErr = nil;
//gcdBlock ,都包裹在自动释放池中
dispatch_block_t block = ^{ @autoreleasepool {
// Check for problems with host parameter
if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];
//其实就是return,大牛的代码真是充满逼格
return_from_block;
}
// Run through standard pre-connect checks
//一个前置的检查,如果没通过返回,这个检查里,如果interface有值,则会将本机的IPV4 IPV6的 address设置上。
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
// We've made it past all the checks.
// It's time to start the connection process.
//flags 做或等运算。 flags标识为开始Socket连接
flags |= kSocketStarted;
//又是一个{}? 只是为了标记么?
LogVerbose(@"Dispatching DNS lookup...");
// It's possible that the given host parameter is actually a NSMutableString.
//很可能给我们的服务端的参数是一个可变字符串
// So we want to copy it now, within this block that will be executed synchronously.
//所以我们需要copy,在Block里同步的执行
// This way the asynchronous lookup block below doesn't have to worry about it changing.
//这种基于Block的异步查找,不需要担心它被改变
//copy,防止改变
NSString *hostCpy = [host copy];
//拿到状态
int aStateIndex = stateIndex;
__weak GCDAsyncSocket *weakSelf = self;
//全局Queue
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//异步执行
dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
//忽视循环引用
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
//查找错误
NSError *lookupErr = nil;
//server地址数组(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in类型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
//strongSelf
__strong GCDAsyncSocket *strongSelf = weakSelf;
//完整Block安全形态,在加个if
if (strongSelf == nil) return_from_block;
//如果有错
if (lookupErr)
{
//用cocketQueue
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
//一些错误处理,清空一些数据等等
[strongSelf lookup:aStateIndex didFail:lookupErr];
}});
}
//正常
else
{
NSData *address4 = nil;
NSData *address6 = nil;
//遍历地址数组
for (NSData *address in addresses)
{
//判断address4为空,且address为IPV4
if (!address4 && [[self class] isIPv4Address:address])
{
address4 = address;
}
//判断address6为空,且address为IPV6
else if (!address6 && [[self class] isIPv6Address:address])
{
address6 = address;
}
}
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
}
#pragma clang diagnostic pop
}});
//开启连接超时
[self startConnectTimeout:timeout];
result = YES;
}};
//在socketQueue中执行这个Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否则同步的调起这个queue去执行
else
dispatch_sync(socketQueue, block);
//如果有错误,赋值错误
if (errPtr) *errPtr = preConnectErr;
//把连接是否成功的result返回
return result;
}
这个方法非常长,它主要做了以下几件事:
- 首先我们需要说一下的是,整个类大量的会出现
LogTrace()
类似这样的宏,我们点进去发现它的本质只是一个{},什么事都没做。
原来这些宏是为了追踪当前执行的流程用的,它被定义在一个大的#if #else
中:
#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif
#if GCDAsyncSocketLoggingEnabled
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// https://github.com/robbiehanson/CocoaLumberjack
//
// It allows us to do a lot of logging without significantly slowing down the code.
#import "DDLog.h"
#define LogAsync YES
#define LogContext GCDAsyncSocketLoggingContext
#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
#ifndef GCDAsyncSocketLogLevel
#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE
#endif
// Log levels : off, error, warn, info, verbose
static const int logLevel = GCDAsyncSocketLogLevel;
#else
// Logging Disabled
#define LogError(frmt, ...) {}
#define LogWarn(frmt, ...) {}
#define LogInfo(frmt, ...) {}
#define LogVerbose(frmt, ...) {}
#define LogCError(frmt, ...) {}
#define LogCWarn(frmt, ...) {}
#define LogCInfo(frmt, ...) {}
#define LogCVerbose(frmt, ...) {}
#define LogTrace() {}
#define LogCTrace(frmt, ...) {}
#endif
而此时因为GCDAsyncSocketLoggingEnabled
默认为0,所以仅仅是一个{}。当标记为1时,这些宏就可以用来输出我们当前的业务流程,极大的方便了我们的调试过程。
- 接着我们回到正题上,我们定义了一个
Block
,所有的连接操作都被包裹在这个Block
中。我们做了如下判断:
//在socketQueue中执行这个Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否则同步的调起这个queue去执行
else
dispatch_sync(socketQueue, block);
保证这个连接操作一定是在我们的socketQueue
中,而且还是以串行同步的形式去执行,规避了线程安全的问题。
- 接着把Block中连接过程产生的错误进行赋值,并且把连接的结果返回出去
//如果有错误,赋值错误
if (errPtr) *errPtr = preConnectErr;
//把连接是否成功的result返回
return result;
接着来看这个方法声明的Block内部,也就是进行连接的真正主题操作,这个连接过程将会调用许多函数,一环扣一环,我会尽可能用最清晰、详尽的语言来描述...
1.这个Block首先做了一些错误的判断,并调用了一些错误生成的方法。类似:
if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];
//其实就是return,大牛的代码真是充满逼格
return_from_block;
}
//用该字符串生成一个错误,错误的域名,错误的参数
- (NSError *)badParamError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
}
2.接着做了一个前置的错误检查:
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
这个检查方法,如果没通过返回NO。并且如果interface有值,则会将本机的IPV4 IPV6的 address设置上。即我们之前提到的这两个属性: //本机的IPV4地址
NSData * connectInterface4;
//本机的IPV6地址
NSData * connectInterface6;
我们来看看这个前置检查方法:
本文方法三--前置检查方法
//在连接之前的接口检查,一般我们传nil interface本机的IP 端口等等
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
//先断言,如果当前的queue不是初始化quueue,直接报错
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//无代理
if (delegate == nil) // Must have delegate set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//没有代理queue
if (delegateQueue == NULL) // Must have delegate queue set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//当前不是非连接状态
if (![self isDisconnected]) // Must be disconnected
{
if (errPtr)
{
NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//判断是否支持IPV4 IPV6 &位与运算,因为枚举是用 左位移<<运算定义的,所以可以用来判断 config包不包含某个枚举。因为一个值可能包含好几个枚举值,所以这时候不能用==来判断,只能用&来判断
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
//是否都不支持
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
if (errPtr)
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//如果有interface,本机地址
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
//得到本机的IPV4 IPV6地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
//如果两者都为nil
if ((interface4 == nil) && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv4Disabled && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv6Disabled && (interface4 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
*errPtr = [self badParamError:msg];
}
return NO;
}
//如果都没问题,则赋值
connectInterface4 = interface4;
connectInterface6 = interface6;
}
// Clear queues (spurious read/write requests post disconnect)
//清除queue(假的读写请求 ,提交断开连接)
//读写Queue清除
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
return YES;
}
又是非常长的一个方法,但是这个方法还是非常好读的。
主要是对连接前的一个属性参数的判断,如果不齐全的话,则填充错误指针,并且返回NO。
在这里如果我们interface这个参数不为空话,我们会额外多执行一些操作。
首先来讲讲这个参数是什么,简单来讲,这个就是我们设置的本机IP+端口号。照理来说我们是不需要去设置这个参数的,默认的为localhost(127.0.0.1)本机地址。而端口号会在本机中取一个空闲可用的端口。
而我们一旦设置了这个参数,就会强制本地IP和端口为我们指定的。其实这样设置反而不好,其实大家也能想明白,这里端口号如果我们写死,万一被其他进程给占用了。那么肯定是无法连接成功的。
所以就有了我们做IM的时候,一般是不会去指定客户端bind某一个端口。而是用系统自动去选择。我们最后清空了当前读写queue中,所有的任务。
至于有interface
,我们所做的额外操作是什么呢,我们接下来看下一章
作者:Cooci
链接:https://www.jianshu.com/p/9968ff0280e5