探讨Linux网络编程基础API与内核中TCP/IP协议族之间的关系,并未后续章节提供编程基础。从3个方面讨论Linux网络API.
socket地址API。socket 最开始的含义是一个IP地址和端口对(ip, port)。它唯一地表示了使用TCP通信的一端。本书称其为socket地址。
socket基础API。socket的主要API都定义在 sys/socket.h 头文件中,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、 获取地址信息、检测带外标记,以及读取和设置socket选项。
网络信息API。Linux 提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。 这些API都定义在 netdb.h 头文件中,我们将讨论其中几个主要的函数。
Linux定义了新的通用socket地址结构体,而且还是内存对齐的(__ss_aligin成员的作用)
所有专用socket地址(以及sockaddr storage)类型的变量 在实际使用时都要转化为通用socket地址类型sockaddr(强制转换即可), 因为所有socket编程接口使用的地址参数的类型都是sockaddr。
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
inet_ntoa不可重入,非线程安全,该函数内部用一个静态变量存储转化结果, 函数返回值指向该静态内存。
inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址 或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst 指向的内存中。其中,
inet_ntop函数进行相反的转换,前3个参数的含义与inet_pton的参数相同,最后一个参数cnt 指定目标存储单元大小。下面的2个宏可以帮助我们指定这个大小(分别用于IPv4和IPv6)
UNIX/Linux的一个哲学是:所有的东西都是文件。socket也不例外,它就是可读、可写、 可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket:
protocol 参数是在前面两个参数构成的协议集合下,再选择一个具体协议。通常为0,使用默认协议。
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定成为给socket命名。
在服务器程序中,我们通常要命名socket,因为只有命名之后客户端才能知道该如何连接它。 客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。
socket被命名之后,还不能马上接受客户端连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户端连接:
addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。
accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应的客户端通信。
如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
一旦成功建立连接,sockfd 就唯一表示了这个连接,客户端就可以通过读写sockfd来与服务器通信。
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:
不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1.只有当fd的引用计数为0时,才真正关闭连接。 在多进程中,一次fork系统调用默认将是父进程中打开的socket的引用计数加1,因此必须在父进程和子进程中 都对该socket执行close调用才能真正将连接关闭。
无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用:
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用, 它们增加了对数据读写的控制。 其中适用于TCP流数据读写的系统调用是:
recv读取sockfd上的数据, buf和len参数分别制定读写缓冲区的位置和大小, flags参数通常设置为0。 recv可能返回0,这意味着通信对方已经关闭连接了。 recv读取到的数据可能小于期望的长度,因此可能需要多次调用recv,才能读取到完整的数据。
send往sockfd上写入数据, buf和len参数分别指定写缓冲区的位置和大小。 flags参数为数据收发提供了额外的控制(MSG_MORE)
UDP通信没有连接的概念,每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。
如果是,sockatmark返回1,此时我们就可以利用MSG_OOB标志的recv调用来接收带外数据。 如果不是,则sockatmark返回0。
在某些情况下,我们想知道一个连接socket的本端socket地址, 以及远端的socket地址。 下面这2个函数正是用于解决这个问题:
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数 指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。 如果实际socket地址的长度大于address所指内存的大小, 那么该socket地址将被截断。
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法, 那么下面两个系统调用则是专门用来读取和设置socket文件描述符属性的方法:
sockfd参数指定被操作的目标socket。 level参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、TCP等。 option_name参数则指定选项的名字。 option_value和option_len参数分别是被操作选项的值和长度。
对服务器而言,有部分socket选项要在监听(listen)前针对监听socket设置才有效。 对客户端而言,这些socket选项则应在调用connect函数之前设置, 因为connect调用成功之后,TCP三次握手已完成。
SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。 不过,当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时, 系统都会将其值加倍,并且不得小于某个最小值。
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区 和发送缓冲区的低水位标记。 它们一般被I/O复用系统调用,用来判断socket是否可读或可写。
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。 默认情况下,当我们使用close系统调用来关闭一个socket时, close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区 中残留的数据发送给对方。
gethostbyname 函数根据主机名称获取主机的完整信息, gethostbyaddr函数根据IP地址获取主机的完整信息。 gethostbyname函数通常先在本地的 /etc/hsots配置的文件中查找主机, 如果没有找到,再去访问DNS服务器。
getservbyname函数根据名称获取某个服务的完整信息, getsrvbyport函数根据端口号获取某个服务的完整信息。 他们实际上都是通过读取 /etc/services 文件来获取服务信息的。
name参数指定目标服务器的名字,port参数指定目标服务对应的端口号, proto参数指定服务类型。
hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4用点分十进制 字符串,IPv6用十六进制字符串)。 同样,service参数可以接收服务名,也可以接收字符串表示的十进制端口号。 hints参数是应用程序给getaddrinfo的一个提示,一对getaddrinfo的输出进行更精确的控制。 result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
getaddrinfo将隐式地分配堆内存(可通过valgrind工具查看), 因为res指针原本没有指向一块合法内存的, 所以,getaddrinfo调用结束后,必须使用如下配对函数来释放这块内存:
getnameinfo将返回的主机名存储在host参数指向的缓存中, 将服务名存储在serv参数指向的缓存中, hostlen和servlen参数分别指定这两块缓存的长度。 flags参数控制getnameinfo的行为。
Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。 同样,下面的函数可将表5-8(getaddrinfo和getnameinfo的错误码)的错误码转换成其字符串形式:

