第五章 Linux网络编程基础API
5.1 socket地址API
5.1.1 主机字节序和网络字节序
字节序分为:
1. 大端字节序:一个整数的高位字节(23-31 bit)存储在内存的低地址处,低位字节(0-7 bit)存储在内存的高地址处。
2. 小端字节序:整数的低位字节存储在内存的低地址处,而高位字节则存储在内存的高地址处。
小端字节序又被称为主机字节序。
大端字节序又被称为网络字节序。
判断机器字节序:
union{
short value;
char union_bytes[ sizeof(short) ] ;
}test ;
赋值给test.value 然后比较 test.union_bytes数组,可得到机器字节序。
Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:
#include<netinet/in.h>
unsigned long int htonl( unsigned long int hostlong ) ;
unsigned short int htons( unsigned short int hostshort ) ;
unsigned long int ntohl( unsigned long int netlong ) ;
unsigned short int ntohs( unsigned short int netshort ) ;
长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此,任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。
5.1.2 通用socket地址
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family ;
char sa_data[ 14 ] ;
}
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain)和对应的地址族,如表:
宏PF_* 和AF_* 都定义在bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表:
由表5-2可见,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 )] ;
}
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
5.1.3 专用socket地址
Unix 本地域协议族使用如下专用 socket 地址结构体:
#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 地址结构体,分别用于 IPv4 和 IPv6 :
struct sockaddr_in
{
sa_family_t sin_family ; /* 地址族:AF_INET */
u_intl6_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 。
5.1.4 IP地址转换函数
下面三个函数可用于点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
#include<arpa/inet.h>
in_addr_t inet_addr ( const char * strptr ) ;
int inet_aton ( const char *cp , struct in_addr * inp) ;
char * inet_ntoa ( struct in_addr in ) ;
inet_addr 函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。失败时返回 INADDR_NONE。
inet_aton函数完成和inet_addr 同样的功能,但是将转化结果存储于参数inp 指向的地址结构中 。 成功返回 1 ,失败则返回0 。
inet_ntoa 函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
注意:该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa 是不可重入的 。
下面这对更新的函数也能完成和前面三个函数同样的功能,并且它们同时适用于IPv4和IPv6地址:
#include<arpa/inet.h>
int inet_pton ( int af , const char* src , void* dst ) ;
const char* inet_ntop( int af, ,const void* src , char* dst , socklen_t cnt) ;
inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst 指向的内存中。
af 参数指定地址族(AF_INET或者AF_INET6)
inet_pton成功时返回1,失败则返回0,并设置errno 。
inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个cnt指定目标存储单元的大小。
下面两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6)
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno 。
5.2 创建socket
UNIX/Linux 的一个哲学:万物皆文件。
socket 就是可读,可写,可控制,可关闭的文件描述符。
下面的socket系统调用可创建一个socket:
#include<sys/types.h>
#include<sys/socket.h>
int socket ( int domain , int type , int protocol ) ;
domain 参数告诉系统使用哪个底层协议族,对TCP/IP协议族而言,该参数设置为PE_INET(用于IPv4)或PF_INET6(用于IPv6);对于UNIX本地域协议族而言,该参数应为PF_UNIX。
type 参数指定服务类型,服务类型主要有 SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。
注意:type参数可以接受上述服务类型与下面两个重要的标志(SOCK_NONBLOCK和SOCK_CLOEXEC)相与的值。
SOCK_NONBLOCK表示将新创建的socket 设为非阻塞的。
SOCK_CLOEXEC表示用fork调用创建子进程时在子进程中关闭该socket 。
protocol 参数是在前两个参数构成协议集合下,再选择一个具体的协议,这个值通常唯一(前两个参数已经决定它的值),几乎所有情况下设置为0,表示使用默认协议。
socket系统调用成功时返回socket文件描述符,失败则返回 -1,设置errno。
5.3 命名(绑定) socket
将一个socket与socket地址绑定称为给socket命名。
在服务器程序中,我们通常要命名socket,因为只有命名后客户端才能知道如何连接它。
客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。
命名socket的系统调用 bind
#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地址的长度。
bind成功时返回0,失败则返回-1并设置errno。
其中两种常见的errno是EACCES和EADDRINUSE,含义如下:
EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。
(例如:普通用户将socket绑定到知名服务端口(0-1023)时,bind将返回EACCES错误。)
EADDRINUSE:被绑定的地址正在使用中。
(例如:将socket绑定到一个处于TIME_WAIT状态的socket地址)。
5.4 监听socket
#include<sys/socket.h>
int listen ( int sockfd , int backlog) ;
sockfd参数指定被监听的socket。
backlog参数提示内核监听队列的最大长度。
监听队列的长度如果超过backlog(监听队列中完整连接的上限通常比backlog值略大),服务器将不受理新的客户连接,客户端也将受到ECONNREFUSED错误信息。
注意:backlog只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由 /proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。
backlog参数的典型值为5。
listen成功时返回0,失败则返回-1并设置errno。
5.5 接受连接
下面的系统调用从listen监听队列中接受一个连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept( int sockfd , struct sockaddr *addr , socklen_t *addrlen);
sockfd参数是执行过listen系统调用的监听socket。
addr 参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。
accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。
accept失败时返回-1并设置errno。
如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常(掉线),或提早退出,那么服务器对这个连接执行的accept调用依然成功。
因此注意:accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。
5.6发起连接
服务器通过listen系统调用被动接受连接,客户端通过connect系统调用来与服务器建立连接。
#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参数指定这个地址的长度。
connect成功时返回0,一旦成功建立连接,sockfd就唯一标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。
connect失败则返回-1并设置errno。
其中两种常见的errno是:
1. ECONNREFUSED:目标端口不存在(客户端收到复位报文段),连接被拒绝。
2. ETIMEOUT:连接超时(网络繁忙导致服务器没有发回应答报文,客户端不断重连,多次重连仍然无效则连接超时)。
5.7 关闭连接
关闭一个连接实际就是关闭该连接对应的socket,可以通过如下系统调用完成:
#include<unistd.h>
int close( int fd ) ;
fd 参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd 的引用计数减一。只有当fd 的引用计数为0时,才真正关闭连接。
多进程程序中,一次fork 系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下shutdown系统调用。
#include<sys/socket.h>
int shutdown ( int sockfd , int howto ) ;
sockfd 参数是待关闭的socket 。
howto 参数决定了shutdown的行为可取下面表中的某个值:
由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。
而close在关闭连接时只能将socket上的读和写同时关闭。
shutdown成功时返回0,失败则返回-1并设置errno 。
5.8 数据读写
5.8.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即可。
recv成功时返回实际读取到的数据的长度。
recv可能返回0,这意味着通信对方已经关闭连接了。
recv出错时返回-1并设置errno 。
flags参数为数据收发提供了额外的控制,它可以取下表中的一个或几个的逻辑或
5.8.2 UDP数据读写
socket编程接口中用于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 参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。
sendto 往sockfd 上写入数据,buf 和 len 参数分别指定缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。
这两个系统调用的flags参数以及返回值的含义均与 send/recv系统调用的flags参数以及返回值相同。
注意:recvfrom / sendto 系统调用也可以用于面向流(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略 发送端/接收端的 socket地址(因为已经建立了连接,已经知道socket地址)。
5.8.3 通用数据读写函数
socket编程接口还提供了一对通用的数据读写系统调用。
可用于TCP流数据 和 UDP数据报。
#include<sys/socket.h>
ssize_t recvmsg ( int sockfd , struct msghdr *msg , int flags ) ;
ssize_t sendmsg ( int sockfd , struct msghdr *msg , int flags ) ;
sockfd 参数指定被操作的目标socket, msg参数是msghdr结构图类型的指针,msghdr结构体的定义如下:
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 ;/*复制函数中的flag参数,并在调用过程中更新*/
};
msg_name成员指向一个socket地址结构变量,它指定通信双方的socket地址。对于TCP,该成员必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。
msg_namelen成员则指定了msg_name所指socket地址的长度。
msg_iov成员是iovec结构体类型的指针,iovec结构体的定义如下:
struct iovec{
void *iov_base ; /*内存起始地址 */
size_t iov_len ; /* 这块内存的长度 */
};
msg_iovlen 指定这样的iovec结构对象有多少个。
对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这成为分散读;对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写。
msg_control和msg_controllen 成员用于辅助数据的传送。
msg_flags 成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
recvmsg/sendmsg 的flags参数以及返回值的含义与send/recv的flags参数及返回值相同。
5.9 带外标记
Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收,内核通知应用程序带外数据到达得两种常见方式是:I/O复用产生的异常事件和SIGURG信号。
但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。
可用如下系统调用实现:
#include<sys/socket.h>
int sockatmark ( int sockfd ) ;
sockatmark 判断 sockfd 是否处于带外标记,即下一个读取到的数据是否是带外数据。如果是,sockatmark 返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
5.10 地址信息函数
当我们想知道一个连接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参数指定的内存中,该socket地址的长度则存储于
address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。
getsockname成功时返回0,失败返回-1并设置errno。
getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同。
5.11 socket选项
下面两个系统调用则是专门用来读取和设置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 参数则指定选项的名字。
下表列举了socket通信中几个比较常用的socket选项,option_value和option_len参数分别是被操作选项的值和长度,不同选项具有不同类型的值。
注意,对服务器而言,有部分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三次握手已完成 。
5.11.1 SO_REUSEDDR选项
TCP连接处于TIME_WAIT状态时,服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT状态的连接占用的socket地址。
经过setsocketopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。此外,我们也可以
通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状
态,进而允许应用程序立即重用本地的socket地址。
5.11.2 SO_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接收缓冲区和发送缓冲区的大小没有最小值限制。