如何在Swift中获取IP地址

28

如何获取本地IP地址?

我尝试使用这个Obj C示例:如何在 iPhone 上以编程方式获取 IP 地址

当我到达函数getifaddrs()时,我无法继续。我无法使用该功能。

是否有其他方法来解决这个问题,或者我走错了路?


Swift 不能直接与 C 进行通信,你必须使用 Objective-C 作为桥梁。 - akashivskyy
1
@akashivskyy:一般来说,这不是真的。 许多C函数都暴露给Swift。然而,getifaddrs()似乎对Swift不可见,可能使用的数据结构不兼容。 - Martin R
@akashivskyy 这并不完全正确,有很多情况下 Swift 可以直接与 C 通信,例如几乎所有的 CoreFoundation API 都是可访问的。话虽如此,在这种特定情况下,由于需要指针算术和结构解包,编写一个 Objective-C 粘合层可能会更容易些。 - David Berry
5个回答

55
在讨论中发现,OP需要在Mac上获取接口地址,而不是我最初认为的iOS设备。问题中引用的代码检查WiFi接口iPhone上的“en0”接口名称。在Mac上,检查任何“运行中”的接口更有意义。 因此,我已经重写了答案。它现在是检测任何连接的网络代码的Swift翻译。
getifaddrs()的定义在<ifaddrs.h>中,这个头文件默认没有包含。因此,你需要创建一个桥接头文件并添加。
#include <ifaddrs.h>
以下函数返回一个包含所有本地“启动和运行”网络接口的名称的数组。
func getIFAddresses() -> [String] {
    var addresses = [String]()

    // Get list of all interfaces on the local machine:
    var ifaddr : UnsafeMutablePointer<ifaddrs> = nil
    if getifaddrs(&ifaddr) == 0 {

        // For each interface ...
        var ptr = ifaddr
        while ptr != nil {
            defer { ptr = ptr.memory.ifa_next } 

            let flags = Int32(ptr.memory.ifa_flags)
            let addr = ptr.memory.ifa_addr.memory

            // Check for running IPv4, IPv6 interfaces. Skip the loopback interface.
            if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) {
                if addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) {

                    // Convert interface address to a human readable string:
                    var hostname = [CChar](count: Int(NI_MAXHOST), repeatedValue: 0)
                    if (getnameinfo(ptr.memory.ifa_addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count),
                        nil, socklen_t(0), NI_NUMERICHOST) == 0) {
                        if let address = String.fromCString(hostname) {
                            addresses.append(address)
                        }
                    }
                }
            }
        }
        freeifaddrs(ifaddr)
    }

    return addresses
}
Swift 3更新:除了采用适合Swift 3的众多变化之外,现在迭代所有接口也可以使用新的泛化sequence()函数:
func getIFAddresses() -> [String] {
    var addresses = [String]()

    // Get list of all interfaces on the local machine:
    var ifaddr : UnsafeMutablePointer<ifaddrs>?
    guard getifaddrs(&ifaddr) == 0 else { return [] }
    guard let firstAddr = ifaddr else { return [] }

    // For each interface ...
    for ptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
        let flags = Int32(ptr.pointee.ifa_flags)
        let addr = ptr.pointee.ifa_addr.pointee

        // Check for running IPv4, IPv6 interfaces. Skip the loopback interface.
        if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) {
            if addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) {

                // Convert interface address to a human readable string:
                var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
                if (getnameinfo(ptr.pointee.ifa_addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count),
                                nil, socklen_t(0), NI_NUMERICHOST) == 0) {
                    let address = String(cString: hostname)
                    addresses.append(address)
                }
            }
        }
    }

    freeifaddrs(ifaddr)
    return addresses
}

当我运行它时,我发现tmpAddr中的所有实例变量都具有值0。因此,它们没有指向任何东西。我认为这不正确?if语句当然会失败。 - Mads Gadeberg
@MadsGadeberg:奇怪,我已经在设备和模拟器上测试了代码,并且它对我有效。-哪个if语句失败了? - Martin R
我们要检查 sa_family 是否为 AF_INET。我是否连接在 WiFi 上有关系吗? - Mads Gadeberg
同样的结果。问题在于指针tmpAddr.ifa_addr的值为0x0,因此它没有指向任何实例。因此if语句当然会失败。我还不太擅长苹果的世界,但难道我们分配tmpAddr的方式不正确吗?(不知道pt.memory是什么) - Mads Gadeberg
@MartinR 谢谢您的快速回复,我还需要蜂窝网络的IP地址。您知道怎么做吗? - cengo
显示剩余15条评论

