移动端 IP 优选方案
1. IP 优选目的
无论是从 Local DNS 解析域名,获取到 IP 列表,还是从第三方的 DNS 解析服务中,获取到域名对应的 IP 列表。我们获得多个 IP 后,总是想选取一个最优的 IP 使用,本文主要探讨如何在客户端探测 IP 的连接性以及连接速度,保证返回可用性最好的IP,以达到“IP优选”的目的。
2. 新浪开源的 httpdns 的 sdk 里的测速逻辑
新浪开源一个 HTTPDNSLib ,里面包含了测速逻辑,GitHub地址如下:
我们以该 sdk 里的测速逻辑为例进行原理解析。
3. IP 测试实现原理
使用 linux socket connect 和 select 函数实现的。 基于以下原理:
- 即使套接口是非阻塞的。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功,我们必须处理这种情况。
- 源自Berkeley的实现(和Posix.1g)有两条与select 和非阻塞IO相关的规则:
A. 当连接建立成功时,套接口描述符变成可写;
B. 当连接出错时,套接口描述符变成既可读又可写。
详细的测速实现如下,原理参考注释:
以 iOS 实现为例:
- (int)testSpeedOf:(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;
// MARK: - 设置端口,这里需要根据需要自定义,默认是80端口。
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr([ip UTF8String]);
//saddr.sin_addr.s_addr = inet_addr("1.1.1.123");
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 = HTTPDNS_SOCKET_CONNECT_TIMEOUT;
tv.tv_usec = 0;
fd_set myset;
FD_ZERO(&myset);
FD_SET(s, &myset);
// MARK: - 使用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 = 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);
//valopt 表示错误信息。
// MARK: - 测试核心逻辑,连接后,获取错误信息,如果没有错误信息就是访问成功
/*!
* //getsockopt函数可获取影响套接字的选项,比如SOCKET的出错信息
* (get socket option)
*/
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;
}
目前 《阿里云 HTTPDNS SDK 》 内部已经集成了该逻辑,如果有兴趣可以进钉钉群交流:"【客服群】阿里云移动服务-HTTPDNS",群号:11777313。