面试题中IP地址常用操作以及Go中的net包

1,472 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

引言

接上篇文章,如何实现查询IP属地信息?本文将接着讲解关于IP的一些操作,例如如何获取内网IP、公网IP?IP地址和数字的转换,判断是否是有效IP地址等等。

先来看一道面试题:

力扣 468. 验证IP地址

image.png

最直接的方法是使用内置函数和 try/catch 结构检查 IP 地址的正确性:在 Python 中使用 ipaddress ,在 Java 中使用 InetAddress ,在Golang中使用net.ParseIP

Java代码:

import java.net.*;
class Solution {
  public String validIPAddress(String IP) {
    try {
      return (InetAddress.getByName(IP) instanceof Inet6Address) ? "IPv6": "IPv4";
    } catch(Exception e) {}
    return "Neither";
  }
}

Python代码:

from ipaddress import ip_address, IPv6Address
class Solution:
    def validIPAddress(self, IP: str) -> str:
        try:
            return "IPv6" if type(ip_address(IP)) is IPv6Address else "IPv4"
        except ValueError:
            return "Neither"

但是在Golang中,To4()判断是允许前导零的,所以需要再判断一下:

Go代码:

func validIPAddress(queryIP string) string {
	ip := net.ParseIP(queryIP)
	if ip == nil {
		return "Neither"
	}
	if ip.To4() != nil {
		for _, s := range strings.Split(queryIP, ".") {
			if len(s) > 1 && s[0] == '0' {
				return "Neither"
			}
		}
		return "IPv4"
	}
	for _, s := range strings.Split(queryIP, ":") {
		if len(s) > 4 || s == "" {
			return "Neither"
		}
	}
	return "IPv6"
}

如果面试题的重点不是对于IP的处理,那么可以使用内置函数实现。但是如果这道题就是想考你对于字符串的操作,很显然是不能使用内置库函数的。

下面是直接翻译原题要求的解法:

  • 对于 IPv4 地址,通过界定符 . 将地址分为四块;对于 IPv6 地址,通过界定符 : 将地址分为八块。
  • 对于 IPv4 地址的每一块,检查它们是否在 0 - 255 内,且没有前置零。
  • 对于 IPv6 地址的每一块,检查其长度是否为 1 - 4 位的十六进制数。

其中IPv6使用正则表达式进行判断:

import (
   "regexp"
   "strconv"
   "strings"
)

func solve( IP string ) string {
   if validIPv4(IP) {
      return "IPv4"
   }else if validIPv6(IP) {
      return "IPv6"
   }else {
      return "Neither"
   }
}
func validIPv4(IP string) bool {
   strs := strings.Split(IP,".")
   if len(strs)!=4 {
      return false
   }
   for _, str := range strs {
      if len(str)>3 {
         return false
      }
      if len(str)>1 && str[0]=='0' {
         return false
      }
      val,err := strconv.Atoi(str)
      if err!= nil || val<0 || val > 255 {
         return false
      }
   }
   return true
}
func validIPv6(IP string) bool {
   strs := strings.Split(IP,":")
   if len(strs) != 8 {
      return false
   }
   for _, str := range strs {
      if len(str)>4 || len(str)==0 {
         return false
      }
      re := regexp.MustCompile(`^([0-9]|[a-f]|[A-F])+$`)
      if !re.MatchString(str) {
         return false
      }
   }
   return true
}

讲完了面试题,我们来总结一下常见关于IP的操作:

int和net.IP的转换

//将 int 转换成 netIP ([]byte)
func intToNetIP(ipnr int) net.IP {
   var bytes [4]byte
   bytes[0] = byte(ipnr >> 24)
   bytes[1] = byte(ipnr >> 16)
   bytes[2] = byte(ipnr >> 8)
   bytes[3] = byte(ipnr)

   return net.IPv4(bytes[0], bytes[1], bytes[2], bytes[3])
}

//将 netIP ([]byte) 转换成 uint32
func netIPToInt(ipnr net.IP) int {
   bits := strings.Split(ipnr.String(), ".")
   b0, _ := strconv.Atoi(bits[0])
   b1, _ := strconv.Atoi(bits[1])
   b2, _ := strconv.Atoi(bits[2])
   b3, _ := strconv.Atoi(bits[3])

   var sum int
   sum |= int(b0) << 24
   sum |= int(b1) << 16
   sum |= int(b2) << 8
   sum |= int(b3)
   return sum
}

int和IP(string)的转换

//ipv4地址转int
func ipv4ToInt(s string) int {
   arr := strings.Split(s,".")
   res := 0
   for i, str := range arr {
      val,_ := strconv.Atoi(str)
      val = val<<(24-8*i)
      res |= val
   }
   return res
}

//int转IPv4 string
func intToIpv4(ipInt int) string {
   return fmt.Sprintf("%d.%d.%d.%d",
      byte(ipInt>>24), byte(ipInt>>16), byte(ipInt>>8), byte(ipInt))
}

