套接字地址与套接字地址结构
套接字是网络通信的主要手段,每一个套接字都需要一个参数,机套接字地址。套接字地址是IP地址和端口号的总和。个协议族(IPv4,IPv6)都定义了自己的套接字地址结构。这些结构的名字均以sockaddr_开头,不同的协议族有不同的后缀。在构造套接字(调用套接字函数)时,需要一个指向某个套接字地址结构的指针作为参数。
套接字地址:IP地址+端口号
套接字地址结构:不同协议族专门定义的一种结构体,用来表示套接字地址。
IPv4的套接字地址结构
定义在<netinet/in.h>头文件中
sockaddr_in
struct in_addr{ //定义该结构来表示IPv4地址
in_addr_t s_addr; //32位IPv4的地址
};
struct sockaddr_in{ //套接字地址结构
uint8_t sin_len; //1字节,表示整个结构的长度=16字节
sa_family_t sin_family; //1字节AF_INET,表示是IPv4地址族
in_port_t sin_port; //2字节,uint16_t,16位的端口号,0~65535
struct in_addr sin_addr; //4字节,32位IPv4地址
char sin_zero[8]; //8字节,unused,用来补充位数,一般为0
};
struct in_addr_t可以指明为通配地址,此时sin_addr=INADDR_ANY
POSIX数据类型
符合POSIX(可移植操作系统接口)的结构需要至少3个字段:sin_family、sin_addr和sin_port。
POSIX定义的常见数据类型:
数据类型 | 说明 | 头文件 |
int8_t | 带符号8位整数 | <sys/types.h> |
uint8_t | 无符号8位整数 | |
int16_t | 带符号 | |
uint16_t | 无 | |
int32_t | 带 | |
uint32_t | 无 | |
sa_family_t | 套接字地址结构的地址族 | <sys/socket.h> |
socklen_t | 套接字地址结构的长度,一般为uint32_t | |
in_addr_t | IPv4地址,一般为uint32_t | <netinet/in.h> |
in_port_t | TCP或UDP端口,一般为uint16_t | |
对于IPv4地址,使用一个结构in_addr来包裹这个32位无符号整数。因此IPv4地址有两种访问方式。
为什么要定义结构in_addr来表示IPv4地址:因为早期这个结构中还存储了其他类型,用于给IPv4地址进行A、B、C类的分类。先在随着子网划分技术的来临,这些分类逐渐被废除。现在的in_addr中仅仅有一个in_addr_t字段。
设serv为某个套接字地址结构变量。那么:
serv.sin_addr访问的是结构in_addr;
serv.sin_addr.s_addr则直接访问了这个32位无符号整数。
这两种方式都可以使用IPv4地址,对于不同的函数,使用的类型不同。
通用套接字地址结构
在任何套接字函数中,需要传递进一个指向套接字地址结构的指针作为参数。为了适用于任意的协议族的不同套接字地质结构,因此定义了通用套接字地质结构。
通用套接字地址结构可以看作时任何协议族套接字地址结构的“父类“(实际上c语言中没有这个概念)。这一个通用套接字地址结构一般用于IPv4。
通用套接字地址结构定义在<sys/socket.h>中:
struct sockaddr{
uint8_t sa_len;
sa_family sa_family;
char sa_data[14];
}
例如bind函数:
int bind(int,struct sockaddr *,socklen_t);
使用该函数时,只需要对特定的协议族套接字地址函数指针进行强制类型转换即可:
struct sockaddr_in serv;
bind(sockfd,(struct sockaddr *)&serv,sizeof(serv));
IPv6套接字地址结构
定义在<netinet/in.h>中。
struct in6_addr{
uint8_t sa_addr[16]; //128位IPv6地址,IPv4为32位in_addr_t(uint32)
}
struct sockaddr_in6{
uint8_t sin6_len; //1字节,表示整个结构的长度=28字节
sa_family_t sin6_family; //1字节,AF_INET6,表示是IPv6地址族,IPv4为AF_INET
in_port_t sin6_port; //2字节,端口号
uint32_t sin6_flowinfo; //4字节,流量窗口信息,未定义,IPv4没有
struct in6_addr sin6_addr; //16字节,IPv6地址,IPv4为in_addr
uint32_t sin6_scpor_id; //4字节,用来标识地址所在范围的接口,IPv4没有
}
struct in6_addr可以指明为通配地址,此时sin6_addr=in6addr_any(在该头文件中定义了)
新的通用套接字地址结构
新结构struct sockaddr_storage兼容了IPv6的地质结构,可以容纳系统所支持的全部任何套接字地址结构。
定义在头文件<netinet/in.h>中。
struct sockaddr_storage{
uint8_t ss_len; //本结构长度
sa_family_s sa_family; //地址族,AF_xxx。
}
套接字函数传递参数的不同
在套接字相关函数中,需要传递一个指向套接字地址结构的指针作为参数,同时还需要一个参数用来说明这个结构的长度。
这个长度参数可以是值也可以是指针,区别在于该地址结构的传递方向:是从进程到内核(从应用层到传输层)还是从内核到进程(传输层到应用层)。
(a)参数从进程到内核:传递值
从进程到内核传递套接字地址结构的函数有3个:
bind、connect、sendto。
主要用于主动打开和被动打开中。
这些函数的一个参数是一个指向某个已经明确创建的套接字地质结构的指针(本地套接字地址或者目标服务器的套接字地址),另一个参数是该结构的整数大小(一个值)。
例如
struct sockaddr_in serv;
serv.sin_family=AF_INET;
//……
//继续完善套接字地址结构
bind(sockfd,(struct sockaddr *)&serv,sizeof(serv)); //传入的套接字地质结构已经明确了
connect(sockfd,(struct sockaddr *)&serv,sizeof(serv));
从进程到内核传递参数时,传递的是一个值,这个值表示套接字地址结构的最大可能的空间。这是因为从进程向内核传递参数时,套接字地址结构是明确的,已经创建完成的,例如conncet函数,即将一个已经确定的套接字地址结构(指针)传入,因此只需要将该结构大小的确切值传入函数即可。内核通过这个值知道,到底需要将多少字节写入内核。
(b)参数从内核到进程:传递指针(值-结果参数)
从内核到进程传递套接字地址结构的函数有4个:
accept、recvfrom、getsockname、getpeername。
这四个函数需要的参数一个是指向套接字地址结构的指针,另一个是指向表示该结构大小的整数变量的指针。这个指向结构大小的参数(指针),被称为值-结果参数。对于此类函数,套接字地址结构也是函数的潜在返回值。传入的套接字地址结构是新声明的指针变量,没有进行初始化,改指针变量用于存放内核返回的实际数据。因此长度参数是一个值-结果参数。因为我们并不知道内核实际写入的字节长度。因此要用指针。来写入数据。
例如:
struct sockaddr_in cli; //只是声明了变量,实际上该结构变量没有初始化,因此不知道确切大小。
socklen_t len; //表示结构大小的整型变量
len=sizeof(cli);
getpeername(unixfd,(struct sockaddr *)&cli,&len);
这里的结构大小,就是一种值-结果参数。
(c)值-结果参数总结
这里的结构大小是一个指向某个整数的指针,而不是一个明确的值,这取决于参数的传递方向。
从进程到内核传递参数时,传递的是一个值,这个值表示套接字地址结构的最大可能的空间。这是因为从进程向内核传递参数时,套接字地址结构是明确的,已经创建完成的,例如conncet函数,即将一个已经确定的套接字地址结构(指针)传入,因此只需要将该结构大小的确切值传入函数即可。内核通过这个值知道,到底需要将多少字节写入内核。
struct sockaddr_in serv;
serv.sin_family=AF_INET;
//……
//继续完善套接字地址结构
bind(sockfd,(struct sockaddr *)&serv,sizeof(serv)); //传入的套接字地址结构已经明确了
connect(sockfd,(struct sockaddr *)&serv,sizeof(serv));
而从内核到进程传递参数时,这个长度参数是一个指向整型变量的指针。从内核向进程传递参数的函数时,例如服务器调用的int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);会传入两个变量:
一个指向客户套接字地址结构的指针:
struct sockaddr_in cli;
cli没有进行初始化,因为我们创建他的目的是用于接收调用accept返回的实际套接字地址结构数据。 一个是指向该套接字地址结构的长度的指:
socklen_t len;
len=sizeof(cli);
此时客户套接字地址结构的实际情况服务器并不知道。因此函数调用前,我们将这个指针指向的整数值置为套接字地址结构的最大长度(值),这个值确定了一个最大边界长度,确保内核写进的字节不会越界。函数调用时,内核会将接收到的实际数据写进套接字地质结构。函数返回时,该整数值变为内核存放在该套接字地址结构内的确切字节数(结果)。
struct sockaddr_in cli; //只是声明了变量,实际上该结构变量没有初始化,因此不知道确切大小。
socklen_t len; //表示结构大小的整型变量
len=sizeof(cli);
getpeername(unixfd,(struct sockaddr *)&cli,&len);
这就是值-结果参数的含义。