《UNIX网络编程卷1》笔记 基本套接字编程部分

UNIX网络编程必读书籍——《UNIX网络编程卷1》 基本套接字编程 笔记

套接字编程简介

套接字地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义它自己的套接字地址结构。这些结构的名字均以 sockaddr_开头,并以对应每个协议族的唯一后缀结尾。

IPv4套接字地址结构

IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以 sockaddr_in命名,定义在<netinet/in.h>头文件中。POSIX定义:

下面给出几点说明:

  • 并不是所有厂商都支持sin_len,即使有长度字段,我们也无须设置和检查它,除非涉及路由套接字,处理来自不同协议族的套接字地址结构的例程(例如路由表处理代码)在内核中使用的。
  • POSX规范只需要这个结构中的3个字段:sin_fari1y、 sin_addr和sin_port,

通用套接字地址结构

当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是以指向该结构的指针)来传递。然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构。

现在所采用的方法是在<sys/socket.h>头文件中定义一个通用的套接字地址结构:

于是套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一。这就要求对这些函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行强制转换(casting),变成指向某个通用套接字地址结构的指针

从应用程序开发人员的观点看,这些通用套接字地址结构的唯一用途就是对指向特定于协议的套接字地址结构的指针执行类型强制转换

套接字地址结构比较

值-结果参数

当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。

(1)从进程到内核传递套接字地址结构的函数有3个:bind、connect和 sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小。既然指针和指针所指内容的大小都传递给了内核,于是内核知道到底需从进程复制多少数据进来。

(2)从内核到进程传递套接字地址结构的函数有4个: accept、 recvfrom、 getsockname和 getpeername。这4个函数的其中两个参数是指向某个套接字地址结构的指针指向表示该结构大小的整数变量的指针。把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:当函数被调用时,结构大小是一个值(value),它告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果(result.),它告诉进程内核在该结构中究竟存储了多少信息。这种类型的参数称为值-结果(value-result)参数。

在网络编程中,值-结果参数最常见的例子是所返回套接字地址结构的长度

字节排序函数

内存中有两种存储多字节数的方法:一种是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序

不同的主机中使用的字节序是不一样的。

套接字地址结构中的某些字段必须按照网络字节序进行维护。因此我们要关注如何在主机字节序和网络字节序之间相互转换。这两种字节序之间的转换使用以下4个函数:

在这些函数的名字中,h代表host,n代表network,s代表short,l代表long。

字节操作函数

操纵多字节字段的函数有两组,它们既不对数据作解释,也不假设数据是以空字符结束的C字符串。当处理套接字地址结构时,我们需要这些类型的函数,因为我们需要操纵诸如IP地址这样的字段,这些字段可能包含值为0的字节,却并不是C字符串。以空字符结尾的C字符串是由在<string.h>头文件中定义、名字以str(表示字符串)开头的函数处理的。

bzero把目标字节串中指定数目的字节置为0。我们经常使用该函数来把一个套接字地址结构初始化为0。 bcopy将指定数目的字节从源字节串移到目标字节串。bcmp比较两个任意的字节串,若相同则返回值为0,否则返回值为非0。

memset把目标字节串指定数目的字节置为值c。 memcpy类似bcopy,不过两个指针参数的顺序是相反的。当源字节串与目标字节串重叠时,bco能够正确处理,但是 memcpy的操作结果却不可知。这种情形下必须改用 ANSI C的 mernmove函数。

inet_aton、inet_addr和inet_ntoa函数

在ASCII字符串(这是人们偏爱使用的格式)与网络字节序的二进制值(这是存放在套接字地址结构中的值)之间转换网际地址需要一些函数进行实现。

inet_aton、inet_addr和 inet_ntoa在点分十进制数串(例如“206,168,112.96”)与它长度为32位的网络字节序二进制值间转换IP4地址:

inet_pton和inet_ntop函数

这两个函数是随Iv6出现的新函数,对于IPv4地址和Pv6地址都适用。本书通篇都在使用这两个函数。函数名中和n分别代表表达( presentation)和数值( numeric)。地址的表达格式通常是ASCⅡ字符串,数值格式则是存放到套接字地址结构中的二进制值

