CocoaAsyncSocket源码分析---Connect (四)
本文方法五--创建服务端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
。这里讲讲ai_next
这个指针,因为我们是去获取server
端的地址,所以很可能有不止一个地址,比如IPV4、IPV6,又或者我们之前所说的一个服务器有多个网卡,这时候可能就会有多个地址。这些地址就会用ai_next
指针串联起来,形成一个单链表。
然后我们拿到这个地址链表,去遍历它,对应取出IPV4、IPV6的地址,封装成NSData并装到数组中去。
- 如果中间有错误,赋值错误,返回地址数组,理清楚这几个结构体与函数,这个方法还是相当容易读的,具体的细节可以看看注释。
接着我们回到本文方法二,就要用这个地址数组去做连接了。
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
这里调用了我们本文方法六--开始连接的方法1
//连接的最终方法 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];
}
}
这个方法也比较简单,基本上就是做了一些错误的判断。比如:
- 判断在不在这个
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
//连接最终方法 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;
}
这个方法也仅仅是连接中过渡的一个方法,做的事也非常简单:
- 就是拿到IPV4和IPV6地址,先去创建对应的socket,注意这个socket是本机客户端的,和server端没有关系。这里服务端的IPV4和IPV6地址仅仅是用来判断是否需要去创建对应的本机Socket。这里去创建socket会带上我们之前生成的本地地址信息
connectInterface4
或者connectInterface6
。 - 根据我们的config配置,得到主选连接和备选连接。 然后先去连接主选连接地址,在用我们一开始初始化中设置的属性
alternateAddressDelay
,就是这个备选连接延时的属性,去延时连接备选地址(当然如果主选地址在此时已经连接成功,会再次连接导致socket错误,并且关闭)。
这两步分别调用了各自的方法去实现,接下来我们先来看创建本机Socket的方法:
本文方法八--创建Socket:
//创建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;
}
这个方法做了这么几件事:
- 创建了一个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);
其实这个函数在之前那篇IM文章中也讲过了,大家参考参考注释看看就可以了,这里如果返回值为-1,说明创建失败。
- 去绑定我们之前创建的本地地址,它调用了另外一个方法来实现。
- 最后我们调用了如下函数
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
而这里的目的是为了来避免网络错误而出现的进程退出的情况,调用了这行函数,网络错误后,系统不再发送进程退出的信号。
关于这个进程退出的错误可以参考这篇文章:Mac OSX下SO_NOSIGPIPE的怪异表现
未完总结:
connect
篇还没有完结,奈何篇幅问题,只能断在这里。下一个方法将是socket
本地绑定的方法。再下面就是我们最终的连接方法了,历经九九八十一难,马上就要取到真经了...(然而这仅仅是一个开始...)
下一篇将会承接这一篇的内容继续讲,包括最终连接、连接完成后的source
和流的处理。
我们还会去讲讲iOS
作为服务端的accpet
建立连接的流程。
除此之外还有 unix domin socket
(进程间通信)的连接。
CocoaAsyncSocket源码分析---Connect (一)
CocoaAsyncSocket源码分析---Connect (二)
CocoaAsyncSocket源码分析---Connect (三)
CocoaAsyncSocket源码分析---Connect (四)
CocoaAsyncSocket源码分析---Connect (五)
CocoaAsyncSocket源码分析---Connect (六)
CocoaAsyncSocket源码分析---Connect (七)
CocoaAsyncSocket源码分析---Connect (八)
作者:Cooci
链接:https://www.jianshu.com/p/9968ff0280e5