文章目录
-
- [UDP Socket编程实战(四):地址转换函数深度解析](#UDP Socket编程实战(四):地址转换函数深度解析)
- 一、为什么需要地址转换
-
- [1.1 三种IP地址表示形式](#1.1 三种IP地址表示形式)
- [二、字符串 → in_addr 的转换](#二、字符串 → in_addr 的转换)
-
- [2.1 inet_addr:简单但有缺陷](#2.1 inet_addr:简单但有缺陷)
-
- [2.1.1 函数原型](#2.1.1 函数原型)
- [2.1.2 使用示例](#2.1.2 使用示例)
- [2.1.3 潜在问题:`255.255.255.255` 的二义性](#2.1.3 潜在问题:
255.255.255.255的二义性) - [2.1.4 适用场景](#2.1.4 适用场景)
- [2.2 inet_pton:现代标准推荐](#2.2 inet_pton:现代标准推荐)
-
- [2.2.1 函数原型](#2.2.1 函数原型)
- [2.2.2 使用示例](#2.2.2 使用示例)
- [2.2.3 IPv6支持](#2.2.3 IPv6支持)
- [2.2.4 错误处理更明确](#2.2.4 错误处理更明确)
- [2.2.5 适用场景](#2.2.5 适用场景)
- [2.3 两者对比总结](#2.3 两者对比总结)
- [三、in_addr → 字符串 的转换](#三、in_addr → 字符串 的转换)
-
- [3.1 inet_ntoa:方便但危险](#3.1 inet_ntoa:方便但危险)
-
- [3.1.1 函数原型](#3.1.1 函数原型)
- [3.1.2 使用示例](#3.1.2 使用示例)
- [3.1.3 静态存储区的陷阱](#3.1.3 静态存储区的陷阱)
- [3.1.4 多线程场景的灾难](#3.1.4 多线程场景的灾难)
- [3.1.5 为什么有些系统上看起来没问题](#3.1.5 为什么有些系统上看起来没问题)
- [3.1.6 适用场景](#3.1.6 适用场景)
- [3.2 inet_ntop:线程安全的正确选择](#3.2 inet_ntop:线程安全的正确选择)
-
- [3.2.1 函数原型](#3.2.1 函数原型)
- [3.2.2 使用示例](#3.2.2 使用示例)
- [3.2.3 缓冲区大小的常量](#3.2.3 缓冲区大小的常量)
- [3.2.4 多线程完全安全](#3.2.4 多线程完全安全)
- [3.2.5 IPv6支持](#3.2.5 IPv6支持)
- [3.2.6 适用场景](#3.2.6 适用场景)
- [3.3 两者对比总结](#3.3 两者对比总结)
- 四、实战验证:多线程测试
-
- [4.1 测试inet_ntoa的线程问题](#4.1 测试inet_ntoa的线程问题)
- [4.2 测试inet_ntop的安全性](#4.2 测试inet_ntop的安全性)
- 五、字节序转换函数
-
- [5.1 为什么地址转换和字节序绑定](#5.1 为什么地址转换和字节序绑定)
- [5.2 函数名的记忆技巧](#5.2 函数名的记忆技巧)
- [5.3 使用场景](#5.3 使用场景)
-
- [5.3.1 端口号转换](#5.3.1 端口号转换)
- [5.3.2 从网络地址提取端口号](#5.3.2 从网络地址提取端口号)
- [5.3.3 自定义协议的长度字段](#5.3.3 自定义协议的长度字段)
- [5.4 什么时候不需要转换](#5.4 什么时候不需要转换)
-
- [5.4.1 字符串](#5.4.1 字符串)
- [5.4.2 单字节数据](#5.4.2 单字节数据)
- [5.4.3 浮点数(需要特殊处理)](#5.4.3 浮点数(需要特殊处理))
- 六、实战建议与最佳实践
-
- [6.1 函数选择的原则](#6.1 函数选择的原则)
- [6.2 代码review时的检查点](#6.2 代码review时的检查点)
- [6.3 封装的建议](#6.3 封装的建议)
- 七、本篇总结
-
- [7.1 核心要点](#7.1 核心要点)
- [7.2 容易混淆的点](#7.2 容易混淆的点)
UDP Socket编程实战(四):地址转换函数深度解析
💬 开篇 :前三篇实战中,我们多次用到地址转换函数------
inet_addr把字符串转成网络地址,inet_ntoa把网络地址转回字符串。但这些函数的细节、适用场景、潜在陷阱,我们都是个主题讲透:四个核心函数的对比、字节序转换的底层原理、线程安全的实战验证、IPv6的支持差异。理解了这些,你不仅能写出正确的代码,还能在code review时看出别人代码里的隐患。👍 点赞、收藏与分享:这篇是对整个UDP编程系列的收尾,也是实战中最容易踩坑的地方。如果对你有帮助,请点赞收藏!
🚀 循序渐进:从函数原型到使用场景,从单线程到多线程,从IPv4到IPv6,一步步理解地址转换的所有细节。
一、为什么需要地址转换
1.1 三种IP地址表示形式
在网络编程中,IP地址有三种表示方式:
人类可读的字符串:
bash
"192.168.1.100"
"127.0.0.1"
这是我们在配置文件、命令行参数、日志输出中使用的格式。点分十进制,直观易懂。
网络传输的二进制格式:
bash
0xC0A80164 // 192.168.1.100 的十六进制
0x7F000001 // 127.0.0.1 的十六进制
这是在网络上实际传输的格式,32位整数,按网络字节序(大端)存储。
内核使用的结构体:
c
struct in_addr {
uint32_t s_addr; // 32位整数
};
操作系统内核用这个结构体来表示IP地址,s_addr字段就是网人类可读的字符串
- 命令行参数 :用户输入
./server 192.168.1.1 8080,要把字符串转成网络地址
这就需要在三种表示之间来回转换。地址转换函数就是干这个的。
二、字符串 → in_addr 的转换
2.1 inet_addr:简单但有缺陷
2.1.1 函数原型
c
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
- 参数 :点分十进制字符串,比如
"192.168.1.1" - 返回值 :网络字节序的32位整数。失败返回
INADDR_NONE(即-1或0xFFFFFFFF)
2.1.2 使用示例
c
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("192.168.1.100");
if (addr.sin_addr.s_addr == INADDR_NONE) {
fprintf(stderr, "Invalid IP address\n");
return -1;
}
一步到位:字符串解析 + 字节序转换,都帮你做了。
2.1.3 潜在问题:255.255.255.255 的二义性
255.255.255.255 是一个合法的广播地址,它的32位整数表示就是0xFFFFFFFF。
但也是INADDR_NONE,值也是0xFFFFFFFF`。
c
in_addr_t result = inet_addr("255.255.255.255");
// result == 0xFFFFFFFF
in_addr_t result2 = inet_addr("invalid_ip");
// result2 == 0xFFFFFFFF
// 无法区分是合法地址还是转换失败
大部分场景不受影响,因为255.255.255.255很少用到。但如果你的代码需要严格区分合法和非法,就不能用inet_addr。
2.1.4 适用场景
✅ 适合:
- 简单场景,IP地址不可能是
255.255.255.255 - 单线程程序
- 只需要支持IPv4
❌ 不适合:
- 需要严格错误检测
- 需要支持IPv6
- 多线程环境(虽然本身线程安全,但后续处理可能有问题)
2.2 inet_pton:现代标准推荐
2.2.1 函数原型
c
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
-
af :地址族,
AF_INET(IPv4)或AF_INET6(IPv6) -
src:输入字符串
-
dst :输出缓冲区,指向
struct in_addr(IPv4)或struct in6_addr(IPv6) -
返回值:
- 成功返回
1 - 格式错误返回
0 - 系统错误返回
-1,并设置errno
- 成功返回
2.2.2 使用示例
c
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
int ret = inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
if (ret != 1) {
if (ret == 0) {
fprintf(stderr, "Invalid IP format\n");
} else {
perror("inet_pton");
}
return -1;
}
2.2.3 IPv6支持
c
struct sockaddr_in6 addr6;
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(8080);
int ret = inet_pton(AF_INET6, "2001:db8::1", &addr6.sin6_addr);
if (ret == 1) {
printf("IPv6 address converted successfully\n");
}
同一个函数,通过第一个参数区分IPv4和IPv6。这种设计符合扩展性原则。
2.2.4 错误处理更明确
c
int ret = inet_pton(AF_INET, "999.999.999.999", &addr.sin_addr);
// ret == 0,明确表示格式错误
ret = inet_pton(AF_INET, "255.255.255.255", &addr.sin_addr);
// ret == 1,成功,addr.sin_addr.s_addr == 0xFFFFFFFF
ret = inet_pton(AF_INET6, "192.168.1.1", &addr6.sin6_addr);
// ret == 0,IPv4地址不能用AF_INET6转换
返回值能清楚区分三种情况:成功、格式错误、系统错误。
2.2.5 适用场景
✅ 适合:
- 生产环境代码
- 需要支持IPv6
- 需要严格的错误处理
- 多线程环境(线程安全)
❌ 不适合:
- 对古老系统的兼容性有要求(Windows XP之前不支持)
2.3 两者对比总结
| 特性 | inet_addr | inet_pton |
|---|---|---|
| 返回值 | 直接返回整数 | 通过输出参数 |
| 错误检测 | 模糊(255.255.255.255冲突) | 明确(返回0表示格式错误) |
| IPv6支持 | ✗ | ✓ |
| 线程安全 | ✓ | ✓ |
| 可移植性 | 更好(老系统也支持) | 稍差(较新标准) |
| 推荐度 | 简单场景可用 | 生产环境首选 |
三、in_addr → 字符串 的转换
3.1 inet_ntoa:方便但危险
3.1.1 函数原型
c
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
- 参数 :
struct in_addr结构体 - 返回值:指向点分十进制字符串的指针
3.1.2 使用示例
c
struct sockaddr_in addr;
// ... recvfrom填充了addr ...
char *ip = inet_ntoa(addr.sin_addr);
printf("Received from %s\n", ip);
看起来很简单,一行搞定。
3.1.3 静态存储区的陷阱
陷阱一:多次调用互相覆盖
c
struct sockaddr_in addr1, addr2;
addr1.sin_addr.s_addr = inet_addr("192.168.1.1");
addr2.sin_addr.s_addr = inet_addr("10.0.0.1");
char *ip1 = inet_ntoa(addr1.sin_addr); // "192.168.1.1"
char *ip2 = inet_ntoa(addr2.sin_addr); // "10.0.0.1"
printf("IP1: %s\n", ip1); // 打印 "10.0.0.1" ← 被!
printf("IP2: %s\n", ip2); // 打印 "10.0.0.1"
两个指针指向同一块内存,第二次调用覆盖了第一次的结果。
正确的做法:立刻复制
c
char *ip1 = inet_ntoa(addr1.sin_addr);
char ip1_copy[INET_ADDRSTRLEN];
strncpy(ip1_copy, ip1, sizeof(ip1_copy));
char *ip2 = inet_ntoa(addr2.sin_addr);
char ip2_copy[INET_ADDRSTRLEN];
strncpy(ip2_copy, ip2, sizeof(ip2_copy));
printf("IP1: %s\n", ip1_copy); // 正确
printf("IP2: %s\n", ip2_copy); // 正确
或者用C++的string:
c++
std::string ip1 = inet_ntoa(addr1.sin_addr); // 构造时就复制了
std::string ip2 = inet_ntoa(addr2.sin_addr);
std::cout << "IP1: " << ip1 << std::endl; // 正确
陷阱二:函数调用中的临时性
c
// 错误示例
printf("From %s to %s\n",
inet_ntoa(src_addr.sin_addr),
inet_ntoa(dst_addr.sin_addr));
// 可能打印 "From 10.0.0.2 to 10.0.0.2"
C语言不保证函数参数的求值顺序。如果先求值第二个参数,再求值第一个,结果就会被覆盖。
正确写法:
c
char *src_ip = inet_ntoa(src_addr.sin_addr);
char src_ip_copy[INET_ADDRSTRLEN];
strncpy(src_ip_copy, src_ip, sizeof(src_ip_copy));
char *dst_ip = inet_ntoa(dst_addr.sin_addr);
printf("From %s to %s\n", src_ip_copy, dst_ip);
3.1.4 多线程场景的灾难
c
// 线程1
char *ip1 = inet_ntoa(addr1.sin_addr); // "192.168.1.1"
// ... 此时被调度 ...
// 线程2(同时运行)
char *ip2 = inet_ntoa(addr2.sin_addr); // "10.0.0.1",覆盖了线程1的结果
// 线程1恢复
printf("%s\n", ip1); // 打印 "10.0.0.1",数据错乱
两个线程共享同一个静态缓冲区,谁后调用谁就覆盖前面的。
3.1.5 为什么有些系统上看起来没问题
现代glibc可能用了线程局部存储(Thread-Local Storage,TLS):
c
// 某些glibc的实现(简化版)
char *inet_ntoa(struct in_addr in)
{
static __thread char buf[INET_ADDRSTRLEN]; // 每个线程有自己的buf
// ... 转换逻辑 ...
return buf;
}
用了__thread关键字,每个线程有自己的静态变量副本,不会互相覆盖。
但这不是标准保证的,依赖实现细节:
- 不同操作系统实现不同
- 不同glibc版本实现不同
- 老版本系统肯定没有TLS
所以不能依赖这个行为。
3.1.6 适用场景
✅ 勉强可以用:
- 单线程
- 立刻使用,不保存指针
- 简单调试代码
❌ 绝对不能用:
- 多线程环境
- 需要保存结果
- 生产环境代码
3.2 inet_ntop:线程安全的正确选择
3.2.1 函数原型
c
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
- af :地址族,
AF_INET或AF_INET6 - src :输入的
in_addr或in6_addr结构体指针 - dst:输出缓冲区(由调用者提供)
- size:缓冲区大小
- 返回值 :成功返回
dst指针,失败返回NULL
3.2.2 使用示例
c
struct sockaddr_in addr;
// ... recvfrom填充了addr ...
char ip[INET_ADDRSTRLEN]; // IPv4地址最长15字符 + '\0' = 16
if (inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip)) != NULL) {
printf("Received from %s\n", ip);
} else {
perror("inet_ntop");
}
3.2.3 缓冲区大小的常量
c
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16 // IPv4: "xxx.xxx.xxx.xxx\0"
#define INET6_ADDRSTRLEN 46 // IPv6: "xxxx:xxxx:...:xxxx\0"
不要硬编码16或46,用这两个宏,代码更清晰。
3.2.4 多线程完全安全
c
// 线程1
char ip1[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr1.sin_addr, ip1, sizeof(ip1));
printf("Thread1: %s\n", ip1);
// 线程2(同时运行)
char ip2[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr2.sin_addr, ip2, sizeof(ip2));
printf("Thread2: %s\n", ip2);
每个线程用自己栈上的缓冲区,完全隔离,不会互相干扰。
3.2.5 IPv6支持
c
struct sockaddr_in6 addr6;
// ... 填充addr6 ...
char ip6[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &addr6.sin6_addr, ip6, sizeof(ip6)) != NULL) {
printf("IPv6 address: %s\n", ip6);
}
同一套API,只改地址族参数,就能支持IPv4和IPv6。
3.2.6 适用场景
✅ 适合所有场景:
- 多线程环境
- 生产环境代码
- 需要支持IPv6
- 需要保存结果
❌ 没有缺点,唯一"麻烦"是需要自己提供缓冲区,但这正是线程安全的保证。
3.3 两者对比总结
| 特性 | inet_ntoa | inet_ntop |
|---|---|---|
| 缓冲区管理 | 内部静态缓冲区 | 调用者提供 |
| 多次调用 | 互相覆盖 | 独立不干扰 |
| 线程安全 | ✗(标准不保证) | ✓ |
| IPv6支持 | ✗ | ✓ |
| 使用复杂度 | 简单(一行) | 稍复杂(需要声明缓冲区) |
| 推荐度 | 已过时 | 强烈推荐 |
四、实战验证:多线程测试
4.1 测试inet_ntoa的线程问题
c
#include <stdio.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
void* thread_func1(void* arg)
{
struct in_addr addr;
addr.s_addr = inet_addr("192.168.1.1");
for (int i = 0; i < 10000; i++) {
char *ip = inet_ntoa(addr);
if (strcmp(ip, "192.168.1.1") != 0) {
printf("Thread1 corrupted: %s (should be 192.168.1.1)\n", ip);
}
usleep(1); // 增加冲突概率
}
return NULL;
}
void* thread_func2(void* arg)
{
struct in_addr addr;
addr.s_addr = inet_addr("10.0.0.254");
for (int i = 0; i < 10000; i++) {
char *ip = inet_ntoa(addr);
if (strcmp(ip, "10.0.0.254") != 0) {
printf("Thread2 corrupted: %s (should be 10.0.0.254)\n", ip);
}
usleep(1);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_func1, NULL);
pthread_create(&tid2, NULL, thread_func2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("Test completed\n");
return 0;
}
在老系统或者没有TLS的glibc上,会看到大量错误输出:
Thread1 corrupted: 10.0.0.254 (should be 192.168.1.1)
Thread2 corrupted: 192.168.1.1 (should be 10.0.0.254)
...
在新系统(如CentOS 7+)上可能不会报错,但不能依赖。
4.2 测试inet_ntop的安全性
c
void* thread_func1_safe(void* arg)
{
struct in_addr addr;
addr.s_addr = inet_addr("192.168.1.1");
for (int i = 0; i < 10000; i++) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip, sizeof(ip));
if (strcmp(ip, "192.168.1.1") != 0) {
printf("Thread1 corrupted: %s\n", ip);
}
usleep(1);
}
return NULL;
}
void* thread_func2_safe(void* arg)
{
struct in_addr addr;
addr.s_addr = inet_addr("10.0.0.254");
for (int i = 0; i < 10000; i++) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip, sizeof(ip));
if (strcmp(ip, "10.0.0.254") != 0) {
printf("Thread2 corrupted: %s\n", ip);
}
usleep(1);
}
return NULL;
}
无论什么系统,都不会有任何错误输出。因为每个线程用自己的栈缓冲区。
五、字节序转换函数
5.1 为什么地址转换和字节序绑定
前面的inet_addr和inet_pton,内部都做了字节序转换。但端口号怎么办?端口号也是多字节整数,也需要转换。
这就需要另外一组函数:
c
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 主机序 → 网络序 (32位)
uint16_t htons(uint16_t hostshort); // 主机序 → 网络序 (16位)
uint32_t ntohl(uint32_t netlong); // 网络序 → 主机序 (32位)
uint16_t ntohs(uint16_t netshort); // 网络序 → 主机序 (16位)
5.2 函数名的记忆技巧
h= host(主机)n= network(网络)l= long(32位)s= short(16位)
所以:
htonl= host to network long(主机序转网络序,32位)ntohs= network to host short(网络序转主机序,16位)
5.3 使用场景
5.3.1 端口号转换
c
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 主机序 → 网络序
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
端口号用htons转换。如果不转,在小端机器上8080会被理解成0x1F90(字节序颠倒),实际绑定的端口变成了41021。
5.3.2 从网络地址提取端口号
c
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer, &len);
uint16_t port = ntohs(peer.sin_port); // 网络序 → 主机序
printf("Received from port %u\n", port);
从网络收到的端口号是网络字节序,要用ntohs转回主机序才能正确打印。
5.3.3 自定义协议的长度字段
c
// 发送端
uint32_t msg_len = 1024;
uint32_t net_len = htonl(msg_len);
send(sockfd, &net_len, sizeof(net_len), 0);
send(sockfd, msg_data, msg_len, 0);
// 接收端
uint32_t net_len;
recv(sockfd, &net_len, sizeof(net_len), 0);
uint32_t msg_len = ntohl(net_len);
char *msg_data = malloc(msg_len);
recv(sockfd, msg_data, msg_len, 0);
自定义协议里的长度、序列号等整数字段,都要转换字节序。
5.4 什么时候不需要转换
5.4.1 字符串
c
char message[] = "Hello";
send(sockfd, message, strlen(message), 0);
字符串是字节数组,没有多字节的概念,不需要转换。
5.4.2 单字节数据
c
uint8_t flags = 0x01;
send(sockfd, &flags, sizeof(flags), 0);
只有一个字节,不存在高低位顺序,不需要转换。
5.4.3 浮点数(需要特殊处理)
浮点数的表示比整数复杂得多,不能简单用字节序转换。如果要传输浮点数,要么:
- 转成字符串传输(
sprintf/sscanf) - 用序列化库(protobuf、JSON)
- 定义自己的序列化规则
六、实战建议与最佳实践
6.1 函数选择的原则
字符串 → in_addr:
c
// 推荐
int ret = inet_pton(AF_INET, ip_str, &addr.sin_addr);
if (ret != 1) { /* 错误处理 */ }
// 简单场景可用
addr.sin_addr.s_addr = inet_addr(ip_str);
in_addr → 字符串:
c
// 强烈推荐
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
// 不推荐(除非简单调试)
char *ip = inet_ntoa(addr.sin_addr);
6.2 代码review时的检查点
看到inet_ntoa时,问自己:
- 是否在多线程环境?→ 必须改成
inet_ntop - 是否保存了返回的指针?→ 必须立刻复制或改用
inet_ntop - 是否在同一个表达式里调用多次?→ 危险,可能被覆盖
看到inet_addr时,问自己:
- 是否需要严格错误检测?→ 考虑
inet_pton - 是否需要支持IPv6?→ 必须用
inet_pton
6.3 封装的建议
可以封装一层,统一使用新函数:
c++
class InetAddr {
public:
// 构造时转换,内部存储已转换好的结果
InetAddr(const std::string& ip, uint16_t port) {
memset(&addr_, 0, sizeof(addr_));
addr_.sin_family = AF_INET;
addr_.sin_port = htons(port);
if (inet_pton(AF_INET, ip.c_str(), &addr_.sin_addr) != 1) {
throw std::runtime_error("Invalid IP address");
}
}
// 提供字符串格式的getter
std::string ip() const {
char buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr_.sin_addr, buf, sizeof(buf));
return std::string(buf);
}
uint16_t port() const {
return ntohs(addr_.sin_port);
}
const sockaddr_in& addr() const { return addr_; }
private:
sockaddr_in addr_;
};
这样上层代码完全不用操心字节序和转换函数的选择。
七、本篇总结
7.1 核心要点
四个转换函数:
inet_addr:字符串 → in_addr,简单但有255.255.255.255二义性inet_pton:字符串 → in_addr,支持IPv6,错误检测更明确,推荐inet_ntoa:in_addr → 字符串,静态缓冲区陷阱,多线程不安全inet_ntop:in_addr → 字符串,调用者提供缓冲区,线程安全,强烈推荐
字节序转换:
htonl/htons:主机序 → 网络序ntohl/ntohs:网络序 → 主机序- 端口号、长度字段等多字节整数必须转换
- 字符串和单字节数据不需要转换
线程安全:
inet_ntoa返回静态缓冲区,多线程会互相覆盖- 虽然某些系统用了TLS,但不能依赖
inet_ntop由调用者提供缓冲区,天然线程安全
IPv6支持:
inet_pton/inet_ntop通过第一个参数区分IPv4和IPv6- 缓冲区大小用
INET_ADDRSTRLEN(16)或INET6_ADDRSTRLEN(46)
7.2 容易混淆的点
-
inet_ntoa的返回值能不能free:不能,它指向函数内部的静态区域,free会崩溃。但也不会泄漏,因为是静态变量。
-
为什么inet_pton返回值是1而不是0表示成功 :0表示格式错误,-1表示系统错误(如地址族不支持),1才是成功。不要用
if(ret)判断,要用if(ret == 1)。 -
htons转换后的值能不能直接打印 :不能。转换后是网络字节序,直接打印会是乱码。要么先用
ntohs转回来,要么用%u打印但值是反的。 -
inet_addr的返回值能不能直接赋给int :类型是
in_addr_t(通常是uint32_t),可以赋给uint32_t,但不要赋给int(有符号),可能溢出。 -
为什么INET_ADDRSTRLEN是16不是15 :最长的IPv4地址是
255.255.255.255(15字符),加上'\0'就是16。 -
inet_ntop的第一个参数能不能写错 :如果传
AF_INET6但第二个参数是in_addr指针,不会编译报错(都是void*),但运行时会读取错误的内存大小,导致崩溃或乱码。
💬 总结:这一篇把地址转换函数的所有细节都讲透了。从函数原型到使用场景,从单线程陷阱到多线程验证,从IPv4到IPv6,涵盖了实战中所有可能遇到的问题。掌握了这些,你不仅能写出正确的网络代码,还能在review别人代码时发现潜在的bug。至此,UDP Socket编程的四篇实战全部完成,从基础API到实战项目,从回调设计到线程安全,从简单Echo到复杂聊天室,整个UDP编程的知识体系已经建立完整。
👍 点赞、收藏与分享:四篇UDP实战到这里就全部结束了!如果这个系列帮你系统地掌握了UDP网络编程,请点赞收藏!后面如果写TCP系列,会在UDP的基础上讲清楚三次握手、四次挥手、滑动窗口等TCP特有的机制。感谢阅读!