More-iOS中的Ping

這篇文章是我在項目中須要判斷內外網環境根據網上的資料及本身的改造所得結果,有些不足之處望指出。html

使用ping命令來檢測數據包(ICMP,Internet Control Message Protocol,互聯網控制報文協議)可以經過IP協議到達特定主機,並收到主機的應答,以檢查網絡是否連通和網絡鏈接速度,幫助咱們分析和斷定網絡故障。由於互聯網操做是路由器嚴密監控的。當路由器端處理報文時如有意外發生,事件經過ICMP報告給發送端。git

SimplePing是Appl給開發者提供的一套封裝了底層BSD Sockets ping函數的類,SimplePing下載地址:developer.apple.com/library/con…github

下面咱們一一介紹 SimplePing 類的各個屬性、方法以及delegate回調方法的含義及做用。網絡

初始化方法

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithHostName:(NSString *)hostName NS_DESIGNATED_INITIALIZER;
複製代碼

SimplePing中,禁用了init方法,只提供initWithHostName:這個指定構造方法,它能夠用於初始化一個ping指定的主機實例對象。其中hostName參數能夠是主機的DNS域名,或者是IPv4/IPv6地址的字符串形式。app

@property (nonatomic, copy, readonly) NSString * hostName;
複製代碼

hostName:只讀,保存由初始化方法initWithHostName:傳入的ping操做要鏈接的主機域名或IP地址。dom

@property (nonatomic, assign, readwrite) SimplePingAddressStyle addressStyle;
複製代碼

addressStyle:主機的IP地址類型,如IPv4/IPv6等,其中SimplePingAddressStyle枚舉類型的定義以下:ide

typedef NS_ENUM(NSInteger, SimplePingAddressStyle) {
        SimplePingAddressStyleAny,    // IPv4 或 IPv6
        SimplePingAddressStyleICMPv4, // IPv4
        SimplePingAddressStyleICMPv6  // IPv6
    };
複製代碼
@property (nonatomic, copy, readonly, nullable) NSData * hostAddress;
複製代碼

hostAddress:只讀,在start方法調用以後,根據hostName獲得的要ping的主機的IP地址,它是struct sockaddr形式的NSData數據。當SimplePing實例處於stopped狀態,或者實例調用了start方法,但在simplePing:didStartWithAddress:方法被調用以前,hostAddress 的值都是nil函數

@property (nonatomic, assign, readonly) sa_family_t hostAddressFamily;
複製代碼

hostAddressFamily:只讀,hostAddress的地址族,若是hostAddressnil,則其值爲:AF_UNSPECoop

@property (nonatomic, assign, readonly) uint16_t identifier;
複製代碼

identifier:只讀,當建立一個SimplePing實例對象時,會自動生成一個的隨機的標識符,用來惟一標識當前ping對象。測試

@property (nonatomic, assign, readonly) uint16_t nextSequenceNumber;
複製代碼

nextSequenceNumber:只讀,ping每發送一次數據包都會有一個對應的序列號sequence number,此值爲下一次ping操做要發送數據時的序列號,從0開始遞增,當ping成功發送一次數據到主機並收到應答時,該值+1。而對於本次pingsequence number在成功發送數據(request)和成功接收到響應(response)的delegate回調方法裏都會以方法參數返回,以便進行ping操做耗時的計算等等。

@property (nonatomic, weak, readwrite, nullable) id delegate;
複製代碼

delegate:當前對象的回調,delegate中的回調方法將在對象調用start方法所在的線程對應的run loop中以默認的run loop model執行。

實例方法:

- (void)start;
複製代碼

start 方法:開始一個ping操做,在調用此方法前,必須給SimplePing實例對象的delegete以及其餘參數賦值。當start方法成功執行時,會回調delegate中的simplePing:didStartWithAddress:方法,在該回調方法裏,就能夠經過sendPingWithData:開始發送ICMP數據包,並等待接受主機應答的數據包。另外須要注意的是,當一個實例已經started,又一次調用此start方法會出錯。

- (void)sendPingWithData:(nullable NSData *)data;
複製代碼

sendPingWithData: 方法:向主機發送特定格式的ICMP數據包,調用此方法前必須保證明例已經started而且要等待simplePing:didStartWithAddress:回調執行才能開始發送數據。參數data爲要向主機發送的ICMP數據包,能夠爲nil,默認會發一個標準的64 byte數據包。

