Linux 网络编程学习笔记——五、Linux 网络编程基础 API

目录

一、socket 地址 API

1. 主机字节序和网络字节序

现代 CPU 的累加器一次都能装载(至少)4 字节(下面均考虑 32 位机),即一个整数。那么这 4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。

  • 大端字节序:指一个整数的高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;
  • 小端字节序:指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

现在 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

2. 通用 socket 地址

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)];  // 地址是对齐的
}

3. 专用 socket 地址

上面两个通用的 socket 地址结构体显然很不好用,比如设置与获取 IP 地址和端口号就需要执行繁琐的位操作,所以 Linux 为各个协议族提供了专门的 socket 地址结构体:

  • UNIX 本地域协议族:
    #include<sys/un.h>
    struct sockaddr_un {
    	sa_family_t sin_family;  // 地址族:AF_UNIX
    	char sun_path[108];  // 文件路径名
    };
    
  • TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体:
    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 。

4. IP 地址转换函数

在计算机中使用 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 提供的错误代码。

二、创建 socket

在 Linux 中,所有东西都是文件,socket 本质上就是可读、可写、可控制、可关闭的文件描述符:

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);  // 创建一个 socket
  • domain:表示系统使用哪个底层协议族,对于 TCP/IP 里说,该参数应为 PF_INET
  • type:指定服务类型,对 TCP/IP 协议族而言
    • SOCK_STREAM 表示传输层使用 TCP 协议;
    • SOCK_DGRAM 表示传输层使用 UDP 协议;
    • 自 2.6.17 内核版本起,该参数可接受 SOCK_NONBLOCK 和 SOCK_CLOEXEC ,分别表示将新创建的 socket 设为非阻塞的,以及用 fork 调用创建子程序时在子进程中关闭该 socket ;在该版本之前,文件描述符的这两个属性都需要使用额外的系统调用(fcntl)来设置。
  • protocol:选择一个具体的协议,不过这个值通常是唯一的,设为 0 ;
  • socket 系统调用成功时返回一个 socket 文件描述符,失败时返回 -1 并设置 errno 。

三、命名 socket

创建 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);
  • bind 将 my_addr 所指的 socket 地址分配给未命名的 sockfd 文件描述符,addrlen 参数指出该 socket 地址的长度;
  • bing 成功时返回 0 ,失败时返回 -1 并设置 errno ,其中常见的两种 errno 如下:
    • EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问;
    • EADDRINUSE:被绑定的地址正在使用中。

四、监听 socket

socket 被命名之后,还不能马上接收客户连接,需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:

#include<sys/socket.h> 
int listen(int sockfd,int backlog);
  • sockfd:指定被监听的 socket ;
  • backlog:提示内核监听队列的最大长度,如果超过 backlog ,服务器将不受理新的客户连接,客户端收到 ECONNREFUSED 错误信息,在内核版本 2.2 之前,backlog 参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的 socket 的上限。之后的版本则表示处于完全连接状态的 socket 上限。处于半连接状态的 socket 上限由 /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);
  • sockfd:执行过 listen 系统调用的监听 socket ;
  • addr:用来获取被接收连接的远端 socket 地址,该地址的长度由 addrlen 指出;
  • 接收成功时返回一个新的连接 socket ,它唯一地标识了被接受的这个连接,服务器可通过读写该 socket 来与被接受连接对应的客户端通信;失败时返回 -1 并设置 errno 。

需要注意的是,若再连接过程中客户端出线程序崩溃、网络掉线等问题,服务器端 accept 调用都能正常返回,说明服务器端 accept 只是从监听队列中取出连接,而不论连接处于什么状态,更不关心任何网络状况的变化。

六、发起连接

如果说服务器通过 listen 调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:

#include<sys/types.h> 
#include<sys/socket.h> 
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
  • sockfd:由 socket 系统调用返回一个 socket ;
  • serv_addr:是服务器监听的 socket 地址,addrlen 参数则指定这个地址的长度;
  • 连接成功时返回 0 ,一旦连接建立,sockfd 就唯一地标识了这个连接,客户端就可以通过读写 sockfd 来与服务器通信;失败时返回 -1 并设置 errno :
    • ECONNREFUSED:目标端口不存在,连接被拒绝;
    • ETIMEDOUT:连接超时。

七、关闭连接

关闭一个连接实际上就是关闭该连接对应的 socket ,这可以通过如下关闭普通文件描述符的系统调用来完成:

#include<unistd.h> 
int close(int fd);
  • fd:待关闭的 socket ,但是 close 系统调用并非总是立即关闭一个连接,而是将 fd 的引用计数减一,只有当计数为 0 时才真正关闭。多进程程序中,一次 fork 系统调用默认将使父进程中打开的 socket 的引用计数加一,因此必须在父进程和子进程中都对该 socket 执行 close 调用才能关闭连接。

相对的,还提供了强制终止连接:

#include<sys/socket.h> 
int shutdown(int sockfd,int howto);
  • socket:待关闭的 socket ;
  • howto:决定了关闭的行为:
    可选值 含义
    SHUY_RD 关闭 sockfd 上读的这一半,应用程序不能再针对 socket 文件描述符执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃
    SHUT_WR 关闭 sockfd 上写的这一半,sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该 socket 文件描述符执行写操作,这种情况下连接处于半关闭状态
    SHUT_RDWR 同时关闭读写
  • 关闭成功后返回 0 ,失败时返回 -1 并设置 errno 。

八、数据读写

1. TCP 数据读写

对文件的读写操作 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);
  • recv:读取 sockfd 上的数据,buf 和 len 参数分别指定读缓冲区的位置和大小,flags 通常设为 0 ;读取成功时返回实际读取到的数据的长度,可能小于 len ,因此可能要多次调用 recv 才能读取到完整的数据;若返回值为 0 表示通信对方已经关闭连接;读取出错时返回 -1 并设置 errno ;
  • send:向 sockfd 写数据,buf 和 len 参数分别指定写缓冲区的位置和大小。写成功时返回实际写入的数据的长度,失败时返回 -1 并设置 errno ;
  • 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

2. UDP 数据读写

#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);
  • recvfrom:读取 sockfd 上的数据:
    • buf:指定读缓冲区的位置;
    • len:指定读缓冲区的大小;
    • src_addr:由于 UDP 是无连接的,因此每次发送都需要获取发送端的地址;
    • addrlen:指定地址的长度。
  • sendto:向 sockfd 写入数据:
    • buf:指定写缓冲区的位置;
    • len:指定写缓冲区的大小;
    • dest_addr:指定接收端的 socket 地址;
    • addrlen:指定地址长度。
  • flags 和返回值:与 TCP 相同;
  • 特殊:两个系统调用均可用于面向连接的 socket 数据读写,只需要将最后两个参数设置为 NULL 即可。

3. 通用数据读写函数

下面的接口可用于 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);
  • sockfd:指定被操作的目标 socket ;
  • msg:结构体指针:
    • msg_name:指向一个 socket 地址结构变量,指示通信双方的地址。对于 TCP 无意义,被置为 NULL;
    • msg_namelen:指定了 msg_name 所指 socket 地址的长度;
    • msg_iov:iovec 结构体指针;
    • msg_iovlen:表示这样的指针有多少个。对于 recvmsg 来说,数据将被存放在 msg_iovlen 块分散的内存中,这些内存的位置和长度由 msg_iov 指向的数组指定,称为分散读(scatter read);对于 sendmsg 来说,msg_iovlen 块分散内存中的数据将被一并发送,称为集中写(gather writer);
    • msg_control 和 msg_controllen:用于辅助数据的传送;
    • msg_flags:无需设定,它会复制 recvmsg/sendmsg 的 flags 参数的内容以影响数据读写的过程。前者还会在调用结束前将某些更新后的标志设置到 msg_flags 中;
    • flags 和返回值:与 TCP 相同;

九、带外标记

实际应用中,我们通常无法预期带外数据(紧急数据)何时到来。好在 Linux 内核检测到 TCP 紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:

  • I/O 复用产生的异常事件;
  • SIGURG 信号。

但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一 点可通过如下系统调用实现:

#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);
  • getsockname:获取 sockfd 对应的本端 socket 地址,并将其存储与 address 参数指定的内存中,该地址的长度存储与 address_len 参数指向的变量中,如果实际地址长度大于内存区大小,那么将会被截断。成功 0 失败 -1 并设置 errno ;
  • getpeername:获取 sockfd 对应的远端 socket 地址,参数与本端相同。

十一、socket 选项

如果说 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);
  • sockfd:指定被操作的目标 socket ;
  • level:指定要操作哪个协议的选项(即属性),比如 IPv4 、IPv6 、TCP 等;
  • option_name:指定选项的名字;
  • option_value 和 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 算法
  • 成功 0 失败 -1 并设置 errno 。

值得指出的是,对服务器而言,有部分 socket 选项只能在调用 listen 系统调用前针对监听 socket 设置才有效。这是因为连接 socket 只能由 accept 调用返回,而 accept 从 listen 监听队列中接受的连接至少已经完了 TCP 三次握手的前两个步骤(因为 listen 监听队列中的连接至少已进入 SYN_RCVD 状态),这说明服务器已经往被接受连接上发送出了 TCP 同步报文段。但有的 socket 选项却应该在 TCP 同步报文段中设置,比如 TCP 最大报文段选项。对这种情况,Linux 给开发人员提供的解决方案是:对监听 socket 设置这些 socket 选项,那么 accept 返回的连接 socket 将自动继承这些选项。这些 socket 选项包括:

  • SO_DEBUG ;
  • SO_DONTROUTE ;
  • SO_KEEPALIVE ;
  • SO_LINGER ;
  • SO_OOBINLINE ;
  • SO_RCVBUF ;
  • SO_RCVLOWAT ;
  • SO_SNDBUF ;
  • SO_SNDLOWAT ;
  • TCP_MAXSEG ;
  • TCP_NODELAY 。