sock_ntop和相关函数

inet_ntop的一个基本问题是:它要求调用者传递一个指向某个二进制地址的指针,而该地址通常包含在一个套接字地址结构中,这就要求调用者必须知道这个结构的格式和地址族。

为了解决这个问题,我们将自行编写一个名为sock_ntop的函数,它以指向某个套接字地址结构的指针为参数,查看该结构的内部,然后调用适当的函数返回该地址的表达格式。

1
char *sock_ntop(const struct sockaddr *sockaddr, sockelen_t addrlen);

仅为AF_INET下的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *sock_ntop(const struct sockaddr *sockaddr, sockelen_t addrlen)
{
char portstr[8];
static char str[128];

switch(sockaddr->sa_family){
case AF_INET:
{
struct sockaddr_in *sin = (struct sockaddr_in *) sockeaddr;
if(inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return NULL;
if(ntohs(sin->sin_port) != 0)
{
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return str;
}
}
}

readn、writen和readline函数

字节流套接字(例如TCP套接字)上的read和 write函数所表现的行为不同于通常的文件IO。字节流套接字上调用read或 write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限

因为这个原因,我们可以在原始的读写函数的基础上,为了使得I/O传输所有的字符,我们不断调用read、write直到对所需要的数据完成所有的传输

基本TCP套接字编程

下图给出了TCP客户与服务器进程之间发生的一些典型事件的时间表:

socket函数

为了执行网络O,一个进程必须做的第一件事情就是调用 socket函数,指定期望的通信协议类型(使用IPv4的TCP、使用IPv6的UDP、Unix域字节流协议等)。

1
2
3
4
5
#include <sys/socket.h> 

int socket (int family, int type, int protocol);

// Returns: non-negative descriptor if OK, -1 on error

其中family参数指明协议族。该参数也往往被称为协议域。type参数指明套接字类型。protocol参数应设为某个协议类型常值,或者设为0,以选择所给定mi和nye组合的系统默认值。

socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符(socket descriptor),简称sockfd

对比AF_XXX和PF_XXX

AF前缀表示地址族,PF前缀表示协议族。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF值用来创建套接字,而AF值用于套接字地址结构。但实际上,支持多个地址族的协议族从来就未实现过,而且头文件<sys/socket.h>中为一给定协议定义的PF值总是与此协议的AF值相等。

connect函数

TCP客户用 connect函数来建立与TCP服务器的连接

1
2
3
4
#include <sys/socket.h> 

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
// Returns: 0 if OK, -1 on error

sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。客户在调用函数 connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。

建立连接是通过TCP三次握手建立的,建立连接中产生的具体错误可以通过返回值进行判断

bind函数

bind函数把一个本地协议地址赋予一个套接字

1
2
3
4
5
#include <sys/socket.h> 

int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

// Returns: 0 if OK,-1 on error

第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

  • 服务器在启动时捆绑它们的众所周知端口
  • 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户,这就为在该套接字上发送的IP数据报指派了源IP地址。对于TCP服务器,这就限定该套接字只接收那些目的地为这个P地址的客户连接。

如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址

listen函数

listen函数仅由TCP服务器调用,它做两件事情

  1. 当 socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
  2. 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
1
2
3
4
5
#include <sys/socket.h> 

int listen (int sockfd, int backlog);

// Returns: 0 if OK, -1 on error

为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:

  1. 未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN RCVD状态。
  2. 已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于 ESTABLISHED状态

accept函数

accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接

1
2
3
4
5
#include <sys/socket.h>

int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

// Returns: non-negative descriptor if OK, -1 on error

参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。 addrlen是值-结果参数:调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。

如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。在讨论 accept函数时,我们称它的第一个参数为监听套接字(listening socket)描述符(由socke创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字( connected socket)描述符。

区分这两个套接字非常重要。一个服务器通常仅仅创建个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

并发服务器

然而当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户。Unix中编写并发服务器程序最简单的办法就是foxk一个子进程来服务每个客户。

当一个连接建立时, accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字clientfd),父进程则等待另一个连接(通过监听套接字listened)。既然新的客户由子进程提供服务,父进程就关闭已连接套接。

close函数

close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或wrie的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。既然引用计数值仍大于0,这个close调用并不引发TCP的四分组连接终止序列。对于父进程与子进程共享已连接套接字的并发服务器来说,这正是所期望的。如果我们确实想在某个TCP连接上发送一个FⅠN,那么可以改用shutdow函数以代替close。

I/O复用:select和poll函数

概述

有的时候,进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。这个能力称为I/O复用(I/O multiplexing),是由select和poll这两个函数支持的。

IO复用典型使用在下列网络应用场合:

  • 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
  • 一个客户同时处理多个套接字是可能的,不过比较少见。
  • 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用
  • 如果一个服务器即要处理TCP,又要处理UDP,一般就要使用I/O复用
  • 如果一个服务器要处理多个服务或者多个协议,一般就要使用IO复用。

IO复用并非只限于网络编程,许多重要的应用程序也需要使用这项技术。

I/O模型

在介绍 select和poll这两个函数之前,我们需要回顾整体,查看Uniⅸ下可用的5种I/O模型的基本区别:

  • 阻塞式I/O;
  • 非阻塞式I/O;
  • IO复用(select和poll);
  • 信号驱动式IO(SIGIO);
  • 异步I/O(POSIX的aio_系列函数)。

阻塞式I/O模型

默认情形下,所有套接字都是阻塞的

在图中,进程调用 recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断

非阻塞式I/O模型

进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom时,我们称之为轮询

IO复用模型

有了I/O复用(I/O multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上

我们阻塞于se]ect调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用 recvfrom把所读数据报复制到应用进程缓冲区。

信号驱动式IO模型

我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式I/O( signal-driven I/O)

异步I/O模型

这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式IO是由内核通知我们何时可以启动一个O操作,而异步LO模型是由内核通知我们IO操作何时完成

各种I/O模型的比较

上图对比了上述5种不同的I/O模型。可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。

select 函数

该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。也就是说,我们调用 select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。

1
2
3
4
5
6
7
#include <sys/select.h>

#include <sys/time.h>

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

// Returns: positive count of ready descriptors, 0 on timeout, –1 on error

最后一个参数它告知内核等待所指定描述符中的任何一个就绪可花多少时间。

中间的三个参数 readset、 writeset和 excepts指定我们要让内核测试读、写和异常条件的描述符。目前支持的异常条件只有两个:
(1)某个套接字的带外数据的到达
(2)某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息

如何给这3个参数中的每一个参数指定一个或多个描述符值是一个设计上的问题。select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符

select函数的中间三个参数 readset、 writeset和 excepts中,如果我们对某一个的条件不感兴趣,就可以把它设为空指针。事实上,如果这三个指针}均为空,我们就有了一个比UNIX的s1eep函数更为精确的定时器( sleep睡眠以秒为最小单位)。

头文件<sys/select.h>中定义的 FD_SETSIZE常值是数据类型 fd_set中的描述符总数,其值通常是1024,不过很少有程序用到那么多的描述符。maxfdpl参数迫使我们计算出所关心的最大描述符并告知内核该值

select函数修改由指针 readset、 writeset和 excepts所指向的的描述符集,因而这三个参数都是值结果参数。调用该函数时,我们指定所关心的描述符的值,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,我们使用 FD_ISSET宏来测试 fa_set数据类型中的描述符描述符集内任何与未就绪描述符对应的位返回时均清成0。为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1。

该函数的返回值表示跨所有描述符集的已就绪的总位数。如果在任何描述符就绪之前定时器到时,那么返回0。返回-1表示出错(这是可能发生的,譬如本函数被一个所捕获的信号中断)。

描述符就绪条件

我们一直在讨论等待某个描述符准备好I/O(读或写)或是等待其上发生一个待处理的异常条件(带外数据)。尽管可读性和可写性对于普通文件这样的描述符显而易见,然而对于引起select返回套接字“就绪”的条件我们必须讨论得更明确些

满足下列四个条件中的任何一个时,一个套接字准备好读

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。
  2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
  3. 该套接字是一个监听套接字且已完成的连接数不为0。对这样的套接字的accept通常不会阻塞
  4. 其上有一个套接字错误待处理。

下列四个条件中的任何一个满足时,一个套接字准备好写

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。
  2. 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIP信号
  3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终
  4. 其上有一个套接字错误待处理。

如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。

shutdown函数

终止网络连接的通常方法是调用close函数。不过cose有两个限制,却可以使用shutdown来避免:

  1. close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用 shutdown可以不管引用计数就激发TCP的正常连接终止序列
  2. close终止读和写两个方向的数据传送。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。

1
2
3
4
5
#include <sys/socket.h> 

int shutdown(int sockfd, int howto);

// Returns: 0 if OK, –1 on error

该函数的行为依赖于howo参数的值:

  • SHUT_RD——关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用 shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。
  • SHUT_WR——关闭连接的写这一半——对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。我们已经说过,不管套接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。
  • SHUT_RDWR——连接的读半部和写半部都关闭——这与调用shutdown两次等效:第一次调用指定 SHUT_RD,第二次调用指定 SHUT_WR

pselect 函数

1
2
3
4
5
6
7
8
#include <sys/select.h>
#include <signal.h>

#include <time.h>

int pselect (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);

// Returns: count of ready descriptors, 0 on timeout, –1 on error

pselect相对于通常的select有两个变化。

  • pselect使用timespec结构,而不使用timeval结构。前者指定纳秒数,后者指定微秒数
  • pselect函数增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用select,告诉它重新设置信号掩码。

poll 函数

poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

1
2
3
4
5
#include <poll.h>

int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);