- (void)stop;
複製代碼

stop 方法:當結束要ping操做時,調用此方法。與start方法不一樣的是,當一個實例已經stopped,再次調用此方法也沒事。

delegate回調方法:

start方法執行結果的回調:

// start 方法成功執行,可在此開始發送數據,其中 address 爲主機的 IP 地址;
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address;
// start 方法執行失敗,返回錯誤信息;
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error;
複製代碼

sendPingWithData: 方法執行結果的回調,每發送一次數據,都會同步地回調如下兩個方法其中一個(除非你在發送途中調用了stop方法):

// 成功發送 ICMP 數據包到指定主機,在此傳回已發送的數據包以及本次 ping 對應的序列號;
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
// 發送數據失敗,並返回錯誤信息,絕大部分緣由因爲 hostName 解析失敗。另,當此方法調用時,ping 實例狀態會自動轉爲stopped,不用再顯示調用stop方法;
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error;
複製代碼

接收到主機返回應答數據的回調:

// 成功接收到主機回傳的與以前發送相匹配的 ICMP 數據包;
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
// 收到的未知的數據包。
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet;
複製代碼

注:以上回調方法中的 packet 數據包只包含了 ICMP header 和 sendPingWithData: 中傳入的數據,但不包含任何 IP 層的 header。

封裝了一個簡單SimplePing類,由於只有一個類,就不開repo了:

//
// PJPingManager.h
// DiDiData
//
// Created by PJ on 2018/5/25.
// Copyright © 2018年 Didi.Inc. All rights reserved.
//

#import <Foundation/Foundation.h>

typedef void(^PingSuccessCallback)();
typedef void(^PingFailureCallback)();

@interface IPLPingManager : NSObject

@property (nonatomic, copy) PingSuccessCallback pingSuccessCallback;
@property (nonatomic, copy) PingFailureCallback pingFailureCallback;

- (void)startPing;

@end

複製代碼
//
// PJPingManager.m
// DiDiData
//
// Created by PJ on 2018/5/25.
// Copyright © 2018年 Didi.Inc. All rights reserved.
//

#import "PJPingManager.h"
#import "SimplePing.h"
#include <netdb.h>

@interface PJPingManager ()<SimplePingDelegate>

@property (nonatomic, strong) SimplePing *pinger;
@property (nonatomic, strong) NSTimer *sendTimer;


@end

@implementation PJPingManager

- (instancetype)init {
    self = [super init];
    if (self) {
        NSString *hostName = @"your hostName";
        self.pinger = [[SimplePing alloc] initWithHostName:hostName];
        self.pinger.addressStyle = SimplePingAddressStyleAny;
        self.pinger.delegate = self;
    }
    return self;
}

- (void)startPing {
    [self start];
}

- (void)start {
    [self.pinger start];
}

- (void)stop {
    [self.pinger stop];
    self.pinger = nil;
    
    if ([self.sendTimer isValid])
    {
        [self.sendTimer invalidate];
    }
    self.sendTimer = nil;
}

- (void)sendPing {
    [self.pinger sendPingWithData:nil];
}

#pragma mark - pinger delegate

- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address {
    NSLog(@"pinging %@", [self displayAddressForAddress:address]);
    
    [self sendPing];
    
    
}

- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error {
    NSLog(@"failed: %@", [self shortErrorFromError:error]);
    
    [self stop];
    
    if (self.pingFailureCallback) {
        self.pingFailureCallback();
    }
}

- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
    NSLog(@"#%u sent", (unsigned int) sequenceNumber);
}

- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error {
    NSLog(@"#%u send failed: %@", (unsigned int) sequenceNumber, [self shortErrorFromError:error]);
    
    [self stop];
    
    if (self.pingFailureCallback) {
        self.pingFailureCallback();
    }
}

- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
    NSLog(@"#%u received, size=%zu", (unsigned int) sequenceNumber, (size_t) packet.length);
    
    [self stop];
    
    if (self.pingSuccessCallback) {
        self.pingSuccessCallback();
    }
}

- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet {
    NSLog(@"unexpected packet, size=%zu", (size_t) packet.length);
    
    [self stop];
    
    if (self.pingSuccessCallback) {
        self.pingSuccessCallback();
    }
}

#pragma mark - Others mothods

/** * 將ping接收的數據轉換成ip地址 * @param address 接受的ping數據 */
- (NSString *)displayAddressForAddress:(NSData *)address {
    int err;
    NSString *result;
    char hostStr[NI_MAXHOST];
    
    result = nil;
    
    if (address != nil) {
        err = getnameinfo([address bytes], (socklen_t)[address length], hostStr, sizeof(hostStr),
                          NULL, 0, NI_NUMERICHOST);
        if (err == 0) {
            result = [NSString stringWithCString:hostStr encoding:NSASCIIStringEncoding];
        }
    }
    
    if (result == nil) {
        result = @"?";
    }
    
    return result;
}

/* * 解析錯誤數據並翻譯 */
- (NSString *)shortErrorFromError:(NSError *)error {
    NSString *result;
    NSNumber *failureNum;
    int failure;
    const char *failureStr;
    
    result = nil;
    
    // Handle DNS errors as a special case.
    
    if ([[error domain] isEqual:(NSString *)kCFErrorDomainCFNetwork] &&
        ([error code] == kCFHostErrorUnknown)) {
        failureNum = [[error userInfo] objectForKey:(id)kCFGetAddrInfoFailureKey];
        if ([failureNum isKindOfClass:[NSNumber class]]) {
            failure = [failureNum intValue];
            if (failure != 0) {
                failureStr = gai_strerror(failure);
                if (failureStr != NULL) {
                    result = [NSString stringWithUTF8String:failureStr];
                }
            }
        }
    }
    
    if (result == nil) {
        result = [error localizedFailureReason];
    }
    if (result == nil) {
        result = [error localizedDescription];
    }
    if (result == nil) {
        result = [error description];
    }
    
    return result;
}

@end
複製代碼

給出的代碼中使用到的netdb庫爲Unix和Linux特有的頭文件,主要定義了與網絡有關的結構、變量類型、宏、函數等。這裏有篇在Unix中該函數庫相關介紹:pubs.opengroup.org/onlinepubs/…

NI_MAXHOST給出主機字符串存儲空間的最大長度,值爲1025;

NI_MAXSERV給出服務字符串存儲空間的最大長度,值爲32.

其中還有一些會使用到的宏,以下所示:

解釋
#define NI_NOFQDN 0x00000001 只返回FQDN的主機名部分
#define NI_NUMERICHOST 0x00000002 以數串格式返回主機字符串
#define NI_NAMEREQD 0x00000004 若不能從地址解析出名字則返回錯誤
#define NI_NUMERICSERV 0x00000008 以數串格式返回服務字符串
#define NI_NUMERICSCOPE 0x00000100 以數串格式返回範圍標識字符串
#define NI_DGRAM 0x00000010 數據報服務

使用方法:

  1. 新建ping類,複製以上代碼;
  2. 建立一個NSTimer類型屬性且初始化timer。self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(pingNetWork) userInfo:nil repeats:YES];。在這裏爲啥要初始化一個NSTimer呢?由於若是ping失敗後,也就是發送的測試報文成功,但一直沒收到響應的報文,此時卻不會有任何的回調方法告知咱們,而只ping一次結果也不許確,更況且ping花費的時間很是之短,故我在此加了個NSTimer屢次進行ping,或者也可使用performSelector進行延時判斷,通常0.3~0.8s的延時便可。若是在這期間內未收到響應則可視爲超時。
  3. 對應的方法爲:
- (void)pingNetWork{    
    self.pingManager = [[IPLPingManager alloc] init];
    self.pingManager.pingSuccessCallback = ^{
        
    };
    
    self.pingManager.pingFailureCallback = ^{

    };
    
    [self.pingManager startPing];
}
複製代碼

跑起工程後每秒都會ping目標主機,若是你不併想每秒都執行一次,再自定義一個屬性去標記吧。固然,你也可也徹底沒必要使用我提供的這個封裝,直接使用SimplePing自行編寫也行。

以上內容收錄在個人「iOS開發之路」repo中,若是你喜歡該repo,歡迎start、fork、提PR支持,持續更新。

相關文章
相關標籤/搜索