IP 地址获取小集(iOS+macOS)

# 方法一

通用的做法是使用 getifaddrs 方法获取到指向本机网络接口信息的一个链表,然后通过遍历该链表拿到当前的网卡对应的 IP 地址。 iPhone 上的无线网卡是 en0 ,所以拿到 ifaddrs 结构体后去判断其 name 成员变量的时候,判定 name 是否等于 en0

如果是运行 iPhone 模拟器的话 en0 是代表当前电脑上 en0 对应的网卡地址。

#include <netinet/in.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <arpa/inet.h>
...
struct ifaddrs *address = NULL;
struct ifaddrs *temp = NULL;
if (0 != getifaddrs(&address)) {
    NSLog(@"getifaddrs error = %s",strerror(errno));
    return;
}
temp = address;
while (temp->ifa_next != NULL) {
    NSString *if_name = [NSString stringWithUTF8String:temp->ifa_name];
    if ([if_name isEqualToString:@"en0"]) {
        struct sockaddr *ifa_addr = temp->ifa_addr;
        if (ifa_addr->sa_family == AF_INET) {
            struct sockaddr_in *in_address = (struct sockaddr_in *)ifa_addr;
            char *ip_str = inet_ntoa(in_address->sin_addr);
            NSLog(@"ip %@",[NSString stringWithFormat:@"%s",ip_str]);
        }
    }
    temp = temp->ifa_next;
}
freeifaddrs(address);

# 方法二

iOS 上还有一种方式来获取,拿到通过解析当前机器的 hostname 返回地址链表中的第一个地址作为主 IP。但是这种方法并不适用于 macOS.

+ (NSString *)hostname {
    char baseHostName[256];
    int success = gethostname(baseHostName, 255);
    if (success != 0) return nil;
    baseHostName[255] = '\0';
#if !TARGET_IPHONE_SIMULATOR
    return [NSString stringWithFormat:@"%s.local", baseHostName];
#else
    return [NSString stringWithFormat:@"%s", baseHostName];
#endif
}

// return IP Address
+ (NSString *)localIPAddress {
    struct hostent *host = gethostbyname([[self hostname] UTF8String]);
    if (!host) {herror("resolv"); return nil;}
    struct in_addr **list = (struct in_addr **)host->h_addr_list;
    return [NSString stringWithCString:inet_ntoa(*list[0]) encoding:NSUTF8StringEncoding];
}

# 方法三

macOS 不能使用第二种方法,但是可以使用第一种的方法,但是有一个问题,在带有网口的 Mac 电脑上,en0 是代表以太网网卡地址,你获取到的 IP 也是该网卡地址,若想要只获取 WIFI 的对应网卡地址,则需要使用别的关键词,通常是 en1 去筛选。

可以使用 networksetup -listallhardwareports 命令来查看当前的网络硬件配置。

Hardware Port: Ethernet
Device: en0
Ethernet Address: 68:5b:35:a5:a2:d5

Hardware Port: Wi-Fi
Device: en1
Ethernet Address: c8:e0:eb:4c:f9:bf
....

所以想获取 WIFI 的网卡地址话需要将 en0 替换为 en1,但是这并不是通用的方案,因为在 Mac Air 上 en0 再次代表了 WIFIenX 这种判断方式不够靠谱,我们想要一个更加通用的解决方案。

通用的解决方案如下,获取系统配置,通过匹配 AirPort 关键字来进行匹配,里面的关键字参考 System Configuration Programming Guidelines (opens new window)

//1. 创建 dynamic store.
SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, (__bridge CFStringRef)@"example", NULL, NULL);
//2. 通过 keystore 从 dynamic store 中获取数据.
NSString *keyStr = @"Setup:/Network/Global/IPv4";
NSDictionary *global = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)keyStr);
//3. 根据 IPv4 的全局数据拿所有 services
NSArray *services = [global objectForKey:@"ServiceOrder"];
//4. 取出 wifi 相关 service.
//Note: wifi serviceId 和 '/Library/Preferences/SystemConfiguration/preferences.plist' 里 wifi serviceID 一样.
for (NSString *serviceID in services) {
    NSString *serviceKeyStr = [NSString stringWithFormat:@"State:/Network/Service/%@/IPv4",serviceID];
    NSDictionary *serviceInfo = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)serviceKeyStr);
    if (serviceInfo) {
        NSString *interfaceKeyStr = [NSString stringWithFormat:@"Setup:/Network/Service/%@/Interface",serviceID];
        NSDictionary *globalInterface = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)interfaceKeyStr);
        if ([[globalInterface objectForKey:@"Hardware"] isEqualToString:@"AirPort"] ) {
            NSString *wifiAddress = [[serviceInfo objectForKey:@"Addresses"] objectAtIndex:0];
            return wifiAddress;
        }
    }
}

但是这个方法并不适用于 iOS,因为 iOS 不支持上面的 API

# 获取空闲端口

大概思路:

  1. 创建套接字。

    int local_sock = socket(temp_addr->ifa_addr->sa_family,SOCK_DGRAM, 0);
    
  2. 构建本地 sockaddr 的时候 sin_port 变量传入 0 。

    struct sockaddr_in local_addr;
    bzero(&local_addr, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = 0;
    local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
  3. 然后 bind 这个套接字到本地 sockaddr 地址。

    result = bind(local_sock, (struct sockaddr *)&local_addr, sizeof(local_addr));
    
  4. 如果 bind 成功,接下来通过 getsockname 方法来获取 sockaddr 地址。这时候 sockaddr 里的 sin_port 即为没有被占用的端口。

    getsockname(local_sock, (struct sockaddr *)&sin, &len)
    

有点空手套白狼的意思。

# macOS 上监听 WIFI 切换引起 IP 变化的方法

之前使用的 AFNetworkReachabilityManager 不太符合要求,有的时候切换了 WIFI 也不能及时进行变化。 CoreWLAN 框架提供了一个监听的方法

- (BOOL)startMonitoringEventWithType:(CWEventType)type error:(out NSError * _Nullable *)error;

不幸的是,这个方法不能再沙盒之外使用。

经过搜索发现一个方法能完整实现改功能,其实就是上面的获取 IP 的第三个方法,只不过需要增加点东西。具体可以参考下面代码。

static NSString *wifiServiceKeyStr;
//监听 WIFI 变化的回调方法
void dynamicStoreChange(SCDynamicStoreRef store,CFArrayRef changedKeys, void * __nullable info) {
    NSLog(@"store %@,changedKeys %@,info %s",store,changedKeys,info);
    for (NSString *changeKey in (__bridge NSArray *)changedKeys) {
        if ([changeKey isEqualToString:wifiServiceKeyStr]) {
            NSDictionary *serviceInfo = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)changeKey);
            NSLog(@"now server info %@",serviceInfo);
        }
    }
}


//1. 创建 dynamic store.
SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, (__bridge CFStringRef)@"example", dynamicStoreChange, NULL);

NSString *interfaceKey = @"State:/Network/Interface";
NSDictionary *interfaces = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)interfaceKey);
NSLog(@"interfaces = %@",interfaces);

//2. 通过 keystore 从 dynamic store 中获取数据.
NSString *keyStr = @"Setup:/Network/Global/IPv4";
NSDictionary *global = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)keyStr);
//3. 根据 IPv4 的全局数据拿所有 services
NSArray *services = [global objectForKey:@"ServiceOrder"];
//4. 取出 wifi 相关 service.
//Note: wifi serviceId 和 '/Library/Preferences/SystemConfiguration/preferences.plist' 里 wifi serviceID 一样.
//注意不能用 en0 和 en1 进行判断。en0 和 en1 在 iMac 和 macAir 上有对应不同的网卡。
for (NSString *service in services) {
    NSString *serviceKeyStr = [NSString stringWithFormat:@"State:/Network/Service/%@/IPv4",service];
    NSDictionary *serviceInfo = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)serviceKeyStr);
    if (serviceInfo) {
        NSString *interfaceKeyStr = [NSString stringWithFormat:@"Setup:/Network/Service/%@/Interface",service];
        NSDictionary *globalInterface = (__bridge NSDictionary *)SCDynamicStoreCopyValue(store, (__bridge CFStringRef)interfaceKeyStr);
        if ([[globalInterface objectForKey:@"Hardware"] isEqualToString:@"AirPort"] ) {
            NSLog(@"service info = %@",serviceInfo);
            wifiServiceKeyStr = serviceKeyStr;
        }
    }
}
//5. 监听 wifi 相关 service 的变化.
if (wifiServiceKeyStr) {
    SCDynamicStoreSetNotificationKeys(store, NULL,(__bridge CFArrayRef)@[wifiServiceKeyStr]);
    CFRunLoopAddSource(CFRunLoopGetCurrent(),
                       SCDynamicStoreCreateRunLoopSource(NULL, store, 0),
                       kCFRunLoopCommonModes);
}