2

回答“是否有其他获取ip地址的方式替代getifaddrs()函数?”

是的,有另一种方法。您也可以通过使用ioctl()来获取IP地址。最干净的方法是用C语言编写它,然后在Swift中进行包装。因此,请考虑以下内容:

创建一个C源文件(Xcode应该会与.h一起创建它),并记得将头文件添加到项目的桥接头文件中,或者如果您有Cocoa Touch框架,则添加到总头文件中。

将以下内容添加到您的.c源文件中,并将方法的声明添加到.h文件中:

#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>

int _interfaceAddressForName(char* interfaceName, struct sockaddr* interfaceAddress) {

    struct ifreq ifr;
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    ifr.ifr_addr.sa_family = AF_INET;

    strncpy(ifr.ifr_name, interfaceName, IFNAMSIZ-1);

    int ioctl_res;
    if ( (ioctl_res = ioctl(fd, SIOCGIFADDR, &ifr)) < 0){
        return ioctl_res;
    }

    close(fd);
    memcpy(interfaceAddress, &ifr.ifr_addr, sizeof(struct sockaddr));
    return 0;
}

您的Swift包装器可能如下所示:

您的 Swift 包装器可能如下所示:

public enum Error:ErrorType {
    case IOCTLFailed(Int32)
    case StringIsNotAnASCIIString
}

public func interfaceAddress(forInterfaceWithName interfaceName: String) throws -> sockaddr_in {

    guard let cString = interfaceName.cStringUsingEncoding(NSASCIIStringEncoding) else {
        throw Error.StringIsNotAnASCIIString
    }

    let addressPtr = UnsafeMutablePointer<sockaddr>.alloc(1)
    let ioctl_res = _interfaceAddressForName(strdup(cString), addressPtr)
    let address = addressPtr.move()
    addressPtr.dealloc(1)

    if ioctl_res < 0 {
        throw Error.IOCTLFailed(errno)
    } else {
        return unsafeBitCast(address, sockaddr_in.self)
    }
}

然后你可以在你的代码中使用它,如下所示:
let interfaceName = "en0"
do {
    let wlanInterfaceAddress = try interfaceAddress(forInterfaceWithName: interfaceName)
    print(String.fromCString(inet_ntoa(wlanInterfaceAddress.sin_addr))!)
} catch {
    if case Error.IOCTLFailed(let errno) = error where errno == ENXIO {
        print("interface(\(interfaceName)) is not available")
    } else {
        print(error)
    }
}

en0 通常是您需要的接口,代表 WLAN

如果您还需要知道可用接口名称,可以使用 if_indextoname()

public func interfaceNames() -> [String] {

    let MAX_INTERFACES = 128;

    var interfaceNames = [String]()
    let interfaceNamePtr = UnsafeMutablePointer<Int8>.alloc(Int(IF_NAMESIZE))
    for interfaceIndex in 1...MAX_INTERFACES {
        if (if_indextoname(UInt32(interfaceIndex), interfaceNamePtr) != nil){
            if let interfaceName = String.fromCString(interfaceNamePtr) {
                interfaceNames.append(interfaceName)
            }
        } else {
            break
        }
    }

    interfaceNamePtr.dealloc(Int(IF_NAMESIZE))
    return interfaceNames
}

2
   func getIfConfigOutput() -> [String:String] {
    let cmd = "for i in $(ifconfig -lu); do if ifconfig $i | grep -q \"status: active\" ; then echo $i; fi; done"
    let interfaceString = shell(cmd)
    let interfaceArray = interfaceString.components(separatedBy: "\n")
    var finalDictionary:[String:String] = [String:String]()
    for (i,_) in interfaceArray.enumerated() {
        if (interfaceArray[i].hasPrefix("en")){
            let sp = shell("ifconfig \(interfaceArray[i]) | grep \"inet \" | grep -v 127.0.0.1 | cut -d\\  -f2")
          finalDictionary[interfaceArray[i]] = sp.replacingOccurrences(of: "\n", with: "")
        }
    }
  print(finalDictionary)
    return finalDictionary
}
func shell(_ args: String) -> String {
    var outstr = ""
    let task = Process()
    task.launchPath = "/bin/sh"
    task.arguments = ["-c", args]
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    if let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
        outstr = output as String
    }
    task.waitUntilExit()
    return outstr
}