// Returns: count of ready descriptors, 0 on timeout, –1 on error

第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

要测试的条件由 events成员指定,函数在相应的 revents成员中返回该描述符的状态。

套接字选项

getsockopt和setsockopt函数

1
2
3
4
5
6
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval socklen_t optlen);

// Both return: 0 if OK,–1 on error

其中sockfd必须指向一个打开的套接字描述符,level(级别)指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP)。optval是一个指向某个变量(*optval)的指针, setsockopt从*optval中取得选项待设置的新值, getsockopt则把已获取的选项当前值存放到*optval冲中。*optval的大小由最后一个参数指定,它对于setsockopt是一个值参数,对于 getsockopt是一个值-结果参数

套接字选项粗分为两大基本类型:一是启用或禁止某个特性的二元选项(称为标志选项)二是取得并返回我们可以设置或检查的特定值的选项(称为值选项)。

通用套接字选项

通用套接字选项是与协议无关的,不过其中有些选项只能应用到某些特定类型的套接字中。

SO_BROADCAST套接字选项

本选项开启或禁止进程发送广播消息的能力。只有数据报套接字支持广播,并且还必须是在支持广播消息的网络上(例如以太网、令牌环网等)。我们不可能在点对点链路上进行广播,也不可能在基于连接的传输协议(例如TCP和SCTP)之上进行广播。

