iOS自建DNS之后对IP+Port测速
- 传统的Local DNS域名解析的弊端
- 通过域名拿到IP之后,如何选用最快的一条链路
传统的Local DNS域名解析的弊端
客户端默认使用运营商的Local DNS
来进行域名递归解析,然后将解析后的结果返回给客户端。这个过程当中经常会出现域名劫持,导致出现很多意想不到的结果。而且传统的 DNS
有很多问题,例如解析慢、更新不及时。因为缓存、转发、NAT问题导致客户端误会自己所在的位置和运营商,从而影响流量的调度。
为了解决这个弊端,就得自建域名解析服务来绕过本地的域名解析。通过HTTP协议来发送域名解析请求,拿到一组IP,并缓存到本地。只要缓存不过期,IP地址可以直接从缓存取。缓存过期之后,再次重新解析一次。自建域名解析往往成本比较高,市面上也有一些现成的域名解析厂商,如HTTPDNS
。采用三方的域名解析服务之后,会大大降低我们域名解析的接入成本。
通过域名拿到IP之后,如何选用最快的一条链路
三方域名解析之后,会拿到一组IP,客户端需要根据拿到的IP + Port来进行测速,从而找到最快的一组IP + Port组合,目前网上的方案只有对ipv4 + Port
进行测速,没有对ipv6+ Port
进行测速的方案,因此参考:移动端网络请求优化之 IP 测速排序方案及新浪开源测速方案 进行了改造,改造之后即可以对ipv4 + Port
进行测速,也可以对ipv6 + Port
进行测速。代码如下:
/// 根据ipv4和端口进行测速
/// @param ip ipv4地址
/// @param port 端口号
- (int)testIpv4SpeedOf:(NSString *)ip port:(int16_t)port {
NSString *oldIp = ip;
// request time out
float rtt = 0.0;
// sock:将要被设置或者获取选项的套接字。
int s = 0;
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// 设置端口,这里需要根据需要自定义,默认是80端口。
saddr.sin_port = htons(port);
// 设置ip地址
saddr.sin_addr.s_addr = inet_addr([ip UTF8String]);
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
NSLog(@"ERROR:%s:%d, create socket failed.", __FUNCTION__, __LINE__);
return 0;
}
NSDate *startTime = [NSDate date];
NSDate *endTime;
// 为了设置connect超时 把socket设置称为非阻塞
int flags = fcntl(s, F_GETFL, 0);
fcntl(s, F_SETFL, flags | O_NONBLOCK);
// 对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回;
// 对于非阻塞式套接字,如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成;
// 如果返回0,则表示连接已经建立,这通常是在服务器和客户在同一台主机上时发生。
int i = connect(s, (struct sockaddr *)&saddr, sizeof(saddr));
if (i == 0) {
// 建立连接成功,返回rtt时间。 因为connect是非阻塞,所以这个时间就是一个函数执行的时间,毫秒级,没必要再测速了。
close(s);
return 1;
}
struct timeval tv;
int valopt;
socklen_t lon;
tv.tv_sec = GF_HTTPDNS_SOCKET_CONNECT_TIMEOUT;
tv.tv_usec = 0;
fd_set myset;
FD_ZERO(&myset);
FD_SET(s, &myset);
/**
使用select函数,对套接字的IO操作设置超时。
select函数
select是一种IO多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。
connect本身并不具有设置超时功能,如果想对套接字的IO操作设置超时,可使用select函数。
**/
int maxfdp = s + 1;
int j = select(maxfdp, NULL, &myset, NULL, &tv);
if (j == 0) {
NSLog(@"INFO:%s:%d, test rtt of (%@) timeout.", __FUNCTION__, __LINE__, oldIp);
rtt = GF_HTTPDNS_SOCKET_CONNECT_TIMEOUT_RTT;
close(s);
return rtt;
}
if (j < 0) {
NSLog(@"ERROR:%s:%d, select function error.", __FUNCTION__, __LINE__);
rtt = 0;
close(s);
return rtt;
}
/**
对于select和非阻塞connect,注意两点:
[1] 当连接成功建立时,描述符变成可写; [2] 当连接建立遇到错误时,描述符变为即可读,也可写,遇到这种情况,可调用getsockopt函数。
**/
lon = sizeof(int);
/*!
* 测试核心逻辑,连接后,获取错误信息,如果没有错误信息就是访问成功
* getsockopt(get socket option)函数可获取影响套接字的选项,比如SOCKET的出错信息
* valopt 表示错误信息。
* 返回状态 --- 成功:0,失败:-1
*/
int status = getsockopt(s, SOL_SOCKET, SO_ERROR, (void *)(&valopt), &lon);
// 如果有错误信息:
if (valopt) {
NSLog(@"ERROR:%s:%d, select function error.", __FUNCTION__, __LINE__);
rtt = 0;
} else {
endTime = [NSDate date];
rtt = [endTime timeIntervalSinceDate:startTime] * 1000;
}
close(s);
return rtt;
}
/// 根据ipv6和端口进行测速
/// @param ip ipv6地址
/// @param port 端口号
- (int)testIpv6SpeedOf:(NSString *)ip port:(int16_t)port {
NSString *oldIp = ip;
// request time out
float rtt = 0.0;
// sock:将要被设置或者获取选项的套接字。
int s = 0;
struct sockaddr_in6 saddr6;
// 注意初始化
memset(&saddr6, 0, sizeof(saddr6));
saddr6.sin6_family = AF_INET6;
// 设置端口,这里需要根据需要自定义,默认是80端口。
saddr6.sin6_port = htons(port);
// 设置ip地址
inet_pton(AF_INET6, [ip UTF8String], &(saddr6.sin6_addr));
if ((s = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
NSLog(@"ERROR: %s: %d, create socket failed.", __FUNCTION__, __LINE__);
return 0;
}
NSDate *startTime = [NSDate date];
NSDate *endTime;
// 为了设置connect超时 把socket设置称为非阻塞
int flags = fcntl(s, F_GETFL, 0);
fcntl(s, F_SETFL, flags | O_NONBLOCK);
// 对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回;
// 对于非阻塞式套接字,如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立,建立启动但是尚未完成;
// 如果返回0,则表示连接已经建立,这通常是在服务器和客户在同一台主机上时发生。
int i = connect(s, (struct sockaddr *)&saddr6, sizeof(saddr6));
if (i == 0) {
// 建立连接成功,返回rtt时间。 因为connect是非阻塞,所以这个时间就是一个函数执行的时间,毫秒级,没必要再测速了。
close(s);
return 1;
}
struct timeval tv;
int valopt;
socklen_t lon;
tv.tv_sec = GF_HTTPDNS_SOCKET_CONNECT_TIMEOUT;
tv.tv_usec = 0;
fd_set myset;
FD_ZERO(&myset);
FD_SET(s, &myset);
/**
使用select函数,对套接字的IO操作设置超时。
select函数
select是一种IO多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。
connect本身并不具有设置超时功能,如果想对套接字的IO操作设置超时,可使用select函数。
**/
int maxfdp = s + 1;
int j = select(maxfdp, NULL, &myset, NULL, &tv);
if (j == 0) {
NSLog(@"INFO: %s: %d, test rtt of (%@) timeout.", __FUNCTION__, __LINE__, oldIp);
rtt = GF_HTTPDNS_SOCKET_CONNECT_TIMEOUT_RTT;
close(s);
return rtt;
}
if (j < 0) {
NSLog(@"ERROR: %s: %d, select function error.", __FUNCTION__, __LINE__);
rtt = 0;
close(s);
return rtt;
}
/**
对于select和非阻塞connect,注意两点:
[1] 当连接成功建立时,描述符变成可写; [2] 当连接建立遇到错误时,描述符变为即可读,也可写,遇到这种情况,可调用getsockopt函数。
**/
lon = sizeof(valopt);
/*!
* 测试核心逻辑,连接后,获取错误信息,如果没有错误信息就是访问成功
* getsockopt(get socket option)函数可获取影响套接字的选项,比如SOCKET的出错信息
* valopt 表示错误信息。
* 返回状态 --- 成功:0,失败:-1
*/
int status = getsockopt(s, SOL_SOCKET, SO_ERROR, (void *)(&valopt), &lon);
// 如果有错误信息:
if (valopt) {
NSLog(@"ERROR: %s: %d, select function error.", __FUNCTION__, __LINE__);
rtt = 0;
} else {
endTime = [NSDate date];
rtt = [endTime timeIntervalSinceDate:startTime] * 1000;
}
close(s);
return rtt;
}
/// 根据ip和端口进行测速
/// @param ip ipv4或ipv6地址
/// @param port 端口号
- (int)testSpeedOf:(NSString *)ip port:(int16_t)port {
if (如果是ipv4地址 == YES) {
return [self testIpv4SpeedOf:ip port:port];
} else {
return [self testIpv6SpeedOf:ip port:port];
}
}
判断如果是ipv4
地址则走ipv4
测速,如果是ipv6
地址则走ipv6
测速。
注意:当出现错误的时候测试结果是速度是0,所以排序时不能简单地按照值大小排序,可以先删除速度为0的结果,或者将速度为零重置为超时时间,比如上面的 CYL_SOCKET_CONNECT_TIMEOUT_RTT 。避免错误IP为0,结果排序后排在前面。
在进行ipv6测速时, memset(&saddr6, 0, sizeof(saddr6)); 这一句调用很重要。
参考链接:
【1】https://github.com/ChenYilong/iOSBlog/issues/17
【2】https://github.com/CNSRE/HTTPDNSLib-for-iOS/blob/master/src/DNSCache/SpeedTest/WBDNSTCPSpeedTester.m