iOS ping - SimplePing 源碼解讀

ping 的功能

ping 程序對于開發人員來說應該是不會陌生的, ping 通常用來探測主機到主機之間是否可以通信。如果可以 ping 通,意味著可以和該主機建立網絡連接,就像這樣的。

?  ~ ping www.qq.com
PING www.qq.com (182.254.34.74): 56 data bytes
64 bytes from 182.254.34.74: icmp_seq=0 ttl=53 time=22.996 ms
64 bytes from 182.254.34.74: icmp_seq=1 ttl=53 time=36.688 ms
64 bytes from 182.254.34.74: icmp_seq=2 ttl=53 time=25.390 ms
64 bytes from 182.254.34.74: icmp_seq=3 ttl=53 time=25.516 ms

如果不能 ping 通,那就意味著無法和該主機建立網絡連接,就像下面這樣的。

?  ~ ping www.google.com
PING www.google.com (66.220.147.47): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4

Apple 的 SimplePing 封裝了 ping 的功能,它利用 resolve host,create socket(send & recv data), 解析 ICMP 包驗證 checksum 等實現了 ping 功能。并且支持 iPv4 和 iPv6。

ICMP 協議

ping 功能使用是 ICMP 協議(Internet Control Message Protocol),ICMP 協議定義了一組錯誤信息,當路由器或者主機無法成功處理一個IP 封包的時候,能夠將錯誤信息回送給來源主機,ICMP 常見的錯誤如下。

  1. 傳輸線路或者節點故障導致無法到達目的地主機
  2. 路由器封包重組失敗
  3. 封包存活時間(Time To Live,TTL)變成 0 (防止封包在網絡中永無止境得繞圈)
  4. IP 首部的錯誤檢查碼發現錯誤

iOS SimplePing 的使用

    // 1. 利用 HostName 創建 SimplePing
    SimplePing *pinger = [[SimplePing alloc] initWithHostName:@"www.apple.com"];
    self.pinger = pinger;
    // 2. 指定 IP 地址類型
    if (isIpv4 && !isIpv6) {
        pinger.addressStyle = SimplePingAddressStyleICMPv4;
    }else if (isIpv6 && !isIpv4) {
        pinger.addressStyle = SimplePingAddressStyleICMPv6;
    }
    // 3. 設置 delegate,用于接收回調信息
    pinger.delegate = self;
    // 4. 開始 ping
    [pinger start];

SimplePing 的使用還是非常簡單的,

  1. 利用 HostName 創建 SimplePing
  2. 指定 IP 地址類型
  3. 設置 delegate,用于接收回調信息
  4. 開始 ping

delegate 的回調方法體現了 ping 的過程。

// 解析 HostName 拿到 ip 地址之后,發送封包
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address
{
    NSLog(@"pinging %@", displayAddressForAddress(address));
    [self sendPing];
}
// ping 功能啟動失敗
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error
{
    NSLog(@"failed: %@", shortErrorFromError(error));
    [self stop];
}
// ping 成功發送封包
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber
{
    NSLog(@"#%u sent", sequenceNumber);
}
// ping 發送封包失敗
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error
{
    NSLog(@"#%u send failed: %@", sequenceNumber,shortErrorFromError(error));
}
// ping 發送封包之后收到響應
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber
{
    NSLog(@"#%u received, size=%zu", sequenceNumber, packet.length);
}
// ping 接收響應封包發生異常
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet
{
    NSLog(@"unexpected packet, size=%zu", packet.length);
}

SimplePing 的流程

流程圖

上圖是 SimplePing 執行一次 ping IPv4 地址的流程圖,
ping 的實現并不負責,一共有以下幾個步驟

  1. 解析傳入的 HostName,獲取第一個可用 IP 地址
  2. 創建傳輸/接收數據的 socket
  3. 發送數據,封裝一個 ICMP 包
  4. 解析目標 IP 傳回的 ICMP 包

HostName 的解析

關于 HostName 的解析,SimplePing 采用 CFHost 這個異步 API 方案,通過CFHost解析主機名主要有以下幾個步驟:

  1. 通過調用 CFHostCreateWithName 創建一個 CFHostRef 對象。
  2. 調用 CFHostSetClient 并且提供一個上下文對象和回調函數,這個回調函數在解析結束的時候會被調用。
  3. 調用 CFHostScheduleWithRunLoop 用于在 RunLoop 中執行具體的解析操作。
  4. 調用 CFHostStartInfoResolution 來告訴解析器開始解析,把它的第二個參數設置為 kCFHostAddresses 表明你想要返回一個 IP 地址。
  5. 等待解析器調用你的回調函數,通過你的回調函數,調用 CFHostGetAddressing 函數來獲取解析結果。這個函數返回 CFDataRef 對象的一個數組,其中的每一個都包含一個 POSIX 的 sockaddr 結構體。

下面的這段代碼執行的是 1 - 4 過程