由于应用进程在发送广播数据报之前必须设置本套接字选项,因此它能够有效地防止一个进程在其应用程序根本没有设计成可广播时就发送广播数据报。

SO_DEBUG套接字选项

本选项仅由TCP支持。当给一个TCP套接字开启本选项时,内核将为TCP在该套接字发送和接收的所有分组保留详细跟踪信息。这些信息保存在内核的某个环形缓冲区中,并可使用trpt程序进行检查。

SO_DONTROUTE套接字选项

本选项规定外出的分组将绕过底层协议的正常路由机制

SO_ERROR套接字选项

当一个套接字上发生错误时,源自 Berkeley的内核中的协议模块将该套接字的名为so_error的变量设为标准的Unix Exxx值中的一个,我们称它为该套接字的待处理错误( pendingeror)。内核能够以下面两种方式之一立即通知进程这个错误。

  1. 如果进程阻塞在对该套接字的 select调用上,那么无论是检查可读条件还是可写条件, select均返回并设置其中一个或所有两个条件。
  2. 如果进程使用信号驱动式I/O模型,那就给进程或进程组产生一个SIGTO信号。

SO_KEEPALIVE套接字选项

给一个TCP套接字设置保持存活( keep-alive)选项后,如果2小时内在该套接字的任一方向上都没有数据交换,TCP就自动给对端发送一个保持存活探测分节( keep-alive probe)。这是一个对端必须响应的TCP分节,它会导致以下三种情况之一

  • 对端以期望的ACK响应。应用进程得不到通知(因为一切正常)。在又经过仍无动静的时后,TCP将发出另一个探测分节。
  • 对端以RST响应,它告知本端TCP:对端已崩溃且己重新启动。该套接字的待处理错误被置为 ECONNRESET,套接字本身则被关闭。
  • 对端对保持存活探测分节没有任何响应。

