41、IP地址转换:inet_addr/inet_ntoa(旧)、inet_pton/inet_ntop(新)
做网络编程,IP地址的转换是躲不开的基本功。说白了,就是把人能读懂的字符串IP(比如"192.168.1.1")和机器能处理的二进制IP(32位或128位整数)之间来回倒腾。
我刚开始写网络程序那会儿,用的还是老一套的 inet_addr 和 inet_ntoa。后来项目越做越大,发现这些老函数坑不少,才慢慢换成了新的 inet_pton 和 inet_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_INET或AF_INET6src:点分十进制字符串dst:指向struct in_addr或struct 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支持 | 不支持 | 原生支持 | 面向未来 |
| 线程安全 | 否(静态缓冲区) | 是(调用者提供缓冲区) | 多线程无忧 |
知识体系结构图
避坑指南
我这些年踩过的坑,总结成几条,你写代码时多留个心眼:
- 缓冲区大小别算错:IPv4用
INET_ADDRSTRLEN(16字节),IPv6用INET6_ADDRSTRLEN(46字节)。别图省事写个char buf[16],万一以后要支持IPv6呢? - 别混用新旧API:一个项目里统一用一套。我见过有人
inet_pton转完,又用inet_ntoa打印,虽然能跑,但代码风格很割裂。 - 检查返回值:
inet_pton返回0表示输入格式无效,返回-1表示系统错误。别只判断if (ret != 1)就完事。 - 多线程环境别用老API:
inet_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_addr 和 inet_ntoa 虽然还能用,但坑太多。新API inet_pton 和 inet_ntop 更安全、更清晰、更现代。
我个人建议:写新代码直接上 inet_pton/inet_ntop,维护老代码时遇到相关bug也顺手改掉。别给自己留隐患。
嗯,今天就聊到这儿。记住一点:网络编程里,字符串和二进制之间的转换,用对API能省一半的调试时间。