移動端 IP 優選方案

貓耳發表於2018-01-02

摘要: 無論是從 Local DNS 解析域名,獲取到 IP 列表,還是從第三方的 DNS 解析服務中,獲取到域名對應的 IP 列表。我們獲得多個 IP 後,總是想選取一個最優的 IP 使用,本文主要探討如何在客戶端探測 IP 的連線性以及連線速度,保證返回可用性最好的IP,以達到“IP優選”的目的。

移動端 IP 優選方案
1. IP 優選目的
無論是從 Local DNS 解析域名,獲取到 IP 列表,還是從第三方的 DNS 解析服務中,獲取到域名對應的 IP 列表。我們獲得多個 IP 後,總是想選取一個最優的 IP 使用,本文主要探討如何在客戶端探測 IP 的連線性以及連線速度,保證返回可用性最好的IP,以達到“IP優選”的目的。

2. 新浪開源的 httpdns 的 sdk 裡的測速邏輯
新浪開源一個 HTTPDNSLib ,裡面包含了測速邏輯,GitHub地址如下:

《HTTPDNSLib-for-iOS》
《HTTPDNSLib》
我們以該 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;
}

點選檢視原文

相關文章