而对客户端而言,这些 socket 选项则应该在调用 connect 函数之前设置,因为 connect 调用成功返回之后,TCP 三次握手已完成。

1. SO_REUSEADDR 选项

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 地址。

2. OS_RCVBUF 和 SO_SNDBUF 选项

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 接收缓冲区和发送缓冲区的大小没有最小值限制。

3. SO_RCVLOW AT 和 SO_SNDLOWAT 选项

SO_RCVLOWAT 和 SO_SNDLOWAT 选项分别表示 TCP 接收缓冲区和发送缓冲区的低水位标记。它们一般被 I/O 复用系统调用来判断 socket 是否可读或可写。当 TCP 接收缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以从对应的 socket 上读取数据;当 TCP 发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时, I/O 复用系统调用将通知应用程序可以往对应的 socke 上写入数据。默认情况下,TCP 接收缓冲区的低水位标记和 TCP 发送缓冲区的低水位标记均为 1 字节。

4. SO_LINGER 选项

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;  // 滞留时间
};

根据结构体中两个成员变量的不同值,可能产生三种行为:

  • l_onoff = 0:此时 SO_LINGER 不起作用,close 用默认行为进行关闭;
  • l_onoff != 0:此时 close 系统调用立即返回,TCP 模块将丢弃被关闭的 socket 对应的 TCP 发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此这种情况给服务器提供了异常终止一个连接的方法;
  • l_onoff != 0 ,L_linger > 0:此时 close 的行为取决于两个条件:
    • 被关闭的 socket 对应的 TCP 发送缓冲区中是否还有残留的数据;
    • 该 socket 是阻塞的还是非阻塞的。
    • 对于阻塞的 socket ,close 将等待一段长为 l_linger 的时间,直到 TCP 模块发送完所有残留数据并得到对方的确认;
    • 如果这段时间内 TCP 模块没有发送完残留数据并得到对方的确认,那么 close 系统调用将返回 -1 并设置 errno 为 EWOULDBLOCK ;
    • 如果 socket 是非阻塞的,close 将立即返回,此时需要根据其返回值和 errno 来判断残留数据是否已经发送完毕。

十二、网络信息 API

socket 地址的两个要素,即 IP 地址和端口号,都是用数值表示的。 这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。但是 telnet 可以使用服务名来代替端口号,下面两条语句作用相同:

telnet 127.0.0.1 80 
telnet localhost www

上面的例子中,telnet 客户端程序通过调用某些网络信息 API 来实现主机名到 IP 地址的转换,以及服务名称到端口号的转换。

1. gethostbyname 和 gethostbyaddr

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);
  • name:指定目标主机的主机名;
  • addr:指定目标主机的 IP 地址;
  • len:指定 addr 所指 IP 地址的长度;
  • type:指定 addr 所指 IP 地址的类型,其合法取值包括 AF_INET(用于IPv4地址)和 AF_INET6(用 于IPv6地址)。

2. getservbyname 和 getservbyport

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);
  • name:指定目标服务的名字;
  • port:指定目标服务对应的端口号;
  • proto:指定服务类型,给它传递 tcp 表示获取流服务,给它传递 udp 表示获取数据报服务,给它传递 NULL 则表示获取所有类型的服务。

3. getaddrinfo

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);
  • hostname:可以接收主机名,也可以接收字符串表示的 IP 地址(IPv4 采用点分十进制字符串,IPv6 则采用十六进制字符串);
  • service:可以接收服务名,也可以接收字符串表示的十进制端口号;
  • hints:是应用程序给 getaddrinfo 的一个提示,以对 getaddrinfo 的输出进行更精确的控制。可以被设置为 NULL ,表示允许 getaddrinfo 反馈任何可用的结果;
  • result:指向一个链表,该链表用于存储 getaddrinfo 反馈的结果。
  • ai_protocol:指具体的网络协议,其含义和 socket 系统调用的第三个参数相同,通常被设置为 0 ;
  • ai_flags:
    选项 含义
    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 。

4. getnameinfo

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);
  • getnameinfo:将返回的主机名存储在 host 参数指向的缓存中,将服务名存储在 serv 参数指向的缓存中;
  • hostlen 和 servlen:分别指定这两块缓存的长度;
  • flags:控制 getnameinfo 的行为:
    选项 含义
    NI_NAMEREQD 如果通过 socket 地址不能获得主机名,则返回一个错误
    NI_DGRAM 返回数据报服务,大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务。但端口 512 ~ 514 是例外。比如 TCP 中 514 端口提供 shell 登录服务;UDP 中端口 514 提供 syslog 服务
    NI_NUMERICHOST 返回字符串表示的 IP 地址,而不是主机名
    NI_NUMERICSERV 返回字符串表示的十进制端口号,而不是服务名
    NI_NOFQDN 仅返回主机域名的第一部分,比如对主机名 nebula.testing.com 只写入 nebula
  • 成功 0 ,失败错误码:
    选项 含义
    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);

你可能感兴趣的:(计算机网络,linux,网络,学习)