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_INET6
  • src:输入字符串或二进制地址
  • 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 本章知识体系图

下面这张图把本章的核心知识点串起来了,你可以对照着复习:

Socket地址结构知识体系 Socket地址结构知识体系 struct sockaddr_in sin_family = AF_INET sin_port (网络字节序) sin_addr (网络字节序) sin_zero (清零) 网络字节序(大端) htonl / htons ntohl / ntohs 点分十进制 ↔ 二进制 inet_pton (字符串→二进制) inet_ntop (二进制→字符串) 获取本机IP地址 ioctl / getifaddrs 核心:所有多字节数据在网络中必须使用大端序