本选项的功用是检测对端主机是否崩溃或变得不可达(譬如拨号调制解调器连接掉线,电源发生故障,等等)。如果对端进程崩溃,它的TCP将跨连接发送一个FIN,这可以通过调用select很容易地检测到。同时也要认识到,即使对任何保持存活探测分节均无响应(第三种情况),我们也不能肯定对端主机已经崩溃,因而TCP可能会终止一个有效连接。

本选项一般由服务器使用,不过客户也可以使用。服务器使用本选项是因为它们花大部分时间阻塞在等待穿越TCP连接的输入上,也就是说在等待客户的请求。然而如果客户主机连接掉线、电源掉电或系统崩溃,服务器进程将永远不会知道,并将继续等待永远不会到达的输入我们称这种情况为半开连接( half-open connection)。保持存活选项将检测出这些半开连接并终止它们。

SO_LINGER套接字选项

本选项指定close函数对面向连接的协议(例如TCP和SCTP,但不是UDP)如何操作。默认操作是close立即返回,但是如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端。

客户可以设置so_LINGER套接字选项,指定一个正的延滞时间。这种情况下客户的close要到它的数据和FIN已被服务器主机的TCP确认后才返回

然而我们会面临一个问题:在服务器应用进程读剩余数据之前,服务器主机可能崩溃,并且客户应用进程永远不会知道。更糟糕的是,下图展示了当给 SO_LINGER选项设置偏低的延滞时间值时可能发生的现象。

让客户知道服务器已读取其数据的一个方法是改为调用shutdown(并设置它的第二个参数为 SHUT_WR)而不是调用close,并等待对端close连接的当地端(服务器端)。

IPv4套接字选项

这些套接字选项由IPv4处理,它们的级别(即 getsockopt和 setsockopt函数的第二个参数)为IPPROTO_IP。

IP_HDRINCL套接字选项

如果本选项是给一个原始IP套接字设置的,那么我们必须为所有在该原始套接字上发送的数据报构造自己的IP首部。一般情况下,在原始套接字上发送的数据报其IP首部是由内核构造的,不过有些应用程序(特别是路由跟踪程序 traceroute)需要构造自己的IP首部以取代IP置于该首部中的某些字段。

IP_OPTIONS套接字选项

本选项的设置允许我们在IPv4首部中设置IP选项

IP_RECVDSTADDR套接字选项

本套接字选项导致所收到UDP数据报的且的IP地址由 recvmsg函数作为辅助数据返回

IP_TOS套接字选项

本套接字选项允许我们为TCP、UDP或SCTP套接字设置IP首部中的服务类型字段

TCP套接字选项

TCP有两个套接字选项,它们的级别(即 get_sockopt和 set_sockopt函数的第二个参数)为IPPROTO_TCP。

TCP_MAXSEG套接字选项

本选项允许我们获取或设置TCP连接的最大分节大小(MSS)。返回值是我们的TCP可以发送给对端的最大数据量,它通常是由对端使用SYN分节通告的MSS,除非我们的TCP选择使用个比对端通告的MSS小些的值。如果该值在相应套接字的连接建立之前取得,那么返回值是未从对端收到MSS选项的情况下所用的默认值。

TCP_NODELAY套接字选项

开启本选项将禁止TCP的 Nagle算法。默认情况下该算法是启动的。

