1. 套接字地址结构
1.1 IPv4套接字地址结构
IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在<netinet/in.h>头文件中。下边给出它的定义:
1)struct in_addr专门用来存储IP地址,对于IPv4来说,IP地址为32位无符号整数。其定义如下:
注:in是internet缩写。
struct in_addr {
unsigned long s_addr;
}
具体在<netinet/in.h>的定义是这样子的:
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
2)struct sockaddr_in结构定义如下:
struct sockaddr_in { /* in表示Internet */
unsigned short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
char sin_zero[]; /* 填充0(保持与struct sockaddr 一样大小) */
};
具体在<netinet/in.h>的定义是这样子的:
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
}; /* Notes */
/* bits/sockaddr.h */
typedef unsigned short int sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family #define __SOCKADDR_COMMON_SIZE (sizeof(unsigned short int))
其中一个值得注意的是32位IPv4地址存在两种不同的访问方法。举例来说,如果serv定义为某个网际套接字地址结构,那么serv.sin_addr将按in_addr结构引用其中的32为IPv4地址,而serv.sin_addr_s_addr将按in_addr_t(通常是一个无符号的32位整数)引用同一个32位IPv4地址。因此,我们必须正确使用IPv4地址,尤其是将它作为函数的参数时,因为编译器对传递结构和传递整数的处理是完全不一样的。
sin_addr字段是一个结构,而不仅仅是一个in_addr_t类型的无符号长整数,这是有历史原因的。早期的版本(4.2BSD)把in_addr结构定义为多种结构的联合(union),允许访问一个32位IPv4地址中的所有4个字节,或者访问它的2个16位值。这用在地址被划分成A、B和C三类的时期,便于获取地址中的适当字节。然而随着子网划分技术的来临和无类地址编排的出现,各种地址类正在消失,那个联合已不再需要了。如今大多数系统已经废除了该联合,转而把in_addr定义为仅有一个in_addr_t字段的结构。
3)通用套接字地址结构
当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是以指针指向该结构的指针)来传递。然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构。
在如何声明所传递指针的数据类型上存在一个问题。有了ANSI C后解决办法很简单:void* 是通用的指针类型。然而套接字函数是在ANSI C之前定义的,在1982年采取的办法是在<sys/socket.h>头文件中定义一个通用的套接字地址结构struct sockaddr,如下所示:
struct sockaddr {
sa_family_t sa_family; /* 地址族, AF_xxx */
char sa_data[]; /* 14字节的协议地址 */
}
于是套接字函数定义为以指向某个通用套接字地址结构的一个指针作为其参数之一,这正如bind函数的ANSI C函数原型所示:
int bind(int, struct sockaddr *, socklen_t);
这就要求对这些函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行类型强制转换(casting),变成指向某个通用套接字地址结构的指针。下边例子展示了这个过程:
A. 首先,定义一个sockaddr_in结构变量,并将它初始化为0,代码如下:
struct sockaddr_in myad;
memset(&myad, , sizeof(myad));
B. 然后,给这个结构变量赋值,代码如下:
myad.sin_family = AF_INET;
myad.sin_port = htons();
myad.sin_addr.s_addr = htonl(INADDR_ANY);
C. 在进行函数调用时,将这个结构强制转换为struct sockaddr类型,代码如下:
bind(serverFd, (struct sockaddr *) &myad, sizeof(myad));
附:Socket常用数据类型说明:
1.2 IPv6套接字地址结构
IPv6套接字地址结构定义如下:
struct in6_addr {
uint8_t s6_addr[]; /* 128-bit IPv6 address */
/* network byte ordered */
} #define SIN6_LEN /* required for compile-time tests */ struct sockaddr_in6 {
uint8_t sin6_len; /* length of this struct (28) */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* transport layer port# */
/* network byte ordered */
uint32_t sin6_flowinfo; /* flow information, undefined */
struct in6_addr sin6_addr; /* IPv6 address */
/* network byte ordered */
uint32_t sin6_scope_id; /* set of interfaces for a scope */
}
具体在<netinet/in.h>头文件定义如下:
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[];
#if defined __USE_MISC || defined __USE_GNU
uint16_t __u6_addr16[];
uint32_t __u6_addr32[];
#endif
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#if defined __USE_MISC || defined __USE_GNU
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
}; /* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
2. 整形字节序转换函数
为保证“大端”和“小端”字节序的机器之间能相互通信,需在发送多字节整数时,将主机字节序(host byte order)转换成网络字节序(network byte order),或将网络字节序转换为主机字节序。下图说明了网络字节序与小端字节序、大端字节序的对照关系。字节转换主要是针对整形进行转换,字符型由于是单字节,所以不存在这个问题。整形字节序转换函数原型及其说明如下表所示:
注:h: host; n: network; s: short; l: long。
* 大端和小端 科普 *
1)小端就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
2)大端就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
A. 大端模式:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
B. 小端模式:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
3. IP地址转换函数
IP地址转换函数是指完成点分十进制IP地址与二进制IP地址之间的相互转换。IP地址转换主要由inet_addr、inet_aton和inet_ntoa这3个函数完成,但这三个函数都只能处理IPv4地址而不能处理IPv6地址。新的函数inet_pton跟inet_ntop则同时处理IPv4和IPv6。
1)inet_pton
注:pton:presentation to numeric
inet_pton函数原型如下[将"点分十进制" -> "整数"]
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
//这个函数转换字符串到网络地址,第一个参数af是地址族,转换后存在dst中
//若成功则返回1;若输入不是有效的表达式则返回0;若出错则返回-1。
inet_pton是inet_addr的扩展,支持的多地址族有下列:
af = AF_INET
src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
af = AF_INET6
src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
2)inet_ntop
inet_ntop函数原型如下[将"点分十进制" -> "整数"]
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
//这个函数转换网络二进制结构到ASCII类型的地址,参数的作用和上面相同,只是多了一个参数socklen_t cnt,
//它是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC。
//若成功则返回指向结果的指针,若出错则返回NULL。
如果以不被支持的地址族作为family参数,这两个函数就都返回一个错误,并将errno设置为EAFNOSUPPORT。
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main (void)
{
char IPdotdec[]; // 存放点分十进制IP地址
struct in_addr s; // IPv4地址结构体
// 输入IP地址
printf("Please input IP address: ");
scanf("%s", &IPdotdec);
// 转换
inet_pton(AF_INET, IPdotdec, (void *)&s);
printf("inet_pton: 0x%x\n", s.s_addr); // 注意得到的字节序
// 反转换
inet_ntop(AF_INET, (void *)&s, IPdotdec, );
printf("inet_ntop: %s\n", IPdotdec);
}
3)inet_aton
注:aton:address to network
4)inet_ntoa
5)inet_addr(该函数无法处理255.255.255.255的IP地址,已弃用)
参考资料
《UNIX网络编程 卷1》
《深入浅出Linux工具与编程》(余国平著)