网络编程学习(三)

I/O复用典型使用在下列网络应用场合

1.当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用(5.12)。

2.一个客户同时处理多个套接字是可能的,不过比较少见(Web客户的上下文 16.5)。

3.如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用(6.8)。

4.如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用(8.15)。

5.如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用(inetd守护进程 13.5)。

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


Unix下可用的5种I/O模型:

阻塞式I/O;

非阻塞式I/O;

I/O复用(select和poll);

信号驱动式I/O(SIGIO);

异步I/O(POSIX的aio_系列函数)

与I/O复用密切相关的另一种I/O模型是在多线程中使用阻塞式I/O。这种模型与上述模型即为相似,但它没有使用select阻塞在多个文件描述附上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以自由地调用诸如recvfrim之类的阻塞式I/O系统调用了。

一个输入操作通常包括两个不同的阶段:

1.等待数据准备好;

2.从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。

第二步就是把数据从内核缓冲区复制到应用进程缓冲区。


信号驱动式I/O模型

我们可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。

我们首先开启套接字的信号驱动式I/O功能(25.2),并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理(25.3),也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据以准备好被处理,也可以是数据报已准备好被读取。


异步I/O模型

POSIX规范定义的实时函数的工作机制是:告知内核启动某个操作,并让内核在整个 操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。

与前一个模型的区别是:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。本例子中我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式I/O模型。


阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。


尽管timeval结构允许我们指定了一个微秒级的分辨率,然而内核支持的真实分辨率往往粗糙得多。举例来说,许多Unix内核把超时值向上舍入成10ms的倍数。另外还涉及调度延迟,也就是说定时器时间到后,内核还需花一点时间调度相应进程运行。

有些linux版本会修改这个timeval结构。因此从移植性考虑,我们应该假设该timeval结构在select返回时未被定义,因而每次调用select之前都得对它进行初始化。POSIX规定对该结构使用const限定词。

select支持的异常条件只有两个:

1.某个套接字的带外数据的到达

2.某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。


void FD_ZERO(fd_set *fdset);

void FD_SET(int fd, fd_set *fdset);

void FD_CLR(int fd, fd_set *fdset);

void FD_ISSET(int fd, fd_set *fdset);

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

maxfdp1指定的是描述符的个数而非最大值,而描述符是从0开始的。存在这个参数以及计算其值的额外负担纯粹是为了效率原因。

select返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1。

使用select时最常间的两个编程错误是:忘了对最大描述符加1;忘了描述符是值-结果参数。第二个错误导致调用select时,描述符集内我们认为是1的位却被置为0。

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


描述符就绪条件:

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

1.该套接字接受缓冲区中的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小。对着样的套接字执行读操作将不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。

2.该连接的读半部关闭(也就是接受了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。

3.该套接字是一个监听套接字且已完成的连接数不为0。对这样的套接字的accept通常不会阻塞,但后面会讲解accept可能阻塞的一种时序条件(15.6)。

4.其上有一个套接字错误处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt清除。

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

1.该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞(16),写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。

2.该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。

3.使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。

4.其上有一个套接字错误待处理。对着样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清楚。

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

注意:当某个套接字上发生错误时,它将由select标记为既可读又可写。

接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。

任何UDP套接字只要其发送低水位标记小于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。



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

1.close把描述符的引用技术减1,仅在该计数变为0时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。

2.close终止读和写两个方向的数据传送。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据发送给我们。


假设服务器是在前台启动的,那么描述符0、1和2分别被设置为标准输入、标准输出和标准错误输出。

可见监听套接字的第一个可用描述符是3。



poll识别三类数据:普通(normal)、优先级带(priority band)和高优先级(high priority)。

POLLIN可被定义为POLLRDNORM和POLLRDBAND的逻辑或。POLLOUT等同于POLLWRNORM。

1.所有正规TCP数据和所有UDP数据都被认为是普通数据。

2.TCP的带外数据(.24)被认为是优先级带数据。

3.当TCP连接的读半部关闭时(譬如受到了一个来自对端的FIN),也被认为是普通数据,随后的读操作将返回0.

4.TCP连接存在错误既可认为是普通数据,也可认为是错误(POLLERR)。无论哪种情况,随后的读操作将返回-1,并把errno设置成合适的值。这可用于处理诸如受到RST或发生超时等条件。

5.在监听套接字上有新的连接可用既可认为是普通数据,也可认为是优先级数据。大多数实现视之为普通数据。

6.非阻塞式connect的完成被认为是使使相应套接字可写。

结构数组中元素的个数是由nfds(nfds_t -- unsigned int/unsigned long)参数指定。


当发生错误时,poll函数的返回值为-1,若定时器到时之前没有任何描述符就绪,则返回0,否则返回就绪描述符的个数,即revents成员值非0的描述符个数。

如果我们不再关心某个特定描述符,那么可以把与它对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将它的revents成员的值置为0。

就FD_SETSIZE以及就每个描述符集中最大描述符数目相比每个进程中最大描述符数目展开的讨论(6.3结尾),有了poll就不再有那样的问题了,因为分配一个pollfd结构的数组并把该数组中元素的数目通知内核成了调用者的责任。内核不再需要知道类似fd_set的固定大小的数据类型。


我们以批量方式运行用select编写的回射客户程序,发现即使已经遇到了用户输入的结尾,仍可能有数据处于去往或来自服务器的管道中。处理这种情形要求使用shutdown函数,这使得我们能够用上TCP的半关闭特性。

混合使用stdio缓冲机制(我们自己的readline缓冲机制也不例外)和select的危险促成我们提供针对缓冲区而不是文本行操作的回射客户程序和服务器程序的正确版本。

你可能感兴趣的:(网络编程学习(三))