注册

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

至于有interface,我们所做的额外操作是什么呢,我们接下来看看这个方法:本文方法四--本地地址绑定方法

- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
address6:(NSMutableData **)interfaceAddr6Ptr
fromDescription:(NSString *)interfaceDescription
port:(uint16_t)port
{
NSMutableData *addr4 = nil;
NSMutableData *addr6 = nil;

NSString *interface = nil;

//先用:分割
NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
if ([components count] > 0)
{
NSString *temp = [components objectAtIndex:0];
if ([temp length] > 0)
{
interface = temp;
}
}
if ([components count] > 1 && port == 0)
{
//拿到port strtol函数,将一个字符串,根据base参数转成长整型,如base值为10则采用10进制,若base值为16则采用16进制
long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
//UINT16_MAX,65535最大端口号
if (portL > 0 && portL <= UINT16_MAX)
{
port = (uint16_t)portL;
}
}

//为空则自己创建一个 0x00000000 ,全是0 ,为线路地址
//如果端口为0 通常用于分析操作系统。这一方法能够工作是因为在一些系统中“0”是无效端口,当你试图使用通常的闭合端口连接它时将产生不同的结果。一种典型的扫描,使用IP地址为0.0.0.0,设置ACK位并在以太网层广播。
if (interface == nil)
{

struct sockaddr_in sockaddr4;

//memset作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法

//memset(void *s,int ch,size_t n);函数,第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小)
memset(&sockaddr4, 0, sizeof(sockaddr4));
//结构体长度
sockaddr4.sin_len = sizeof(sockaddr4);
//addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
sockaddr4.sin_family = AF_INET;
//端口号 htons将主机字节顺序转换成网络字节顺序 16位
sockaddr4.sin_port = htons(port);
//htonl ,将INADDR_ANY:0.0.0.0,不确定地址,或者任意地址 htonl 32位。 也是转为网络字节序

//ipv4 32位 4个字节 INADDR_ANY,0x00000000 (16进制,一个0代表4位,8个0就是32位) = 4个字节的
sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));

sockaddr6.sin6_len = sizeof(sockaddr6);
//ipv6
sockaddr6.sin6_family = AF_INET6;
//port
sockaddr6.sin6_port = htons(port);

//共128位
sockaddr6.sin6_addr = in6addr_any;

//把这两个结构体转成data
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

//ipv4
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));

sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);

//#define INADDR_LOOPBACK (u_int32_t)0x7f000001
//7f000001->1111111 00000000 00000000 00000001->127.0.0.1
sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

//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_loopback;
//赋值
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
//非localhost、loopback,去获取本机IP,看和传进来Interface是同名或者同IP,相同才给赋端口号,把数据封装进Data。否则为nil
else
{
//转成cString
const char *iface = [interface UTF8String];

//定义结构体指针,这个指针是本地IP
struct ifaddrs *addrs;
const struct ifaddrs *cursor;

//获取到本机IP,为0说明成功了
if ((getifaddrs(&addrs) == 0))
{
//赋值
cursor = addrs;
//如果IP不为空,则循环链表去设置
while (cursor != NULL)
{
//如果 addr4 IPV4地址为空,而且地址类型为IPV4
if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
{
// IPv4

struct sockaddr_in nativeAddr4;
//memcpy内存copy函数,把src开始到size的字节数copy到 dest中
memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));

//比较两个字符串是否相同,本机的IP名,和接口interface是否相同
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
//相同则赋值 port
nativeAddr4.sin_port = htons(port);
//用data封号IPV4地址
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
//本机IP名和interface不相同
else
{
//声明一个IP 16位的数组
char ip[INET_ADDRSTRLEN];

//这里是转成了10进制。。(因为获取到的是二进制IP)
const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));

//如果conversion不为空,说明转换成功而且 ,比较转换后的IP,和interface是否相同
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
//相同则赋值 port
nativeAddr4.sin_port = htons(port);

addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
}
}
//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)];
}
}
}

//指向链表下一个addr
cursor = cursor->ifa_next;
}
//和getifaddrs对应,释放这部分内存
freeifaddrs(addrs);
}
}
//如果这两个二级指针存在,则取成一级指针,把addr4赋值给它
if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;

这个方法中,主要是大量的socket相关的函数的调用,会显得比较难读一点,其实简单来讲就做了这么一件事:
interface变成进行socket操作所需要的地址结构体,然后把地址结构体包裹在NSMutableData中。

这里,为了让大家能更容易理解,我把这个方法涉及到的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种,等我们后续代码来补充。


大家了解了我们上述说的知识点,这个方法也就不难度了。这个方法主要是做了本机IPV4IPV6地址的创建和绑定。当然这里分了几种情况:

  1. interface为空的,我们作为客户端不会出现这种情况。注意之前我们是这个参数不为空才会调入这个方法的。
    而这个一般是用于做服务端监听用的,这里的处理是给本机地址绑定0地址(任意地址)。那么这里这么做作用是什么呢?引用一个应用场景来说明:

如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。

  1. 如果interfacelocalhost或者loopback则把IP设置为127.0.0.1,这里localhost我们大家都知道。那么什么是loopback呢?
    loopback地址叫做回环地址,他不是一个物理接口上的地址,他是一个虚拟的一个地址,只要路由器在工作,这个地址就存在.它是路由器的唯一标识。
    更详细的内容可以看看百科:loopback

  2. 如果是一个其他的地址,我们会去使用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];





1 个评论

太强了 !!

要回复文章请先登录注册