注册

CocoaAsyncSocket源码分析---Connect (二)

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

908845925d39d1f0539c537fcd8c879a.png


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

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


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

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

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

//多一个inInterface,本机地址
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
//{} 跟踪当前行为
LogTrace();

// Just in case immutable objects were passed
//拿到host ,copy防止值被修改
NSString *host = [inHost copy];
//interface?接口?
NSString *interface = [inInterface copy];

//声明两个__block的
__block BOOL result = NO;
//error信息
__block NSError *preConnectErr = nil;

//gcdBlock ,都包裹在自动释放池中
dispatch_block_t block = ^{ @autoreleasepool {

// Check for problems with host parameter

if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];

//其实就是return,大牛的代码真是充满逼格
return_from_block;
}

// Run through standard pre-connect checks
//一个前置的检查,如果没通过返回,这个检查里,如果interface有值,则会将本机的IPV4 IPV6的 address设置上。
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}

// We've made it past all the checks.
// It's time to start the connection process.
//flags 做或等运算。 flags标识为开始Socket连接
flags |= kSocketStarted;

//又是一个{}? 只是为了标记么?
LogVerbose(@"Dispatching DNS lookup...");

// It's possible that the given host parameter is actually a NSMutableString.
//很可能给我们的服务端的参数是一个可变字符串
// So we want to copy it now, within this block that will be executed synchronously.
//所以我们需要copy,在Block里同步的执行
// This way the asynchronous lookup block below doesn't have to worry about it changing.
//这种基于Block的异步查找,不需要担心它被改变

//copy,防止改变
NSString *hostCpy = [host copy];

//拿到状态
int aStateIndex = stateIndex;
__weak GCDAsyncSocket *weakSelf = self;

//全局Queue
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//异步执行
dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
//忽视循环引用
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"

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

//strongSelf
__strong GCDAsyncSocket *strongSelf = weakSelf;

//完整Block安全形态,在加个if
if (strongSelf == nil) return_from_block;

//如果有错
if (lookupErr)
{
//用cocketQueue
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
//一些错误处理,清空一些数据等等
[strongSelf lookup:aStateIndex didFail:lookupErr];
}});
}
//正常
else
{

NSData *address4 = nil;
NSData *address6 = nil;
//遍历地址数组
for (NSData *address in addresses)
{
//判断address4为空,且address为IPV4
if (!address4 && [[self class] isIPv4Address:address])
{
address4 = address;
}
//判断address6为空,且address为IPV6
else if (!address6 && [[self class] isIPv6Address:address])
{
address6 = address;
}
}
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {

[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
}

#pragma clang diagnostic pop
}});

//开启连接超时
[self startConnectTimeout:timeout];

result = YES;
}};
//在socketQueue中执行这个Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否则同步的调起这个queue去执行
else
dispatch_sync(socketQueue, block);

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

这个方法非常长,它主要做了以下几件事:

  • 首先我们需要说一下的是,整个类大量的会出现LogTrace()类似这样的宏,我们点进去发现它的本质只是一个{},什么事都没做。

原来这些宏是为了追踪当前执行的流程用的,它被定义在一个大的#if #else中:

#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif
#if GCDAsyncSocketLoggingEnabled
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// https://github.com/robbiehanson/CocoaLumberjack
//
// It allows us to do a lot of logging without significantly slowing down the code.
#import "DDLog.h"
#define LogAsync YES
#define LogContext GCDAsyncSocketLoggingContext
#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
#ifndef GCDAsyncSocketLogLevel
#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE
#endif
// Log levels : off, error, warn, info, verbose
static const int logLevel = GCDAsyncSocketLogLevel;
#else
// Logging Disabled
#define LogError(frmt, ...) {}
#define LogWarn(frmt, ...) {}
#define LogInfo(frmt, ...) {}
#define LogVerbose(frmt, ...) {}
#define LogCError(frmt, ...) {}
#define LogCWarn(frmt, ...) {}
#define LogCInfo(frmt, ...) {}
#define LogCVerbose(frmt, ...) {}
#define LogTrace() {}
#define LogCTrace(frmt, ...) {}
#endif


