41、IP地址转换:inet_addr/inet_ntoa(旧)、inet_pton/inet_ntop(新)

做网络编程,IP地址的转换是躲不开的基本功。说白了,就是把人能读懂的字符串IP(比如"192.168.1.1")和机器能处理的二进制IP(32位或128位整数)之间来回倒腾。

我刚开始写网络程序那会儿,用的还是老一套的 inet_addrinet_ntoa。后来项目越做越大,发现这些老函数坑不少,才慢慢换成了新的 inet_ptoninet_ntop。今天咱们就把这两代API掰开揉碎了讲清楚。

为什么需要IP地址转换?

你想想看,struct sockaddr_in 里存的IP地址是 in_addr_t 类型,一个32位无符号整数。但我们在配置、显示、调试的时候,肯定要用点分十进制字符串。这两者之间必须有个桥梁。

举个例子,你写一个聊天程序,用户输入对方IP是"10.0.0.5",程序得把它转成 0x0A000005 才能塞进套接字结构体。反过来,收到数据包后,你想打印来源IP,又得把二进制转回字符串。

老一代API:inet_addr 和 inet_ntoa

这两个函数来自BSD套接字,历史悠久。我最早学网络编程时,教材里全是它们。

inet_addr:字符串 → 二进制

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

用法很简单:

struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("192.168.1.100");
if (addr.sin_addr.s_addr == INADDR_NONE) {
    // 转换失败
}

⚠️ 坑点来了

我曾经在项目中遇到过一个问题:inet_addr 返回 INADDR_NONE(即 0xFFFFFFFF)表示错误。但 255.255.255.255 这个广播地址的二进制也是 0xFFFFFFFF!也就是说,你没法区分是转换失败还是真的广播地址。这就是个大隐患。

inet_ntoa:二进制 → 字符串

char *inet_ntoa(struct in_addr in);

用法:

struct in_addr addr;
addr.s_addr = 0x0A000005;
char *ip_str = inet_ntoa(addr);
printf("IP: %s\n", ip_str);  // 输出 "10.0.0.5"

⚠️ 又一个坑

inet_ntoa 返回的指针指向一个静态缓冲区。你连续调用两次,第二次的结果会覆盖第一次的。我见过有人这样写:

printf("src=%s, dst=%s\n", inet_ntoa(src), inet_ntoa(dst));

结果两个IP打印出来一模一样!因为两个调用都用了同一个缓冲区,后执行的覆盖了先执行的。

新一代API:inet_pton 和 inet_ntop

POSIX标准后来推出了这两个函数,专门解决老API的痛点。我个人习惯,新项目一律用它们,老代码迁移时也会顺手改掉。

inet_pton:字符串 → 二进制(Presentation to Numeric)

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
  • af:地址族,AF_INETAF_INET6
  • src:点分十进制字符串
  • dst:指向 struct in_addrstruct in6_addr 的指针
  • 返回值:1成功,0输入格式无效,-1系统错误

示例:

struct sockaddr_in addr;
int ret = inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
if (ret == 1) {
    // 成功
} else if (ret == 0) {
    // 格式不对,比如 "999.999.999.999"
} else {
    // 系统错误,比如 af 参数不对
}

💡 我的建议

返回值判断要严谨。很多人只检查 ret <= 0,但0和-1的含义不同。调试时区分这两种情况,能帮你快速定位是用户输入问题还是代码bug。

inet_ntop:二进制 → 字符串(Numeric to Presentation)

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af:地址族
  • src:二进制IP地址
  • dst:输出缓冲区
  • size:缓冲区大小
  • 返回值:成功返回 dst 指针,失败返回 NULL

示例:

struct in_addr addr;
addr.s_addr = 0x0A000005;
char ip_str[INET_ADDRSTRLEN];

if (inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str))) {
    printf("IP: %s\n", ip_str);
} else {
    perror("inet_ntop");
}

🔑 关键点

缓冲区大小用 INET_ADDRSTRLEN(IPv4)或 INET6_ADDRSTRLEN(IPv6),别自己瞎写16或46。这些宏定义在 <netinet/in.h> 里,能保证够用。

新旧API对比一览

功能 旧API 新API 优势
字符串→二进制 inet_addr inet_pton 支持IPv6,错误区分明确
二进制→字符串 inet_ntoa inet_ntop 线程安全,可指定缓冲区
错误处理 返回值歧义 三态返回值 清晰无歧义
IPv6支持 不支持 原生支持 面向未来
线程安全 否(静态缓冲区) 是(调用者提供缓冲区) 多线程无忧

知识体系结构图

IP地址转换知识体系 IP地址转换 旧API:inet_addr / inet_ntoa 仅支持IPv4 静态缓冲区,线程不安全 错误返回值有歧义 新API:inet_pton / inet_ntop 支持IPv4和IPv6 调用者提供缓冲区,线程安全 三态返回值,错误清晰 建议:新项目一律使用 inet_pton / inet_ntop

避坑指南

我这些年踩过的坑,总结成几条,你写代码时多留个心眼:

  • 缓冲区大小别算错:IPv4用 INET_ADDRSTRLEN(16字节),IPv6用 INET6_ADDRSTRLEN(46字节)。别图省事写个 char buf[16],万一以后要支持IPv6呢?
  • 别混用新旧API:一个项目里统一用一套。我见过有人 inet_pton 转完,又用 inet_ntoa 打印,虽然能跑,但代码风格很割裂。
  • 检查返回值inet_pton 返回0表示输入格式无效,返回-1表示系统错误。别只判断 if (ret != 1) 就完事。
  • 多线程环境别用老APIinet_ntoa 的静态缓冲区在多线程下就是定时炸弹。我有个同事线上排查了一整天,最后发现是两个线程同时调 inet_ntoa 互相覆盖了。

💡 实用技巧

写一个通用的IP打印函数,内部用 inet_ntop,这样不管IPv4还是IPv6,调用方都不用操心细节:

void print_ip(const struct sockaddr *sa) {
    char buf[INET6_ADDRSTRLEN];
    const char *ret = NULL;
    
    if (sa->sa_family == AF_INET) {
        ret = inet_ntop(AF_INET, 
            &((struct sockaddr_in *)sa)->sin_addr, 
            buf, sizeof(buf));
    } else if (sa->sa_family == AF_INET6) {
        ret = inet_ntop(AF_INET6, 
            &((struct sockaddr_in6 *)sa)->sin6_addr, 
            buf, sizeof(buf));
    }
    
    if (ret) {
        printf("%s\n", buf);
    } else {
        printf("Unknown address family\n");
    }
}

总结

IP地址转换看着简单,但细节决定成败。老API inet_addrinet_ntoa 虽然还能用,但坑太多。新API inet_ptoninet_ntop 更安全、更清晰、更现代。

我个人建议:写新代码直接上 inet_pton/inet_ntop,维护老代码时遇到相关bug也顺手改掉。别给自己留隐患。

嗯,今天就聊到这儿。记住一点:网络编程里,字符串和二进制之间的转换,用对API能省一半的调试时间。


公众号:蓝海资料掘金营,微信deep3321