Linux网络编程基础(地址结构)
一、Socket地址的本质
Socket最初的含义是一个IP地址和端口对(ip, port),它唯一地表示了使用TCP通信的一端,这就是Socket地址。在网络编程中,需要一种标准化的数据结构来描述这个地址对,这就引出了各种地址结构体。
二、通用Socket地址结构
2.1 sockaddr结构体
Socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h> struct sockaddr { sa_family_t sa_family; // 地址族类型 char sa_data[14]; // 存放socket地址值(14字节) };sa_family:地址族类型,通常与协议族类型对应,如AF_INET表示IPv4协议sa_data:用于存放具体的socket地址值,但只有14字节
2.2 为什么需要通用地址结构?
socket网络编程接口中使用的是sockaddr作为地址参数类型,但不同的协议族(如IPv4、IPv6、UNIX域)有不同的地址格式和长度。14字节的sa_data根本无法完全容纳多数协议族的地址值(例如IPv6地址就需要26字节)。因此,Linux定义了新的通用socket地址结构体sockaddr_storage:
#include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[128 - sizeof(__ss_align)]; };这个结构体不仅提供了足够大的空间(128字节)用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
三、专用Socket地址结构
通用socket地址结构体在设置与获取IP地址和端口号时需要执行繁琐的位置操作,因此Linux为各个协议族提供了专门的socket地址结构体。
3.1 IPv4专用地址结构:sockaddr_in
这是最常用的地址结构,用于IPv4网络通信:
struct sockaddr_in { sa_family_t sin_family; // 地址族:AF_INET u_int16_t sin_port; // 端口号,要用网络字节序表示 struct in_addr sin_addr; // IPv4地址结构体 }; struct in_addr { u_int32_t s_addr; // IPv4地址,要用网络字节序表示 };各成员的作用:
sin_family:必须设置为AF_INET,表示IPv4协议族sin_port:16位端口号,需要使用htons()函数转换为网络字节序sin_addr.s_addr:32位IP地址,需要使用htonl()或inet_addr()等函数转换为网络字节序
3.2 IPv6专用地址结构:sockaddr_in6
用于IPv6网络通信:
struct sockaddr_in6 { sa_family_t sin6_family; // 地址族:AF_INET6 u_int16_t sin6_port; // 端口号,要用网络字节序表示 u_int32_t sin6_flowinfo; // 流信息,应设置为0 struct in6_addr sin6_addr; // IPv6地址结构体 u_int32_t sin6_scope_id; // scope ID,尚处于实验阶段 }; struct in6_addr { unsigned char sa_addr[16]; // IPv6地址,要用网络字节序表示 };3.3 UNIX本地域协议地址结构:sockaddr_un
用于本地进程间通信(UNIX域套接字):
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; // 地址族:AF_UNIX char sun_path[108]; // 文件路径名 };3.4 地址结构的实际使用
所有专用socket地址类型的变量在实际使用中都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr。例如:
struct sockaddr_in server_addr; // 初始化server_addr... bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));四、主机字节序与网络字节序
4.1 字节序的概念
现代CPU的累加器一次都能装载至少4字节(32位机),这4个字节在内存中的排列顺序会影响它们被累加器装载成的整数的值,这就是字节序问题。
字节序分为两种:
- 大端字节序(Big Endian):整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处
- 小端字节序(Little Endian):整数的高位字节存储在内存的高地址处,低位字节存储在内存的低地址处
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
4.2 网络字节序的由来
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释数据。解决问题的办法是:发送端总是把要发送的数据转化成大端字节序后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此,大端字节序也称为网络字节序。
4.3 字节序转换函数
Linux提供了4个函数来完成主机字节序和网络字节序之间的转换:
#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位短整型函数名含义:
h表示host(主机),n表示network(网络)l表示32位长整数(long),s表示16位短整数(short)- 例如
htonl表示将32位的长整数从主机字节序转换为网络字节序
实际应用中:
- 长整型函数(htonl/ntohl)通常用来转换IP地址
- 短整型函数(htons/ntohs)用来转换端口号
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
五、IP地址转换函数
5.1 传统IPv4地址转换函数
以下函数用于将点分十进制字符串表示的IPv4地址和网络字节序整数表示的IPv4地址之间进行转换:
#include <arpa/inet.h> in_addr_t inet_addr(const char *strptr); // 字符串→32位整数 int inet_aton(const char *cp, struct in_addr *inp); // 字符串→结构体 char *inet_ntoa(struct in_addr in); // 结构体→字符串inet_addr():将点分十进制字符串IP转换为32位网络字节序整数,失败返回INADDR_NONEinet_aton():功能同inet_addr(),但将结果存储于inp指向的地址结构中,成功返回1,失败返回0inet_ntoa():将32位整数IP转换为点分十进制字符串,注意:该函数内部用一个静态变量存储转化结果,函数返回值指向该静态内存,因此inet_ntoa是不可重入的(线程不安全)
5.2 通用地址转换函数(推荐使用)
以下函数同时适用于IPv4和IPv6地址,且是可重入的(线程安全):
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);inet_pton():将字符串IP地址转换为二进制格式,af指定协议族(AF_INET或AF_INET6),成功返回1,无效地址返回0,错误返回-1inet_ntop():将二进制地址转换为字符串格式,需要指定输出缓冲区的大小size,返回指向dst的指针
推荐使用inet_pton和inet_ntop替代传统函数,因为它们更安全、更通用。
六、地址结构的使用示例
以下是一个典型的TCP服务器端初始化地址结构的示例:
struct sockaddr_in server_addr; // 1. 清零结构体 memset(&server_addr, 0, sizeof(server_addr)); // 2. 设置地址族 server_addr.sin_family = AF_INET; // 3. 设置端口号(转换为网络字节序) server_addr.sin_port = htons(8080); // 4. 设置IP地址(绑定所有本地网卡) server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 或绑定指定IP:inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); // 5. 绑定地址到套接字 bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));七、总结
Linux网络编程中的地址结构体系可以分为三个层次:
| 层次 | 结构体 | 用途 |
|---|---|---|
| 通用地址 | sockaddr | 作为所有socket API的参数类型 |
| 扩展通用地址 | sockaddr_storage | 提供足够的空间容纳所有协议族的地址 |
| 专用地址 | sockaddr_in/sockaddr_in6/sockaddr_un | 分别用于IPv4、IPv6、UNIX域通信 |
在编程实践中,程序员通常使用专用地址结构(如sockaddr_in)来设置地址信息,然后通过强制类型转换将其传递给接受通用地址结构参数的API函数。同时,务必注意使用字节序转换函数(htons、htonl等)和IP地址转换函数(推荐inet_pton/inet_ntop)来确保数据的正确解释。
