套接字地址结构(Socket Address Structures)
- 大多数套接字函数(socket function)都 需要一个指向套接字地址结构(socket address structure)的指针作为参数
- 每个协议族(protocol suite)都定义它自己的套接字地址结构
- 这些结构的名字均以 sockaddr_开头,并以对应每个协议族的唯一后缀结尾
-
IPV4 套接字地址结构
网际(IPV4)套接字地址结构:sockaddr_in --> 定义在 <netinet/in.h> 当中
struct in_addr{ in_addr_t s_addr; //32-bit ipv4 Address, network byyte ordered //32为的ipv地址,采用网络字节序 }; struct sockaddr_in{ uint8_t sin_len; //length of structure (16) sa_family_t sin_family; //AF_INET ==>本地址结构是IPV4的地址结构,他属于网际协议族 in_port_t sin_port; //16-bit TCP or UDP port, 网络字节序 struct in_addr sin_addr; //32-bit ipv4 address, 网络字节序 char sin_zero[8]; //unused ==> 保留位,未使用 }
- POSIX 规范要求的数据类型:
- IPV4 地址和 TCP 或 UDP 端口号都采用网络字节序(大端序)存储
- IPV4 地址存在两种访问方法(因为历史原因)
- serv.sin_addr (结构体)
- serv.sin_addr.in_addr_t (通常是一个无符号的 32 为整数)
- sin_zero 字段未被使用,不过在填写这种结构的时候 sin_zero 通常被置为 0。(通常的做法是,在填写之前,用 bzero 将整个结构体清 0,再填写,可以保证未填写的部分都是 0)
- 套接字地址结构仅在主机上使用,虽然结构体中的某些字段(例如 IP 地址和端口号)用在不同主机之间的通信,但是结构体本身并不在主机之间传递
- POSIX 规范要求的数据类型:
-
通用套接字地址结构
通用套接字地址结构:sockaddr --> 定义在 <sys/socket.h> 当中
struct sockaddr{ uint8_t sa_len; sa_family_t sa_family; //Address family: AF_XXX value char sa_data[14]; //protocol-specific address }
-
前面提到过,大多数套接字函数都需要一个指向套接字地址结构的指针。但是套接字函数大多支持多个协议族,也就是说在调用的时候可能传入不同协议族的地址结构指针,那套接字函数在定义的时候就必须要有一个类型,可以接收各个协议族对应的地址结构指针。
-
这种需求可以用 void *** 来解决,实际上也更方便,如果用 void * 来定义,可以接收任意类型的指针,而且不用显示转换**。但是 void * 实在 ANSI C 中提出的,而套接字函数是在 ANSI C 之前定义的,所以为了解决上述需求,采用了通用套接字,下面是一个套接字函数的栗子:
int bind(int, struct sockaddr *, socklen_t); //调用方式如下 bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
- 通用套接字地址结构 sockaddr 和其他协议族各自的地址结构 sockaddr_XX 规定的最小 size 是一样的,都是 16 个字节
- 套接字函数在具体处理的时候根据 sa_family 字段区分不同的地址结构,对应不同的处理
-
-
IPV6 套接字地址结构
IPv6 套接字地址结构:sockaddr_in6 --> 定义在 <netinet/in.h> 当中
struct in6_addr { uint8_t s6_addr[16]; //128-bit IPV6 address }; #define SIN6_LEN //require 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; //传输层端口,网络字节序 uint32_t sin6_flowinfo; //flow information, undefined struct in6_addr sin6_addr; //IPV6 address, 网络字节序 uint32_t sin6_scope_id; //set of interfaces for a scope }
值 - 结果参数(Value-Result Argument)
- 一个参数,当函数调用时,其作为一个值从函数外传入函数内,当函数返回时,该参数又存储了函数执行的部分结果,这种类型的参数称为 value-result 参数
- value-result 参数总是以引用 / 指针的方式传递(只能用地址传递的方式,如果用值传递方式获取不到函数的返回信息)
-
上文曾提到过,当往一个套接字函数传递地址结构的时候,该结构总是以引用的方式传递(即传递地址结构的指针)。该结构的长度也作为一个参数来传递,不过其传递的方式可能是传值,也可能是传指针,具体的传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程
-
从进程到内核传递套接字地址结构
-
涉及的函数有:bind、connect、sendto
-
传递结构长度的时候传值就好了
-
举个栗子:
struct sockaddr_in serv; /*fill in serv{}*/ connect(sockfd, (SA *) &serv, sizeof(serv));
-
图示:
-
-
从内核到进程传递套接字地址结构
-
涉及的函数有:accpet、recvfrom、getsockname、getpeername
-
传递结构长度的时候传入一个指向 socklen_t 的指针(而不是 int,POSIX 规范建议将 socklen_t 定义为 uint32_t)
-
举个栗子:
struct sockaddr_un cli; socklen_t len; len = sizeof(cli); getpeername(unixfd, (SA *) &cli, &len);
-
图示:
-
当函数被调用时,结构大小是一个值(value),它告诉内核该结构的大小,这样内核在写该结构的时候不至于越界;当函数返回时,结构大小又是一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息。
-
字节排序函数(Byte Ordering)
-
大端和小端(big-endian and little-endian)
-
小端:低字节存储在起始地址
-
大端:高字节存储在起始地址
-
举个例子:
0x0102 //从左到右为内存增大方向 //在大端系统中存储 00000001 00000010 //在小端系统中存储 00000010 00000001
-
-
测试主机是大端还是小端的实践 –> click_me
./byteorder x86_64-unknown-linux-gnu: little-endian
-
网络字节序
- 不同的机器可能采用不同的存储方式(大端 / 小端),为了统一,便为网际协议约定了一个网络字节序
- 网际协议使用大端字节序来传送多字节整数
- 由于不同主机的差异性,便需要有一些函数来进行网络字节序和主机字节序的转换
-
字节转换函数
#include <netinet/in.h> /** * host to network short * 将主机字节序的16位短整型转换为网络字节序的16位短整型 **/ uint16_t htons(uint16_t host16bitvalue); /** * host to network long * 将主机字节序的32位整型转换为网络字节序的32位整型 **/ uint32_t htonl(uint32_t host32bitvalue); /** * network to host short * 将网络字节序的16位短整型转换为主机字节序的16位短整型 **/ uint16_t ntohs(uint16_t net16bitvalue); /** * network to host long * 将网络字节序的32位整型转换为主机字节序的32位整型 **/ uint32_t ntohl(uint32_t net32bitvalue);
- 事实上,在 64 为的系统中,尽管长整数占 64 位,htonl 和 ntohl 函数操作的仍然是 32 位值
字节操纵函数
字节操作函数和有两组,本书中只用到了 bzero
-
源自 Berkeley 的函数(b 开头的字节操纵函数)
#include <strings.h> /** * 将以dest为起始的目标串的前nbytes个字节置为0 **/ void bzero(void *dest, size_t nbytes); /** * 将dst中的前nbytes个字节拷贝到src串中 **/ void bcopy(const void *src, void *dst, size_t nbytes); /** * 比较ptr1和ptr2串的前n个字节 * @return 若相等则返回0, 否则返回非0 **/ void bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
-
ANSI C 函数(mem 开头的字节操纵函数)
#include <string.h> /** * 把dest串的前len个字节置为c **/ void *memset(void *dest, int c, size_t len); /** * memcpy类似bcopy,但是两个指针的位置时候是相反的 * * PS:当dest串和src串重叠时,bcopy可正常处理,memcpy的处理结果不可知,此时改用memmove函数 **/ void *memcpy(void *dest, const void *src, size_t nbytes); /** * 比较两个串的前nbytes个字节 * @rerurn 0 相等 * >0 第一个不相等字节,ptr1 > ptr2 * <0 第一个不相等字节,ptr1 < ptr2 **/ void *memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
-
之前的文章提到过有一个叫 bzero 宏的东西。其实是因为笔者的系统不是源自 Berkeley 的,所有没有 bzero 函数。但是 Steven 考虑的比较全面,为没有 bzero 函数的系统定义了一个 bzero 宏,间接调用 memset 函数,但是同样可以实现 bzero 的效果。
地址转换函数
- 地址转换函数在 ASCII 字符串和网络字节序的二进制值之间转换网际地址
- inet_aton、inet_addr、inet_ntoa 在点分十进制数串(例如 “192.168.1.1”)与它长度为 32 位的网络字节序二进制值之间转换 IPv4 地址。
- 两个比较新的函数,inet_pton 和 inet_ntop 对于 IPv4 和 IPv6 都适用
-
inet_aton、inet_addr 和 inet_ntoa 函数
#include<arpa/inet.h> /** * 将strptr所指c字符串转换成一个32位的网络字节序二进制值,并通过addrptr指针来存储 **/ int inet_aton(const char *strptr, struct in_addr *addrptr); /** * 将strptr所指c字符串转换成一个32位的网络字节序二进制值, 并返回 **/ in_addr_t inet_addr(const char *strptr); /** * 将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。 **/ char *inet_ntoa(struct in_addr inaddr);
- inet_addr 被废弃
- inet_addr 函数出错时返回 INADDR_NOE(通常是一个 32 位均为 1 的值)
- 这意味着该函数不能处理 “255.255.255.255”,因为它的二进制值和 INADDR_NONE 的值是一样的,被用来指示函数执行失败
- inet_ntoa 函数返回值所指向的串驻留在静态内存当中,着意味着该函数是不可重入的
- 上面这些函数至支持 IPv4,如果要支持 IPv6,使用下面介绍的两个函数
- inet_addr 被废弃
-
inet_pton 和 inet_ntop 函数
#include <arpa/inet.h> /** * 尝试转换由strptr所指的字符串,并通过addrptr指针存放二进制结果 * * @return 1 ==> 转换成功 * 0 ==> 输入的不是有效表达式 * -1 ==> 转换出错 **/ int inet_pton(int family, const char*strptr, void *addrptr); /** * 与inet_pton进行相反的转换,从数值格式(addrptr)转换到表达式格式(strptr)。 * @param len 指定目标存储单元的大小,以免该函数溢出调用者的缓冲区 * @param strptr 用来存储目标串,如果执行成功,返回值即为这个指针 * (不能传递空指针,调用这必须为目标存储单元分配内存,并指定其大小) * @return NULL ==> 失败 * strptr ==> 成功 **/ const char *inet_ntop(int family, void *addrptr, char *strptr, size_t len);
- 两个函数的 family 参数可以是 AF_INET 或 AF_INET6。如果以不被支持的地址族作为 family 参数。这两个函数都返回一个错误,并将 errno 置为 EAFNOSUPPORT
sock_ntop 和相关函数
-
在使用 inet_ntop 的时候,对于 IPv4 和 IPv6 的调用方法不同(这使得我们的代码和协议相关了):
//IPv4 struct sockaddr_in addr; inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str)); //IPv6 struct sockaddr_in6 addr6; inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
-
所以 Steven 针对上述问题写了下面这么个封装函数
#include "unp.h" /** * 同时支持IPv4和IPv6版本的inet_ntop * * @return 成功 ==> 非空指针 * 出错 ==> NULL **/ char *sock_ntop(const struct *sockaddr, socklen_t addrlen);
-
sock_ntop 的实现和其它相关函数此处不做详细讨论,有兴趣可以查看 Steven 的《Unix 网络编程 卷 1》
readn, writen 和 readline 函数
#include "unp.h"
/**
* 从一个描述符中读n字节
**/
ssize_t readn(int filedes, void *buff, size_t nbytes);
/**
* 往一个描述符里写n个字节
**/
ssize_t writen(int filedes, const void *buff, size_t nbytes);
/**
* 从一个描述符中读文本行,以字节为单位
**/
ssize_t readline(int filedes, void *buff, size_t maxlen);
- 上面三个函数对 read 和 write 操作可能发生的 EINTR 错误做了处理,是比较安全的读写函数