判断ip地址是否处于某个地址区间

// IpBetween 判断ip地址区间
func IpBetween(from net.IP, to net.IP, test net.IP) bool {
   if from == nil || to == nil || test == nil {
      fmt.Println("An ip input is nil")
      return false
   }

   from16 := from.To16()
   to16 := to.To16()
   test16 := test.To16()
   if from16 == nil || to16 == nil || test16 == nil {
      fmt.Println("An ip did not convert to a 16 byte")
      return false
   }
   //如果 from16<=test16<=to16 返回true
   if bytes.Compare(test16, from16) >= 0 && bytes.Compare(test16, to16) <= 0 {
      return true
   }
   return false
}

通过dns服务器8.8.8.8:80获取本地IP

// GetLocalIP 通过dns服务器8.8.8.8:80获取本地使用的ip
func GetLocalIP() string {
   // Dial() 在网络network上连接地址address,并返回一个Conn接口。
   conn, _ := net.Dial("udp", "8.8.8.8:80")
   defer conn.Close()
   localAddr := conn.LocalAddr().String()
   idx := strings.LastIndex(localAddr, ":")
   return localAddr[0:idx]
}

获取本机的公网IP

// GetMyPublicIP 通过访问http://myexternalip.com/raw获取公网ip
func GetMyPublicIP() string {
   resp, err := http.Get("http://myexternalip.com/raw")
   if err != nil {
      return ""
   }
   defer resp.Body.Close()
   content, _ := ioutil.ReadAll(resp.Body)
   return string(content)
}

获取本地IP,返回网络接口地址列表

// GetIntranetIp 获取本地IP,返回网络接口地址
func GetIntranetIp() {
   //InterfaceAddrs返回该系统的网络接口的地址列表。
   addrs, err := net.InterfaceAddrs()
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   for _, address := range addrs {
      // 检查ip地址判断是否回环地址
      // IsLoopback() 如果ip是环回地址,则返回真。
      if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
         if ipnet.IP.To4() != nil {
            fmt.Println("ip:", ipnet.IP.String())
         }
      }
   }
}

判断是否是公网ip

// IsPublicIP 判断是否是公网ip
func IsPublicIP(IP net.IP) bool {
   //如果ip是环回地址、链路本地组播地址、链路本地单播地址 返回false
   if IP.IsLoopback() || IP.IsLinkLocalMulticast() || IP.IsLinkLocalUnicast() {
      return false
   }

   //如果是内网ip返回false
   //tcp/ip协议中,专门保留了三个IP地址区域作为私有地址,其地址范围如下:
   //10.0.0.0/8:     10.0.0.0 ~ 10.255.255.255
   //172.16.0.0/12:   172.16.0.0 ~ 172.31.255.255
   //192.168.0.0/16:  192.168.0.0 ~ 192.168.255.255
   if ip4 := IP.To4(); ip4 != nil {
      switch true {
      case ip4[0] == 10:
         return false
      case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
         return false
      case ip4[0] == 192 && ip4[1] == 168:
         return false
      default:
         return true
      }
   }
   return false //ip4 == nil
}

golang提供的标准库:net包

推荐一个GO中文文档:studygolang.com/pkgdoc

type IP

type IP []byte

一个IP是一个单一的IP地址,是一个字节的片断。本包中的函数接受4字节(IPv4)或16字节(IPv6)的片断作为输入。

请注意,在本文档中,将一个IP地址称为IPv4地址或IPv6地址是该地址的语义属性,而不仅仅是字节片的长度:一个16字节的片子仍然可以是一个IPv4地址。

func IPv4

func IPv4(a, b, c, d byte) IP

IPv4返回IPv4地址a.b.c.d的IP地址(以16字节的形式)。

func ParseIP

func ParseIP(s string) IP

ParseIP 将 s 解析为一个 IP 地址,并返回结果。字符串s可以是点阵十进制("192.0.2.1")或IPv6("2001:db8::68")形式。如果s不是一个有效的IP地址的文本表示,ParseIP返回nil。

func (IP) [To16]

func (ip IP) To16() IP

To16将一个IP地址转换为16字节表示。如果ip不是一个IP地址(长度错误),To16会返回nil。

func (IP) [To4]

func (ip IP) To4() IP

To4将一个IPv4地址转换为4字节表示。如果ip不是IPv4地址,To4会返回nil。

func InterfaceAddrs

func InterfaceAddrs() ([]Addr, error)

InterfaceAddrs返回一个系统的单播接口地址列表。

返回的列表并不识别相关的接口;使用Interfaces和Interface.Addrs获取更多细节。

type IPNet 

type IPNet struct {
        IP   IP     // network number
        Mask IPMask // network mask
}

一个IPNet代表一个IP网络。