Nagle算法的目的在于减少广域网(wAN)上小分组的数目。该算法指出:如果某个给定连接上有待确认数据( outstanding data),那么原本应该作为用戶写操作之响应的在该连接上立即发送相应小分组的行为就不会发生,直到现有数据被确认为止。这里“小”分组的定义就是小于MSS的任何分组。TCP总是尽可能地发送最大大小的分组, Nagle算法的目的在于防止一个连接在任何时刻有多个小分组待确认。

fcntl函数

与代表“file control”(文件控制)的名字相符,fcntl函数可执行各种描述符控制操作。

fcntl函数提供了与网络编程相关的如下特性:

  • 非阻塞式I/O。通过使用F_SETFL命令设置O_NONBLOCK文件状态标志,我们可以把一个套接字设置为非阻塞型。
  • 信号驱动式I/O。通过使用F_SEFL命令设置o_ASYNC文件状态标志,我们可以把一个套接字设置成一旦其状态发生变化,内核就产生一个SIGIO信号。
  • F_SETOWN命令允许我们指定用于接收SIGIO和SIGURG信号的套接字属主(进程ID或进程组IO)。其中 SIGIO信号是套接字被设置为信号驱动式I/O型后产生的,SIGURG信号是在新的带外数据到达套接字时产生的。F_GETOWN命令返回套接字的当前属主。

基本UDP套接字编程

概述

在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。然而相比TCP,有些场合确实更适合使用UDP

下图给出了典型的UDP客户服务器程序的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似地,服务器不接受来自客户的连接,而是只管调用 recvfrom函数,等待来自客户的某个数据到达

recvfrom和sendto函数

这两个函数类似于标准的read和write函数,不过需要三个额外的参数。

1
2
3
4
5
6
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);

// Both return: number of bytes read or written if OK, –1 on error

前三个参数sockfdbuffnbytes等同于read和write函数的三个参数:描述符、指向读入或写出缓冲区的指针和读写字节数

sendto的to参数指向一个含有数据报接收者的协议地址(例如IP地址及端口号)的套接字地址结构,其大小由 addrlen参数指定。 recvfrom的fom参数指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,而在该套接字地址结构中填写的字节数则放在addrlen参数所指的整数中返回给调用者。注意, sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值-结果参数)。

数据报的丢失

我们的UDP客户服务器例子是不可靠的。如果一个客户数据报丢失(譬如说,被客户主机与服务器主机之间的某个路由器丢弃),客户将永远阻塞于dg_cli函数中的recvfrom调用,等待一个永远不会到达的服务器应答。类似地,如果客户数据报到达服务器,但是服务器的应答丢失了,客户也将永远阻塞于recvfrom调用。防止这样永久阻塞的一般方法是给客户的recvfrom调用设置一个超时。

但是仅仅给recvfrom调用设置超时并不是完整的解决办法。

UDP的connect函数

除非套接字已连接,否则异步错误是不会返回到UDP套接字的。我们确实可以给UDP套接字调用 connect(4,3节),然而这样做的结果却与TCP连接大相径庭:没有三路握手过程。内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的P地址和端口号(取自传递给 connect的套接字地址结构),然后立即返回到调用进程。

对于已连接UDP套接字,与默认的未连接UDP套接字相比,发生了三个变化

  1. 我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto,而改用write或send写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址(例如IP地址和端口号)
  2. 我们不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg
  3. 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。

给一个UDP套接字多次调用connect

拥有一个已连接UDP套接字的进程可出于下列两个目的之一再次调用 connect:

  1. 指定新的IP地址和端口号
  2. 断开套接字。

第一个目的(即给一个已连接UDP套接字指定新的对端)不同于TCP套接字中 connect的使用:对于TCP套接字, connect只能调用一次。

性能

当应用进程在一个未连接的UDP套接字上调用sendto时,源自Berkeley的内核暂时连接该套接字,发送数据报,然后断开该连接。在一个未连接的UDP套接字上给两个数据报调用sendto函数于是涉及内核执行下列6个步骤:

  • 连接套接字;
  • 输出第一个数据报;
  • 断开套接字连接;
  • 连接套接字;
  • 输出第二个数据报;
  • 断开套接字连接。

