ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出

IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出 做 IP 白名单、按地区统计访问量、把访问日志落库迟早要把 IP 字符串转成整数。网上的写法基本就一行reduce看着没问题一上线就开始出怪事算出来是负数、范围查询漏数据、同一个地址在统计里出现两次。这篇把我实测过的七个坑记下来。环境Node 25.8、Go 1.25、Python 3。文中代码都是为这篇文章现写的示意代码。坑一192.168.1.1 算出来是负数最常见的写法constipToIntipip.split(.).reduce((acc,o)(acc8)Number(o),0)ipToInt(192.168.1.1)// -1062731519ipToInt(255.255.255.255)// -1原因是 JS 的位运算会先把数字转成32 位有符号整数。192 左移 24 位之后最高位是 1结果就成了负数。所有第一段 ≥ 128 的地址B 类、C 类、整个 192.168 内网全中招。两种改法结果一样// 1) 最后用无符号右移 0 位把结果按无符号解释constaip.split(.).reduce((acc,o)(acc8)Number(o),0)0// 2) 干脆不用位运算乘 256constbip.split(.).reduce((acc,o)acc*256Number(o),0)// 两个都是 3232235777我更倾向第二种读代码的人不用去想 0是干嘛的。坑二反过来转的时候和差一个符号整数转回点分十进制有人这么写constn3232235777constfirstn24// -64是有符号右移会把符号位往右填于是第一段成了 -64。要么用要么每段都 255constintToIpn[n24,(n16)255,(n8)255,n255].join(.)intToIp(3232235777)// 192.168.1.1我在 Node 里同时试了三种n 24是 -64(n 24) 255是 192n 24是 192。只有第一种是错的但它恰好是最顺手的写法。坑三数据库字段用 INT一半的地址存不进去IPv4 转成整数的范围是 0 到 42949672952³² − 1而 MySQL 的INT是有符号的上限 2147483647。凡是第一段 ≥ 128 的地址都超了。两个常见后果严格模式下直接插入报错有人为了能存进去在应用层先转成有符号数再存就是坑一那个负数于是库里一半是正数、一半是负数BETWEEN做网段查询时跨过 128.0.0.0 的范围会整段查不到。字段用INT UNSIGNED或BIGINT存的时候保证是非负数这事就结束了。Go 里也一样binary.BigEndian.Uint32拿到的是uint32别图省事转成int32ip:net.ParseIP(192.168.1.1).To4()n:binary.BigEndian.Uint32(ip)// 3232235777fmt.Println(int32(n))// -1062731519坑四Go 里忘了To4()结果是 0这个坑很隐蔽因为不报错ip:net.ParseIP(192.168.1.1)len(ip)// 16binary.BigEndian.Uint32(ip[:4])// 0binary.BigEndian.Uint32(ip.To4())// 3232235777net.ParseIP返回的是 16 字节的切片IPv4 被存成 IPv4-mapped IPv6 的形式前面 10 个 0 字节、2 个 0xff最后 4 字节才是地址。直接取前 4 字节拿到的永远是 0。所有地址都转成 0测试时如果只看有没有报错是发现不了的。新代码可以直接用net/netipnetip.ParseAddr(s)拿到Addr后As4()返回[4]byte类型上就不会搞混。坑五127.1、0177.0.0.1也是合法地址这是我觉得最值得记住的一条。把下面几个字符串交给浏览器的 URL 解析器for(consthof[127.1,0177.0.0.1,0x7f.1,2130706433,192.168.257])console.log(h,→,newURL(http://${h}/).hostname)实测输出输入解析结果127.1127.0.0.10177.0.0.1127.0.0.10 开头按八进制0x7f.1127.0.0.1十六进制2130706433127.0.0.1整数192.168.257192.168.1.1最后一段可以超过 255吃掉剩下的字节这套宽松规则来自老的inet_aton。Python 的socket.inet_aton(127.1)和socket.inet_aton(0177.0.0.1)也都返回7f000001。问题在于同一个地址可以有好几种写法而你代码里不同环节用的可能不是同一套规则。最直观的后果是统计口径乱掉日志里127.1和127.0.0.1被当成两个来源按字符串去重、按字符串匹配黑白名单都会漏。所以凡是要拿 IP 做比较、去重、匹配的地方都应该先解析成标准形式再比较而不是直接对原始字符串做。反过来严格的解析器会直接拒绝这些写法Go net.ParseIP(0177.0.0.1) → nil Go netip.ParseAddr(0177.0.0.1) → IPv4 field has octet with leading zero Go netip.ParseAddr(127.1) → IPv4 address too short Python ipaddress.ip_address(127.1) → does not appear to be an IPv4 or IPv6 address所以同一个字符串在 Go 里是非法的在浏览器和inet_aton里是本机。两边混用时要特别小心。坑六parseInt太宽容校验形同虚设自己手写解析时很多人用parseInt做每一段的转换再判断 0–255parseInt(1abc)// 1parseInt( 12 )// 12parseInt(0x1F)// 31parseInt()// NaNNumber()// 0parseInt遇到非数字字符就停前面能读出数字就算成功所以192.168.1.1abc每段都能合法通过。换成Number又有另一个问题空字符串是 0192.168..1会被当成192.168.0.1。稳妥的做法是先用正则把格式卡死再转数字constSEG(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)constIPV4newRegExp(^${SEG}(\\.${SEG}){3}$)IPV4.test(192.168.1.1)// trueIPV4.test(192.168.1.1abc)// falseIPV4.test(0177.0.0.1)// false不允许前导 0IPV4.test(192.168..1)// false前导 0 要不要允许取决于你的下游怎么解析。下游是inet_aton一类的宽松实现时0 开头会被当成八进制最省心的就是直接拒绝。坑七按字符串排序10.0.0.10 排在 10.0.0.2 前面日志里按 IP 排序或者前端表格点按 IP 排序[10.0.0.2,10.0.0.10,9.1.1.1].sort()// [10.0.0.10, 10.0.0.2, 9.1.1.1]字符串比较是逐字符的1 2所以.10跑到了.2前面9.x还排在10.x后面。按转换后的整数排就对了ips.sort((a,b)ipToInt(a)-ipToInt(b))数据库里同理存整数、按整数排序和范围查询比存字符串再做各种LIKE和截取要省事得多。顺手的工具排查日志时我经常要手动核对一个整数到底是哪个 IP。福兮的 IPv4 地址转换工具forxi.cn/hub/it-tools/ipv4就是做这一件事的点分地址和整数两个方向互转输出是无符号的192.168.1.1给的是 3232235777不会出现负数。说下它的局限只做 IPv4 和整数之间的互转不支持 IPv6也不做网段CIDR计算和子网划分输入请用标准的四段点分写法127.1这类缩写不在它的处理范围里。批量转换或者要做网段判断还是在代码里用上面的方法自己写。小结七个坑归成三类位运算的符号问题坑一、二、三JS 位运算是 32 位有符号的数据库 INT 也是有符号的IPv4 正好用满 32 位无符号最高位一碰就出事。解析规则不统一坑四、五、六inet_aton很宽松Go 和 Python 的新库很严格浏览器跟着宽松那一派。校验和使用必须用同一套规则最好都先归一化成标准形式。表示方式坑七存储、排序、范围查询都用整数展示时再转回字符串。IPv6 是 128 位JS 的 Number 装不下得用 BigInt规则也复杂得多::压缩、IPv4 映射地址、zone id那是另一篇的内容了。
返回列表