- (void)start {
    Boolean             success;
    CFHostClientContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
    CFStreamError       streamError;
    
    assert(self.host == NULL);
    assert(self.hostAddress == nil);

    self.host = (CFHostRef) CFAutorelease( CFHostCreateWithName(NULL, (__bridge CFStringRef) self.hostName) );
    assert(self.host != NULL);
    
    CFHostSetClient(self.host, HostResolveCallback, &context);
    
    CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    
    success = CFHostStartInfoResolution(self.host, kCFHostAddresses, &streamError);
    if ( ! success ) {
        [self didFailWithHostStreamError:streamError];
    }
}

在系統解析 HostName 成功之后會調用 HostResolveCallback 這個回調,這個回調的作用相當于重定向,將內容從 c 轉成適當的 Objective-C 內容。

static void HostResolveCallback(CFHostRef theHost, CFHostInfoType typeInfo, const CFStreamError *error, void *info) {
    // This C routine is called by CFHost when the host resolution is complete. 
    // It just redirects the call to the appropriate Objective-C method.
    SimplePing *    obj;
    obj = (__bridge SimplePing *) info;
    assert([obj isKindOfClass:[SimplePing class]]);
    // 省略代碼 ......
    if ( (error != NULL) && (error->domain != 0) ) {
        [obj didFailWithHostStreamError:*error];
    } else {
       // 在這個方法獲取 HostName 對應的地址
        [obj hostResolutionDone];
    }
}

調用 CFHostGetAddressing 函數來獲取解析結果,這個函數返回一個數組,從這個數組中取得 HostName 對應的 IP。從服務端的角度來說,為了實現負載均衡,一個域名是可以對應多個 IP 的,但是從客戶端的角度來說,一個域名就是對應一個 IP。

- (void)hostResolutionDone {
    Boolean     resolved;
    NSArray *   addresses;
    
    // Find the first appropriate address.
    
    addresses = (__bridge NSArray *) CFHostGetAddressing(self.host, &resolved);
    if ( resolved && (addresses != nil) ) {
        resolved = false;
        for (NSData * address in addresses) {
            const struct sockaddr * addrPtr;
            
            addrPtr = (const struct sockaddr *) address.bytes;
            if ( address.length >= sizeof(struct sockaddr) ) {
                switch (addrPtr->sa_family) {
                    case AF_INET: {
                        if (self.addressStyle != SimplePingAddressStyleICMPv6) {
                            self.hostAddress = address;
                            resolved = true;
                        }
                    } break;
                    case AF_INET6: {
                        if (self.addressStyle != SimplePingAddressStyleICMPv4) {
                            self.hostAddress = address;
                            resolved = true;
                        }
                    } break;
                }
            }
            if (resolved) {
                break;
            }
        }
    }

    // We're done resolving, so shut that down.
    
    [self stopHostResolution];
    
    // If all is OK, start the send and receive infrastructure, otherwise stop.
    
    if (resolved) {
        [self startWithHostAddress];
    } else {
        [self didFailWithError:[NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]];
    }
}

Socket 操作

HostName 解析成功拿到對應的 IP 之后,SimplePing 調用startWithHostAddress 創建 socket 。

  1. 使用 CFSocketCreateWithNative 創建一個 CFSocket
  2. 使用 CFSocketCreateRunLoopSource 為 CFSocket 創建一個 CFRunLoopSourceRef,
  3. 使用 CFRunLoopAddSource 將 CFRunLoopSourceRef 添加到 RunLoop 的 kCFRunLoopDefaultMode 模式中。
- (void)startWithHostAddress {
 // 省略代碼 ......
        CFSocketContext         context = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFRunLoopSourceRef      rls;
        id<SimplePingDelegate>  strongDelegate;
        
        // Wrap it in a CFSocket and schedule it on the runloop.
        
        self.socket = (CFSocketRef) CFAutorelease( CFSocketCreateWithNative(NULL, fd, kCFSocketReadCallBack, SocketReadCallback, &context) );
        assert(self.socket != NULL);
        
        // The socket will now take care of cleaning up our file descriptor.
        
        assert( CFSocketGetSocketFlags(self.socket) & kCFSocketCloseOnInvalidate );
        fd = -1;
        
        rls = CFSocketCreateRunLoopSource(NULL, self.socket, 0);
        assert(rls != NULL);
        
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
    
        CFRelease(rls);
 // 省略代碼 ......
}

在 CFSocketCreateWithNative 的官方文檔描述中有提到,CFSocketCreateWithNative 在創建 socket 的時候是有一個復用機制的。

The new CFSocket object, or `NULL` if an error occurred. 
If a CFSocket object already exists for `sock`, 
the function returns the pre-existing object instead of creating a new object; 

封裝 ICMP 包

在 socket 創建完成之后,接下來就要開始組裝 IP 封包并發送了。組裝 IP 封包并發送的過程需要我們手動在這個回調方法觸發。

- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address
{
// 調用 - (void)sendPingWithData:(NSData *)data 
}

sendPingWithData 這個方法做的操作是組裝 IP 封包然后發送封包,調用這個過程對應的回調方法。發送封包的過程是調用 sendto 方法

- (void)sendPingWithData:(NSData *)data {
    // 省略代碼 ......

    // Send the packet.
    
    if (self.socket == NULL) {
        bytesSent = -1;
        err = EBADF;
    } else {
        bytesSent = sendto(
            CFSocketGetNative(self.socket),
            packet.bytes,
            packet.length, 
            0,
            self.hostAddress.bytes, 
            (socklen_t) self.hostAddress.length
        );
        err = 0;
        if (bytesSent < 0) {
            err = errno;
        }
    }
    // 省略代碼 ......

組裝 IP 封包是調用下面這個方法來完成,這個方法把數據按照 ICMPHeader 結構體的格式進行初始化并返回 IP 封包,關于 ICMPHeader 的結構這里就不再累贅,通過 ICMPHeader 結構體的定義就可以明白。

- (NSData *)pingPacketWithType:(uint8_t)type 
                   payload:(NSData *)payload 
                  requiresChecksum:(BOOL)requiresChecksum ;

解析 ICMP 包

完成了發送操作之后,接下來就是等待 ping 的響應了。當 socket 收到 ping 響應的時候回調 SocketReadCallback ,這個回調的作用相當于重定向,將內容從 c 轉成適當的 Objective-C 內容,SocketReadCallback 里面調用了 readData 方法。

static void SocketReadCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) 

在 readData 方法里面做的工作就是讀取響應數據,驗證響應的數據正確性,執行相應的回調方法。與sendto對應,讀取數據使用的是 recvfrom 方法。驗證響應數據調用的是下面的方法。

- (BOOL)validatePing4ResponsePacket:(NSMutableData *)packet sequenceNumber:(uint16_t *)sequenceNumberPtr 

這個方法接收 ping 響應數據的時候,會對 ICMP 包進行校驗,會跳過 IP 頭,畢竟 IP 首部對于 ping 功能來說并不重要,重要的是 ICMP 協議的內容,其中主要驗證的字段是 checksum 和 sequenceNumber(iPv6 只需要驗證 sequenceNumber)。
停止 ping 的時候需要做一些清理工作,包括 socket 和 CFHost 對應的銷毀。
到這里,整個 ping 的基本流程就結束了。

總結

ICMP 協議規定,目的主機必須返回 ICMP 回送應答消息給源主機,如果源主機在一定時間內收到應答,則認為主機可達,而 ping 功能使用的是 ICMP 協議。
SimplePing 實現 ping 操作的原理步驟是這樣的,先解析出 HostName 對應的 IP 地址,這個才知道數據包要發送給哪個目的主機,接著構造符合 ICMP 協議格式的數據包并發送,等待目的主機響應。一段時間過后,目的主機響應數據到達源主機,源主機接收響應數據包,驗證數據包,然后去掉數據包的 IP 首部,拿到 ICMP 數據。由于個人水平有限,文章若有不對之處懇請指出,我稍作修改,大家共同進步。

參考

  1. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/ResolvingDNSHostnames.html#//apple_ref/doc/uid/TP40012543-SW1
  2. https://github.com/iOS-Developer-Documents-Chinese/iOS-Developer-Documents-Chinese/blob/master/Socket/DNS%E4%B8%BB%E6%9C%BA%E5%90%8D%E7%9A%84%E8%A7%A3%E6%9E%90.md
  3. https://www.cnblogs.com/cuihongyu3503319/archive/2012/07/09/2583129.html
  4. http://blog.163.com/qhj4433210@126/blog/static/165975282201592251248584/
  5. http://blog.csdn.net/inject2006/article/details/2139149
  6. https://zhaoxinyu.me/2017-04-12-simple-ping/
  7. https://en.wikipedia.org/wiki/IPv4_header_checksum
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.這篇文章不是本人原創的,只是個人為了對這部分知識做一個整理和系統的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,128評論 6 174
  • 簡介 用簡單的話來定義tcpdump,就是:dump the traffic on a network,根據使用者...
    保川閱讀 5,987評論 1 13
  • 個人認為,Goodboy1881先生的TCP /IP 協議詳解學習博客系列博客是一部非常精彩的學習筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,090評論 0 8
  • 1, 網絡是什么 計算機網絡的組成組件: 節點 (node):節點主要是具有網絡地址 (IP) 的設備之稱。 服務...
    求閑居士閱讀 1,494評論 0 3
  • 一日一景 露從今夜白,月是故鄉明。 玉良畫才女,青丹展后人。 潘玉良《月是故鄉明》藝術作品展在江蘇美術館陳列館(國...
    吉光片羽_9bc2閱讀 233評論 0 4