这会对您有所帮助。代码返回一个包含接口和与之关联的IP地址的字典。


1

代码适用于Swift 5+和iOS 13+

要获取设备在打开Wifi有线网络移动数据时的IP地址,请使用以下方法:

func getIPAddress() -> String? {
    var address : String?

    // Get list of all interfaces on the local machine:
    var ifaddr : UnsafeMutablePointer<ifaddrs>?
    guard getifaddrs(&ifaddr) == 0 else { return nil }
    guard let firstAddr = ifaddr else { return nil }

    // For each interface ...
    for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
        let interface = ifptr.pointee

        // Check for IPv4 or IPv6 interface:
        let addrFamily = interface.ifa_addr.pointee.sa_family
        if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {

            // Check interface name:
            // wifi = ["en0"]
            // wired = ["en2", "en3", "en4"]
            // cellular = ["pdp_ip0","pdp_ip1","pdp_ip2","pdp_ip3"]
            
            let name = String(cString: interface.ifa_name)
            if  name == "en0" || name == "en2" || name == "en3" || name == "en4" || name == "pdp_ip0" || name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" {

                // Convert interface address to a human readable string:
                var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
                getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
                            &hostname, socklen_t(hostname.count),
                            nil, socklen_t(0), NI_NUMERICHOST)
                address = String(cString: hostname)
            }
        }
    }
    freeifaddrs(ifaddr)

    return address
}

这里的代码/检查适用于Wifi有线网络移动数据,代码如下:

if name == "en0" || name == "en2" || name == "en3" || name == "en4" || name == "pdp_ip0" || name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3"

希望对您有所帮助! :)


1
在Swift 5中,您可以直接使用ioctl来获取给定接口的IP地址详细信息,而无需诉诸于'C'语言。如下所示:
private enum AddressRequestType {
    case ipAddress
    case netmask
}

// From BDS ioccom.h
// Macro to create ioctl request
private static func _IOC (_ io: UInt32, _ group: UInt32, _ num: UInt32, _ len: UInt32) -> UInt32 {
    let rv = io | (( len & UInt32(IOCPARM_MASK)) << 16) | ((group << 8) | num)
    return rv
}

// Macro to create read/write IOrequest
private static func _IOWR (_ group: Character , _ num : UInt32, _ size: UInt32) -> UInt32 {
    return _IOC(IOC_INOUT, UInt32 (group.asciiValue!), num, size)
}

private static func _interfaceAddressForName (_ name: String, _ requestType: AddressRequestType) throws -> String {
    
    var ifr = ifreq ()
    ifr.ifr_ifru.ifru_addr.sa_family = sa_family_t(AF_INET)
    
    // Copy the name into a zero padded 16 CChar buffer
    
    let ifNameSize = Int (IFNAMSIZ)
    var b = [CChar] (repeating: 0, count: ifNameSize)
    strncpy (&b, name, ifNameSize)
    
    // Convert the buffer to a 16 CChar tuple - that's what ifreq needs
    ifr.ifr_name = (b [0], b [1], b [2], b [3], b [4], b [5], b [6], b [7], b [8], b [9], b [10], b [11], b [12], b [13], b [14], b [15])
    
    let ioRequest: UInt32 = {
        switch requestType {
        case .ipAddress: return _IOWR("i", 33, UInt32(MemoryLayout<ifreq>.size))    // Magic number SIOCGIFADDR - see sockio.h
        case .netmask: return _IOWR("i", 37, UInt32(MemoryLayout<ifreq>.size))      // Magic number SIOCGIFNETMASK
        }
    } ()
    
    if ioctl(socket(AF_INET, SOCK_DGRAM, 0), UInt(ioRequest), &ifr) < 0 {
        throw POSIXError (POSIXErrorCode (rawValue: errno) ?? POSIXErrorCode.EINVAL)
    }
    
    let sin = unsafeBitCast(ifr.ifr_ifru.ifru_addr, to: sockaddr_in.self)
    let rv = String (cString: inet_ntoa (sin.sin_addr))
    
    return rv
}

public static func getInterfaceIPAddress (interfaceName: String) throws -> String {
    return try _interfaceAddressForName(interfaceName, .ipAddress)
}

public static func getInterfaceNetMask (interfaceName: String) throws -> String {
    return try _interfaceAddressForName(interfaceName, .netmask)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接