现代 CPU 的累加器一次都能装载(至少)4 字节(下面均考虑 32 位机),即一个整数。那么这 4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。
现在 PC 大多采用小端字节序,因此小端字节序又被称为主机字节序。当格式化的数据(比如 32 bit 整型数和 16 bit 短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释之。解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
需要指出的是,即使是同一台机器上的两个进程(比如一个由 C 语言编写,另一个由 JAVA 编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
Linux 提供了 4 个函数来完成主机字节序和网络字节序之间的转换:
#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong); // host long to network long
unsigned short int htons(unsigned short int hostshort); // host short to network short
unsigned long int ntohl(unsigned long int netlong); // network long to host long
unsigned short int ntohs(unsigned short int netshort); // network short to host short
socket 网络编程接口中表示 socket 地址是结构体 sockaddr :
#include<bits/socket.h>
struct sockaddr {
sa_family_t sa_family; // 地址族类型 地址族类型变量
char sa_data[14];
}
地址族类型通常与协议族类型对应,常见的协议族(protocol family,domain)和对应的地址族关系如下:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AFUNIX | UNIX 本地域协议族 |
PF_INET | AF_INET | TCP/IPv4 协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6 协议族 |
宏 PF_*
和 AF_*
都定义在 bits/socket.h
头文件中,前后者的值完全相同,因此二者可以混用。
sa_data
成员用于存放 socket
地址值,但是不同的协议族的地址值具有不同的含义和长度:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到 108 字节 |
PF_INET | 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID ,共 26 字节 |
由上表可知 14 字节的 sa_data 根本无法容纳多数协议族的地址,因此 Linux 定义了新的通用 socket 地址结构体:
#include<bits/socket.h>
struct sockaddr_storage {
sa_family_t sa_family;
unsigned long int__ss_align;
char__ss_padding[128-sizeof(__ss_align)]; // 地址是对齐的
}
上面两个通用的 socket 地址结构体显然很不好用,比如设置与获取 IP 地址和端口号就需要执行繁琐的位操作,所以 Linux 为各个协议族提供了专门的 socket 地址结构体:
#include<sys/un.h>
struct sockaddr_un {
sa_family_t sin_family; // 地址族:AF_UNIX
char sun_path[108]; // 文件路径名
};
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地址,要用网络字节序表示
};
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地址,要用网络字节序表示
};
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转换即可),因为所有 socket 编程接口使用的地址参数的类型都是 sockaddr 。
在计算机中使用 IP 地址,需要转换为二进制;而在记录日志中,则需要转换为供人可读的字符串:
// 老版本
#include<arpa/inet.h>
in_addr_t inet_addr(const char*strptr); // 将点分十进制的ip地址转换为网络字节序表示的地址,失败时返回INADDR_NONE
int inet_aton(const char*cp,struct in_addr*inp); // 与上一个相同,但是将转化结果存储在inp指向的地址中,成功时返回1,失败时返回0
char*inet_ntoa(struct in_addr in); // 将网络字节序地址转换为点分十进制,函数内部用一个静态变量存储结果,返回值指向静态内存,因此不可重入
char*szValue1=inet_ntoa(“1.2.3.4”);
char*szValue2=inet_ntoa(“10.194.71.60”);
printf(“address 1:%s\n”,szValue1); // address1:10.194.71.60,被第二局覆盖了
printf(“address 2:%s\n”,szValue2); // address1:10.194.71.60
// 新版本
#include<arpa/inet.h>
int inet_pton(int af,const char*src,void*dst); // 十(六)进制字符串IP转网络字节序,并把结果存入dst指向的内存,af指定地址族(不支持unix),成功返回1,失败返回0
const char*inet_ntop(int af,const void*src,char*dst,socklen_t cnt); // 与上一条相反,cnt指定目标存储单元的大小,成功时返回目标存储单元地址,失败返回null,并设置errno
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16 // 利用宏指定cnt大小,对应IPv4
#define INET6_ADDRSTRLEN 46 // 对应IPv6
errno 是 Linux 提供的错误代码。
在 Linux 中,所有东西都是文件,socket 本质上就是可读、可写、可控制、可关闭的文件描述符:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol); // 创建一个 socket
PF_INET
;创建 socket 时,我们给它制定了地址族,但是并未指定使用该地址族中的哪个具体 socket 地址。将一个 socket 与 socket 地址绑定称为给 socket 命名。在服务器程序中,我们通常要命名 socket ,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名 socket ,而是采用匿名方式,即使用操作系统自动分配的 socket 地址,系统调用如下:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr*my_addr,socklen_t addrlen);
socket 被命名之后,还不能马上接收客户连接,需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
#include<sys/socket.h>
int listen(int sockfd,int backlog);
/proc/sys/net/ipv4/tcp_max_syn_backlog
内核参数定义。backlog 参数的典型值是 5 ,listen 成功时返回 0 ,失败时返回 -1 并设置 errno 。下面编写程序来研究 backlog 对 listen 系统调用的实际影响,首先将以下代码运行在服务器端:
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<string.h>
static bool stop=false;
/*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
static void handle_term(int sig) {
stop=true;
}
int main(int argc,char*argv[]) { // 可变参数模板
signal(SIGTERM,handle_term); // 触发时结束主函数中的循环
if(argc<=3) { // 判断参数个数
printf("usage:%s ip_address port_number backlog\n",basename(argv[0]));
return 1;
}
const char*ip=argv[1]; // ip地址
int port=atoi(argv[2]); // 端口
int backlog=atoi(argv[3]); // backlog
int sock=socket(PF_INET,SOCK_STREAM,0); // 创建socket
assert(sock>=0); // 判断创建是否成功
/*创建一个IPv4 socket地址*/
struct sockaddr_in address;
bzero(&address,sizeof(address)); // 初始化地址
address.sin_family=AF_INET; // 地址族
inet_pton(AF_INET,ip,&address.sin_addr); // IP转网络字节序
address.sin_port=htons(port); // 端口转网络字节序
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address)); // 绑定地址和socket
assert(ret!=-1); // 判断绑定是否成功
ret=listen(sock,backlog); // 创建监听队列
assert(ret!=-1); // 判断创建是否成功
/*循环等待连接,直到有SIGTERM信号将它中断*/
while(!stop) {
sleep(1)
}
/*关闭socket,见后文*/
close(sock);
return 0;
}
该服务器程序接收 3 各参数:IP 地址、端口号、backlog 值,此时用客户端多次执行 telnet 命令连接服务器,每建立一个连接就使用 netstat 命令查看服务器连接状态:
$./testlisten 192.168.1.109 12345 5 # 监听12345端口,给backlog传递典型值5
$telnet 192.168.1.109 12345 # 多次执行之
$netstat-nt|grep 12345 # 多次执行之
此时输出如下:
Proto Recv-Q Send-Q Local Address Foreign Address Statetcp
tcp 0 0 192.168.1.109:12345 192.168.1.108:2240 SYN_RECV
tcp 0 0 192.168.1.109:12345 192.168.1.108:2228 SYN_RECV
tcp 0 0 192.168.1.109:12345 192.168.1.108:2230 SYN_RECV
tcp 0 0 192.168.1.109:12345 192.168.1.108:2238 SYN_RECV
tcp 0 0 192.168.1.109:12345 192.168.1.108:2236 SYN_RECV
tcp 0 0 192.168.1.109:12345 192.168.1.108:2217 ESTABLISHED
tcp 0 0 192.168.1.109:12345 192.168.1.108:2226 ESTABLISHED
tcp 0 0 192.168.1.109:12345 192.168.1.108:2224 ESTABLISHED
tcp 0 0 192.168.1.109:12345 192.168.1.108:2212 ESTABLISHED
tcp 0 0 192.168.1.109:12345 192.168.1.108:2220 ESTABLISHED
tcp 0 0 192.168.1.109:12345 192.168.1.108:2222 ESTABLISHED
可以发现最多只能有 backlog + 1 个完整连接,说明监听队列中完整连接的上限通常比 backlog 大。
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);
需要注意的是,若再连接过程中客户端出线程序崩溃、网络掉线等问题,服务器端 accept 调用都能正常返回,说明服务器端 accept 只是从监听队列中取出连接,而不论连接处于什么状态,更不关心任何网络状况的变化。
如果说服务器通过 listen 调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
关闭一个连接实际上就是关闭该连接对应的 socket ,这可以通过如下关闭普通文件描述符的系统调用来完成:
#include<unistd.h>
int close(int fd);
相对的,还提供了强制终止连接:
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
可选值 | 含义 |
---|---|
SHUY_RD | 关闭 sockfd 上读的这一半,应用程序不能再针对 socket 文件描述符执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃 |
SHUT_WR | 关闭 sockfd 上写的这一半,sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该 socket 文件描述符执行写操作,这种情况下连接处于半关闭状态 |
SHUT_RDWR | 同时关闭读写 |
对文件的读写操作 read 和 write 同样适用于 socket 。但是 socket 编程接口提供了几个专门用于 socket 数据读写的系统调用,它们增加了对数据读写的控制。其中用于 TCP 流数据读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd,void*buf,size_t len,int flags);
ssize_t send(int sockfd,const void*buf,size_t len,int flags);
选项名 | 含义 | send | recv |
---|---|---|---|
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,直到得到答复。仅用于 SOCK_DGRAM 和 SOCK_RAW 类型的 socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地直到目标主机就在本地网络上 | Y | N |
MSG_DONTWAIT | 对 socket 的此次操作将是非阻塞的 | Y | Y |
MSG_MORE | 告诉内核程序还有更多数据要发送,内核将超时等待新数据写入 TCP 发送缓存区后一并发送,这样可以防止 TCP 发送过多小的报文段,从而提高传输效率 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 | N | Y |
MSG_OOB | 发送或接受紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或者 socket 连接中写数据时不引发 SIGPIPE 信号 | Y | N |
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd,void*buf,size_t len,int flags,struct sockaddr*src_addr,socklen_t*addrlen);
ssize_t sendto(int sockfd,const void*buf,size_t len,int flags,const struct sockaddr*dest_addr,socklen_t addrlen);
下面的接口可用于 TCP 和 UDP :
#include<sys/socket.h>
struct iovec {
void*iov_base; // 内存起始地址
size_t iov_len; // 这块内存的长度
};
struct msghdr {
void*msg_name; // socket地址
socklen_t msg_namelen; // socket地址的长度
struct iovec*msg_iov; // 分散的内存块,见后文
int msg_iovlen; // 分散内存块的数量
void*msg_control; // 指向辅助数据的起始位置
socklen_t msg_controllen; // 辅助数据的大小
int msg_flags; // 复制函数中的flags参数,并在调用过程中更新
};
ssize_t recvmsg(int sockfd,struct msghdr*msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr*msg,int flags);
实际应用中,我们通常无法预期带外数据(紧急数据)何时到来。好在 Linux 内核检测到 TCP 紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:
但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一 点可通过如下系统调用实现:
#include<sys/socket.h>
int sockatmark(int sockfd);
用于判断 sockfd 是否处于带外标记,即下一个被读取到的数据是否是带外数据,如果是则返回 1 ,此时就可以利用带 MSG_OOB 标志的 recv 调用来接受带外数据;如果不是则返回 0 。
在某些情况下,我们想知道一个连接 socket 的本端 socket 地址,以及远端的 socket 地址:
#include<sys/socket.h>
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
如果说 fcntl 系统调用是控制文件描述符属性的通用 POSIX 方法,那么下面两个系统调用则是专门用来读取和设置 socket 文件描述符属性的方法:
#include<sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,const void*option_value,socklen_t option_len);
level | option name | 数据类型 | 说明 |
---|---|---|---|
SOL_SOCKET(通用 socket 选项,与协议无关) | SO_DEBUG | int | 打开调试信息 |
SO_REUSEADDR | int | 重用本地地址 | |
SO_TYPE | int | 获取 socket 类型 | |
SO_ERROR | int | 获取并清除 socket 错误状态 | |
SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机 | |
SO_RCVBUF | int | TCP 接收缓冲区大小 | |
SO_SNDBUF | int | TCP 发送缓冲区大小 | |
SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 | |
SO_OOBINLINE | int | 接收到的带外数据将存留在普通数据的输入队列中(在线留存),此时不能使用带 MSG_OOB 标志的读操作来读取带外数据,而应像普通数据那样读取 | |
SO_LINGER | linger | 若有数据待发送,则延迟关闭 | |
SO_RCVLOWAT | int | TCP 接收缓存区低水位标记 | |
SO_SNDLOWAT | int | TCP 发送缓冲区低水位标记 | |
SO_RCVTIMEO | timeval | 接收数据超时 | |
SO_SNDTIMEO | timeval | 发送数据超时 | |
IPPROTO_IP(IPv4 选项) | IP_TOS | int | 服务类型 |
IP_TTL | int | 存活时间 | |
IPPROTO_IPv6(IPv6 选项) | IPV6_NEXTHOP | sockaddr_in6 | 下一跳 IP 地址 |
IPV6_RECVPKTINFO | int | 接受分组信息 | |
IPV6_DONTFRAG | int | 禁止分片 | |
IPV6_RECVTCLASS | int | 接受通信类型 | |
IPPROTO_TCP(TCP 选项) | TCP_MAXSEG | int | TCP 最大报文段大小 |
TCP_NODELAY | int | 禁止 Nagle 算法 |
值得指出的是,对服务器而言,有部分 socket 选项只能在调用 listen 系统调用前针对监听 socket 设置才有效。这是因为连接 socket 只能由 accept 调用返回,而 accept 从 listen 监听队列中接受的连接至少已经完了 TCP 三次握手的前两个步骤(因为 listen 监听队列中的连接至少已进入 SYN_RCVD 状态),这说明服务器已经往被接受连接上发送出了 TCP 同步报文段。但有的 socket 选项却应该在 TCP 同步报文段中设置,比如 TCP 最大报文段选项。对这种情况,Linux 给开发人员提供的解决方案是:对监听 socket 设置这些 socket 选项,那么 accept 返回的连接 socket 将自动继承这些选项。这些 socket 选项包括:
而对客户端而言,这些 socket 选项则应该在调用 connect 函数之前设置,因为 connect 调用成功返回之后,TCP 三次握手已完成。
int sock=socket(PF_INET,SOCK_STREAM,0); // 创建socket
assert(sock>=0); // 创建是否成功
int reuse=1; // 标志状态
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)); // 设置socket选项,SO_REUSEADDR
struct sockaddr_in address; // 创建地址结构体变量
bzero(&address,sizeof(address)); // 初始化
address.sin_family=AF_INET; // 设置地址族
inet_pton(AF_INET,ip,&address.sin_addr); // 转换为网络字节序地址
address.sin_port=htons(port); // 设置端口
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address)); // 绑定地址
通过选项 SO_REUSEADDR ,即使 sock 处于 TIME_WAIT 状态,与之绑定的 socket 地址也可以立即被重用,此外还可以设置 /proc/sys/net/ipv4/tcp_tw_recycle
来快速回收被关闭的 socket ,从而使得 TCP 连接根本就不进入 TIME_WAIT 状态,进而允许应用程序立即重用本栋 socket 地址。
SO_RCVBUF 和 SO_SNDBUF 选项分别表示 TCP 接收缓冲区和发送缓冲区的大小。不过在使用 setsockopt 来设置 TCP 的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP 接收缓冲区的最小值是 256 字节,而发送缓冲区的最小值是 2048 字节(不同系统可能有不同默认最小值)。系统这样做的目的主要是确保一个 TCP 连接拥有足够的空闲缓冲区来处理拥塞 (比如快速重传算法就期望 TCP 接收缓冲区能至少容纳 4 个大小为 SMSS 的 TCP 报文段)。
可以直接修改内核数 /proc/sys/net/ipv4/tcp_rmem
和 /proc/sys/net/ipv4/tcp_wmem
来强制 TCP 接收缓冲区和发送缓冲区的大小没有最小值限制。
SO_RCVLOWAT 和 SO_SNDLOWAT 选项分别表示 TCP 接收缓冲区和发送缓冲区的低水位标记。它们一般被 I/O 复用系统调用来判断 socket 是否可读或可写。当 TCP 接收缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以从对应的 socket 上读取数据;当 TCP 发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时, I/O 复用系统调用将通知应用程序可以往对应的 socke 上写入数据。默认情况下,TCP 接收缓冲区的低水位标记和 TCP 发送缓冲区的低水位标记均为 1 字节。
SO_LINGER 选项用于控制 close 系统调用在关闭 TCP 连接时的行为。默认情况下,当我们使用 close 系统调用来关闭一个 socket 时,close 将立即返回,TCP 模块负责把该 socket 对应的 TCP 发送缓冲区中残留的数据发送给对方。
设置(获取)SO_LINGER 选项的值时,我们需要给 setsockopt(getsockopt)系统调用传递一个 linger 类型的结构体,其定 义如下:
#include<sys/socket.h>
struct linger {
int l_onoff; // 开启(非0)还是关闭(0)该选项
int l_linger; // 滞留时间
};
根据结构体中两个成员变量的不同值,可能产生三种行为:
socket 地址的两个要素,即 IP 地址和端口号,都是用数值表示的。 这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。但是 telnet 可以使用服务名来代替端口号,下面两条语句作用相同:
telnet 127.0.0.1 80
telnet localhost www
上面的例子中,telnet 客户端程序通过调用某些网络信息 API 来实现主机名到 IP 地址的转换,以及服务名称到端口号的转换。
gethostbyname 函数根据主机名称获取主机的完整信息, gethostbyaddr 函数根据 IP 地址获取主机的完整信息。gethostbyname 函数通常先在本地的 /etc/hosts 配置文件中查找主机,如果没有找到,再去访问 DNS 服务器:
#include<netdb.h>
struct hostent {
char*h_name; // 主机名
char**h_aliases; // 主机别名列表,可能有多个
int h_addrtype; // 地址类型(地址族)
int h_length; // 地址长度
char**h_addr_list // 按网络字节序列出的主机IP地址列表
};
struct hostent*gethostbyname(const char*name);
struct hostent*gethostbyaddr(const void*addr,size_t len,int type);
getservbyname 函数根据名称获取某个服务的完整信息,getservbyport 函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取 /etc/services 文件来获取服务的信息的:
#include<netdb.h>
struct servent {
char*s_name; // 服务名称
char**s_aliases; // 服务的别名列表,可能有多个
int s_port; // 端口号
char*s_proto; // 服务类型,通常是tcp或者udp
};
struct servent*getservbyname(const char*name,const char*proto);
struct servent*getservbyport(int port,const char*proto);
getaddrinfo 函数既能通过主机名获得 IP 地址(内部使用的是 gethostbyname 函数),也能通过服务名获得端口号(内部使用的是 getservbyname 函数)。它是否可重入取决于其内部调用的 gethostbyname 和 getservbyname 函数是否是它们的可重入版本。该函数的定义如下:
#include<netdb.h>
struct addrinfo {
int ai_flags; // 见后文
int ai_family; // 地址族
int ai_socktype; // 服务类型,SOCK_STREAM或SOCK_DGRAM
int ai_protocol; // 见后文
socklen_t ai_addrlen; // socket地址ai_addr的长度
char*ai_canonname; // 主机的别名
struct sockaddr*ai_addr; // 指向socket地址
struct addrinfo*ai_next; // 指向下一个sockinfo结构的对象
};
int getaddrinfo(const char*hostname,const char*service,const struct addrinfo*hints,struct addrinfo**result);
选项 | 含义 |
---|---|
AI_PASSIVE | 在 hints 参数中设置,表示调用者是否会将取得的 socket 地址用于被动打开。服务器通常需要设置它,表示接受任何本地 socket 地址上的服务请求,客户端不能设置 |
AI_CANONNAME | 在 hints 参数中设置,告诉 getaddrinfo 函数返回主机的别名 |
AI_NUMERICHOST | 在 hints 参数中设置,表示 hostname 必须是用字符串表示的 IP 地址,从而避免了 DNS 查询 |
AI_NUMERICSERV | 在 hints 参数中设置,强制 service 参数使用十进制端口号的字符串形式,而不能是服务名 |
AI_V4MAPPED | 在 hints 参数中设置,如果 ai_family 被设置为 AF_INET6 ,那么当没有满足条件的 IPv6 地址被找到时,将 IPv4 地址映射为 IPv6 地址 |
AI_ALL | 必须和 AI_V4MAPPED 同时使用,否则将被忽略,表示同时返回符合条件的 IPv6 地址以及由 IPv4 地址映射得到的 IPv6 地址 |
AI_ADDRCONFIG | 仅当至少配置有一个 IPv4 地址(除了回路地址)时,才返回 IPv4 地址信息;同样,仅当至少配置一个 PIv6 地址(除了回路地址)时,才返回 IPv6 地址信息,它和 AI_V4MAPPED 是互斥的 |
当使用 hints 参数时,可以设置其 ai_flags ,ai_family ,ai_socktype 和 ai_protocol 四个字段,其他字段则必须被设置为 NULL 。
getnameinfo 函数能通过 socket 地址同时获得以字符串表示的主机名(内部使用的是 gethostbyaddr 函数)和服务名(内部使用的是 getservbyport 函数)。它是否可重入取决于其内部调用的 gethostbyaddr 和 getservbyport 函数是否是它们的可重入版本。该函数的定义如下:
#include<netdb.h>
int getnameinfo(const struct sockaddr*sockaddr,socklen_t addrlen,char*host,socklen_t hostlen,char*serv,socklen_t servlen,int flags);
选项 | 含义 |
---|---|
NI_NAMEREQD | 如果通过 socket 地址不能获得主机名,则返回一个错误 |
NI_DGRAM | 返回数据报服务,大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务。但端口 512 ~ 514 是例外。比如 TCP 中 514 端口提供 shell 登录服务;UDP 中端口 514 提供 syslog 服务 |
NI_NUMERICHOST | 返回字符串表示的 IP 地址,而不是主机名 |
NI_NUMERICSERV | 返回字符串表示的十进制端口号,而不是服务名 |
NI_NOFQDN | 仅返回主机域名的第一部分,比如对主机名 nebula.testing.com 只写入 nebula |
选项 | 含义 |
---|---|
EAI_AGAIN | 调用临时失败,提示应用程序过后再试 |
EAI_BADFLAGS | 非法的 ai_flags 值 |
EAI_FAIL | 名称解析失败 |
EAI_FAMILY | 不支持的 ai_family 参数 |
EAI_MEMORY | 内存分配失败 |
EAI_NONAME | 非法的主机名或服务名 |
EAI_OVERFLOW | 用户提供的缓冲区溢出,仅发生在 getnameinfo 调用中 |
EAI_SERVICE | 没有支持的服务 |
EAI_SOCKTYPE | 不支持的服务类型 |
EAI_SYSTEM | 系统错误,错误值存储在 errno 中 |
Linux 下 strerror 函数能将数值错误码 errno 转换成易读的字符串形式。同样,下面的函数可将上表中的错误码转换成其字符串形式:
#include<netdb.h>
const char*gai_strerror(int error);