而此时因为GCDAsyncSocketLoggingEnabled默认为0,所以仅仅是一个{}。当标记为1时,这些宏就可以用来输出我们当前的业务流程,极大的方便了我们的调试过程。

  • 接着我们回到正题上,我们定义了一个Block,所有的连接操作都被包裹在这个Block中。我们做了如下判断:

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

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

  • 接着把Block中连接过程产生的错误进行赋值,并且把连接的结果返回出去
//如果有错误,赋值错误
if (errPtr) *errPtr = preConnectErr;
//把连接是否成功的result返回
return result;

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

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

if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];

//其实就是return,大牛的代码真是充满逼格
return_from_block;
}
//用该字符串生成一个错误,错误的域名,错误的参数
- (NSError *)badParamError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];

return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
}

2.接着做了一个前置的错误检查:
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
这个检查方法,如果没通过返回NO。并且如果interface有值,则会将本机的IPV4 IPV6的 address设置上。即我们之前提到的这两个属性:
  //本机的IPV4地址
NSData * connectInterface4;
//本机的IPV6地址
NSData * connectInterface6;

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

本文方法三--前置检查方法
//在连接之前的接口检查,一般我们传nil  interface本机的IP 端口等等
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
//先断言,如果当前的queue不是初始化quueue,直接报错
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");

//无代理
if (delegate == nil) // Must have delegate set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//没有代理queue
if (delegateQueue == NULL) // Must have delegate queue set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}

//当前不是非连接状态
if (![self isDisconnected]) // Must be disconnected
{
if (errPtr)
{
NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}

//判断是否支持IPV4 IPV6 &位与运算,因为枚举是用 左位移<<运算定义的,所以可以用来判断 config包不包含某个枚举。因为一个值可能包含好几个枚举值,所以这时候不能用==来判断,只能用&来判断
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;

//是否都不支持
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
if (errPtr)
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}

//如果有interface,本机地址
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;

//得到本机的IPV4 IPV6地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];

//如果两者都为nil
if ((interface4 == nil) && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
*errPtr = [self badParamError:msg];
}
return NO;
}

if (isIPv4Disabled && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
*errPtr = [self badParamError:msg];
}
return NO;
}

if (isIPv6Disabled && (interface4 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
*errPtr = [self badParamError:msg];
}
return NO;
}
//如果都没问题,则赋值
connectInterface4 = interface4;
connectInterface6 = interface6;
}

// Clear queues (spurious read/write requests post disconnect)
//清除queue(假的读写请求 ,提交断开连接)
//读写Queue清除
[readQueue removeAllObjects];
[writeQueue removeAllObjects];

return YES;
}

又是非常长的一个方法,但是这个方法还是非常好读的。

  • 主要是对连接前的一个属性参数的判断,如果不齐全的话,则填充错误指针,并且返回NO。

  • 在这里如果我们interface这个参数不为空话,我们会额外多执行一些操作。
    首先来讲讲这个参数是什么,简单来讲,这个就是我们设置的本机IP+端口号。照理来说我们是不需要去设置这个参数的,默认的为localhost(127.0.0.1)本机地址。而端口号会在本机中取一个空闲可用的端口。
    而我们一旦设置了这个参数,就会强制本地IP和端口为我们指定的。其实这样设置反而不好,其实大家也能想明白,这里端口号如果我们写死,万一被其他进程给占用了。那么肯定是无法连接成功的。
    所以就有了我们做IM的时候,一般是不会去指定客户端bind某一个端口。而是用系统自动去选择。

  • 我们最后清空了当前读写queue中,所有的任务。

至于有interface,我们所做的额外操作是什么呢,我们接下来看下一章






作者:Cooci
链接:https://www.jianshu.com/p/9968ff0280e5






0 个评论

要回复文章请先登录注册