【Linux】UDP Socket编程实战(四):地址转换函数深度解析

文章目录

    • [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(即-10xFFFFFFFF
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_INETAF_INET6
  • src :输入的in_addrin6_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_addrinet_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时,问自己:

  1. 是否在多线程环境?→ 必须改成inet_ntop
  2. 是否保存了返回的指针?→ 必须立刻复制或改用inet_ntop
  3. 是否在同一个表达式里调用多次?→ 危险,可能被覆盖

看到inet_addr时,问自己:

  1. 是否需要严格错误检测?→ 考虑inet_pton
  2. 是否需要支持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 容易混淆的点

  1. inet_ntoa的返回值能不能free:不能,它指向函数内部的静态区域,free会崩溃。但也不会泄漏,因为是静态变量。

  2. 为什么inet_pton返回值是1而不是0表示成功 :0表示格式错误,-1表示系统错误(如地址族不支持),1才是成功。不要用if(ret)判断,要用if(ret == 1)

  3. htons转换后的值能不能直接打印 :不能。转换后是网络字节序,直接打印会是乱码。要么先用ntohs转回来,要么用%u打印但值是反的。

  4. inet_addr的返回值能不能直接赋给int :类型是in_addr_t(通常是uint32_t),可以赋给uint32_t,但不要赋给int(有符号),可能溢出。

  5. 为什么INET_ADDRSTRLEN是16不是15 :最长的IPv4地址是255.255.255.255(15字符),加上'\0'就是16。

  6. inet_ntop的第一个参数能不能写错 :如果传AF_INET6但第二个参数是in_addr指针,不会编译报错(都是void*),但运行时会读取错误的内存大小,导致崩溃或乱码。


💬 总结:这一篇把地址转换函数的所有细节都讲透了。从函数原型到使用场景,从单线程陷阱到多线程验证,从IPv4到IPv6,涵盖了实战中所有可能遇到的问题。掌握了这些,你不仅能写出正确的网络代码,还能在review别人代码时发现潜在的bug。至此,UDP Socket编程的四篇实战全部完成,从基础API到实战项目,从回调设计到线程安全,从简单Echo到复杂聊天室,整个UDP编程的知识体系已经建立完整。
👍 点赞、收藏与分享:四篇UDP实战到这里就全部结束了!如果这个系列帮你系统地掌握了UDP网络编程,请点赞收藏!后面如果写TCP系列,会在UDP的基础上讲清楚三次握手、四次挥手、滑动窗口等TCP特有的机制。感谢阅读!

相关推荐
峥嵘life2 小时前
Android16 【GSI】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·运维·服务器·学习
达子6662 小时前
Ubuntu的Gparted 无法扩展内存 报错umount: /sdb1: target is busy
linux·运维·ubuntu
王老师青少年编程2 小时前
2022信奥赛C++提高组csp-s复赛真题及题解:星战
c++·真题·csp·信奥赛·csp-s·提高组·星战
lisanmengmeng2 小时前
cephadm 17.2.5安装部署 (二)
linux·运维·服务器·ceph
dump linux2 小时前
Linux 显示服务器与合成器架构详解
linux·驱动开发·3d
科技块儿2 小时前
跨境业务使用IP数据云IP地址查询定位库判断用户IP是否来自制裁地区?
网络·网络协议·tcp/ip
Blurpath住宅代理2 小时前
如何在Python爬虫中使用代理IP?从配置到轮换的完整指南
网络·爬虫·python·住宅ip·住宅代理·动态住宅代理
GS8FG2 小时前
鲁班猫2,lubancat2,linux sdk4.19整编出现的镜像源的问题修复
linux
燃于AC之乐2 小时前
【Linux系统编程】基础IO:从文件本质到系统操作
linux·文件系统·系统调用·文件描述符·基础io