当应用进程知道自己要给同一目的地址发送多个数据报时,显式连接套接字效率更高。调用 connect后调用两次 write涉及内核执行如下步骤:

  • 连接套接字
  • 输出第一个数据报
  • 输出第二个数据报

在这种情况下,内核只复制一次含有目的IP地址和端口号的套接字地址结构,相反当调用两次 sendto时,需复制两次。临时连接未连接的UDP套接字大约会耗费每个UDP传输三分之一的开销。

名字与地址转换

到目前为止,以上所有例子都用数值地址来表示主机,用数值端口号来标识服务器。然而出于许多理由,我们应该使用名字而不是数值:名字比较容易记住;数值地址可以变动而名字保持不变;随着往Iv6上转移,数值地址变得相当长,手工键入数值地址更易出错。

域名系统

域名系统( Domain Name System,DNS)主要用于主机名字与IP地址之间的映射。主机名既可以是一个简单名字(simple name),,也可以是一个全限定域名(Fully Qualified Domain Name, FQDN), 比如说www.baidu.com

资源记录

DNS中记录的条目称为资源记录,主要的有以下几种:

  • A记录把一个主机名映射成一个32位的IPV4地址
  • AAAA称为“四A”把一个主机名映射成一个128位的Iv6
  • 称为“指针记录”( pointer record)的PTR记录把IP地址映射成主机名
  • MX记录把一个主机指定作为给定主机的“邮件交换器”

解析器和名字服务器

每个组织机构往往运行一个或多个名字服务器。常见的解析所做的东西就是把主机名映射成IPv4地址和做相反的映射。

DNS替代方案

不使用DNS也可能获取名字和地址信息。常用的替代方法有静态主机文件(通常是etc/hosts文件)、网络信息系统( Network Information System,NIS)以及轻权目录访问协议( Lightweight Directory Access Protocol,LDAP)。

gethostbyname函数

查找主机名最基本的函数是 gethostbyname。如果调用成功,它就返回一个指向 hostent结构的指针,该结构中含有所查找主机的所有IPv4地址。这个函数的局限是只能返回IPv4地址

1
2
3
4
5
#include <netdb.h>

struct hostent *gethostbyname (const char *hostname);

// Returns: non-null pointer if OK,NULL on error with h_errno se

hostent的数据结构如下:

gethostbyname执行的是对A记录的查询,所以只能够返回IPv4的地址

gethostbyaddr函数

gethostbyaddr的功能刚好和gethostbyname相反,试图由一个二进制的IP地址找到相应的主机名

1
2
3
4
5
#include <netdb.h>

struct hostent *gethostbyaddr (const char *addr, socklen_t len, int family);

// Returns: non-null pointer if OK, NULL on error with h_errno set

getservbyname和 getservbyport函数

像主机一样,服务也通常靠名字来认知。如果我们在程序代码中通过其名字而不是其端口号来指代一个服务,而且从名字到端口号的映射关系保存在一个文件中(通常是/etc/services),那么即使端口号发生变动,我们需修改的仅仅是/etc/services文件中的某行,而不必重新编译应用程序。

getservtbyname函数用于根据给定名字查找相应服务。

1
2
3
4
5
#include <netdb.h>

struct servent *getservbyname (const char *servname, const char *protoname);

// Returns: non-null pointer if OK, NULL on error

返回的servent结构:

服务名参数 servname必须指定。如果同时指定了协议(即protoname参数为非空指针),那么指定服务必须有匹配的协议。有些因特网服务既用TCP也用UDP提供,其他因特网服务则仅仅支持单个协议(例如FTP要求使用TCP)。如果 protoname未指定而 servname指定服务支持多个协议,那么返回哪个端口号取决于实现。

getservbyport用于根据给定端口号和可选协议査找相应服务。

1
2
3
4
5
#include <netdb.h>

struct servent *getservbyport (int port, const char *protoname);

// Returns: non-null pointer if OK, NULL on error