4. Socket地址结构:struct sockaddr_in、网络字节序与地址转换
各位同学,今天我们来聊聊Socket编程里最基础、也最容易踩坑的一块——地址结构。说白了,就是怎么告诉操作系统你要跟谁通信。
我记得刚入行那会儿,第一次写UDP通信程序,死活连不上对端。查了两天,最后发现是字节序搞反了。嗯,从那以后,我对这块内容就格外上心。
4.1 struct sockaddr_in:IPv4地址结构体
在Linux/Unix系统里,Socket API用struct sockaddr_in来表示一个IPv4的端点地址。你想想看,通信双方要建立连接,至少得知道对方的IP和端口吧?这个结构体就是干这个的。
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址族,AF_INET
in_port_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IPv4地址,网络字节序
unsigned char sin_zero[8]; // 填充字段,全0
};
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
};
这里有几个关键点,我一个个说。
- sin_family:必须填
AF_INET,表示IPv4协议族。别写成AF_UNIX或AF_INET6,否则bind或connect会报错。 - sin_port:端口号,范围0~65535。注意,这里必须用网络字节序存储,不能直接写
8080。 - sin_addr:IP地址,也是一个网络字节序的32位整数。通常我们用
inet_pton来转换。 - sin_zero:填充字段,保证结构体大小与
struct sockaddr一致。使用前务必用bzero或memset清零。
我曾经踩过的坑:忘记清零sin_zero字段。在有些系统上,内核会检查这个字段,非零值会导致bind失败。所以我的习惯是:定义结构体后,第一件事就是
bzero(&addr, sizeof(addr))。
4.2 网络字节序:大端序
为什么要有网络字节序?你想想看,不同CPU存储多字节数据的方式不一样。x86用小端序(低位字节在低地址),而网络协议规定用大端序(高位字节在低地址)。如果不统一,A机器发出去的端口号8080,B机器读出来可能变成32815。
说白了,网络字节序就是大端序。所有多字节数据(端口号、IP地址)在网络上传输时,都必须是大端序。
| 概念 | 说明 | 常见平台 |
|---|---|---|
| 主机字节序 | CPU本地存储方式 | x86小端、ARM可配置 |
| 网络字节序 | 大端序,高位字节在前 | 所有网络协议 |
4.3 htonl / htons / ntohl / ntohs 函数
这四个函数就是用来在主机字节序和网络字节序之间转换的。名字很好记:
- h:host,主机
- n:network,网络
- s:short,16位(端口号)
- l:long,32位(IP地址)
#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位网络→主机
我个人习惯这样用:
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 端口号转网络字节序
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
bzero(addr.sin_zero, sizeof(addr.sin_zero));
小技巧:如果主机本身就是大端序,这些函数什么也不做。但千万别依赖这个,写跨平台代码时一定要用。
4.4 inet_pton / inet_ntop:点分十进制与二进制互转
我们习惯用"192.168.1.1"这种点分十进制表示IP,但内核需要的是32位二进制。这两个函数就是干这个的。
#include <arpa/inet.h>
// 字符串→二进制(presentation to numeric)
int inet_pton(int af, const char *src, void *dst);
// 二进制→字符串(numeric to presentation)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明:
af:地址族,IPv4用AF_INET,IPv6用AF_INET6src:输入字符串或二进制地址dst:输出缓冲区size:输出缓冲区大小(inet_ntop用)
举个例子:
struct sockaddr_in addr;
addr.sin_family = AF_INET;
// 将"192.168.1.100"转为二进制
if (inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr) <= 0) {
perror("inet_pton failed");
exit(1);
}
// 反过来,将二进制转回字符串
char ip_str[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str)) == NULL) {
perror("inet_ntop failed");
exit(1);
}
printf("IP: %s\n", ip_str);
注意:老函数
inet_addr和inet_ntoa已经过时了,不支持IPv6,而且inet_ntoa返回静态缓冲区,不可重入。我建议你一律用inet_pton/inet_ntop。
4.5 获取本机IP地址
有时候我们需要知道本机的IP地址,比如做服务注册或日志记录。常用的方法有两种:
方法一:用ioctl获取
#include <sys/ioctl.h>
#include <net/if.h>
#include <unistd.h>
int get_local_ip(char *ip_buf, size_t len) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return -1;
struct ifreq ifr;
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ); // 网卡名
if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
close(fd);
return -1;
}
struct sockaddr_in *addr = (struct sockaddr_in *)&ifr.ifr_addr;
inet_ntop(AF_INET, &addr->sin_addr, ip_buf, len);
close(fd);
return 0;
}
方法二:用getifaddrs(推荐)
#include <ifaddrs.h>
int get_local_ip_v2(char *ip_buf, size_t len) {
struct ifaddrs *ifaddr, *ifa;
if (getifaddrs(&ifaddr) < 0) return -1;
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL) continue;
if (ifa->ifa_addr->sa_family != AF_INET) continue;
// 跳过回环地址
struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr;
if (ntohl(addr->sin_addr.s_addr) == INADDR_LOOPBACK) continue;
inet_ntop(AF_INET, &addr->sin_addr, ip_buf, len);
freeifaddrs(ifaddr);
return 0;
}
freeifaddrs(ifaddr);
return -1; // 没找到非回环地址
}
我个人的建议:用
getifaddrs更灵活,可以遍历所有网卡,还能区分IPv4/IPv6。不过记得调用freeifaddrs释放内存。
4.6 本章知识体系图
下面这张图把本章的核心知识点串起来了,你可以对照着复习: