《网络编程》I/O 多路复用

在前面的文章中介绍了五种 I/O 模型《I/O 模型》,这里介绍 I/O 模型中 I/O 多路复用在 TCP 套接字编程中的使用。在  I/O 多路复用中主要是 select 和 poll 函数的使用。

select 函数

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

/* IO多路复用 */

/*
 * 函数功能:
 * 返回值:准备就绪的描述符数,若超时则返回0,出错则返回-1;
 * 函数原型:
 */
#include <sys/select.h>
int select(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *tvptr);
/*
 * 说明:
 * 参数maxfdpl是“最大描述符加1”,即指定待测试的描述符个数;
 * 参数readfds、writefds、exceptfds是指向描述符集的指针,即让内核测试读、写或异常条件的描述符;
 * 时间参数有三种取值:
 * tvptr == NULL;
 *      永远等待;若捕获到信号则中断此无限期等待;当所指定的描述符中的一个已准备好或捕获到信号则返回;
 *      若捕获到信号,则select返回-1,errno设置为EINTR;
 *
 * tvptr->tv_sec == 0 && tvptr->tv_usec == 0;
 *      完全不等待;测试所有描述符并立即返回,这是得到多个描述符的状态而不阻塞select函数的轮回方法;
 *
 * tvptr->sec != 0 || tvptr->usec != 0;
 *      等待指定的秒数和微妙数;当指定的描述符已准备好,或超过指定的时间立即返回;
 *      若超过指定的时间还没有描述符准备好,则返回0;
 *
 * tvptr的结构如下:
 */
struct timeval
{
    long tv_sec;    /* seconds */
    long tv_usec;   /* and microseconds */
};

我们可以通过以下函数对 fd_set 数据结构进行处理。声明了一个描述符集后,必须使用 FD_ZERO 清空其所有位达到初始化,然后才可以设置各个位;从 select 返回时,使用 FD_ISSET 测试该集中的一个给定位是否仍旧设置;

#include <sys/select.h>
int  FD_ISSET(int fd, fd_set *fdset); //测试描述符fd是否在描述符集中设置;若fd在描述符集中则返回非0值,否则返回0
void FD_CLR(int fd, fd_set *fdset); //清除在fdset中指定的位fd;
void FD_SET(int fd, fd_set *fdset); //设置fd在fdset中指定的位;
void FD_ZERO(fd_set *fdset); //清除整个fdset;即所有描述符位都为0;

select 函数有三个可能的返回值:

  1. 返回值 -1 表示出错。这种情况下,将不修改其中任何描述符集。
  2. 返回值 0 表示没有描述符准备就绪。若指定的描述符都没有准备就绪,而且指定的时间已经超过,则发生这种情况。此时描述符集都被清 0。
  3. 正返回值表示已经准备就绪的描述符数,该值是三个描述符集中已准备好的描述符之和。三个描述符集中仍旧打开的位对应与已准备就绪的描述符。

描述符就绪条件

套接字描述符准备好读,必须满足以下条件之一:

  1. 该套接字接收缓冲区中的数据字节数 不小于 套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于 0 的值。可以通过 SO_RCVLOWAT 套接字选项设置该套接字的低水位标记;
  2. 该连接的读半部分关闭(即一端已接收到 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回 0(即 EOF 字符);
  3. 该套接字是一个监听套接字且已完成的连接数不为 0 。对这样的套接字的 accept 通常不会阻塞;
  4. 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回 -1(即返回一个错误),并把 errno 设置为确切的错误条件;

套接字描述符准备好写,必须满足以下条件之一:

  1. 该套接字发送缓冲区中的可用空间字节数 不小于 套接字发送缓冲区低水位标记的当前大小,并且该套接字已连接,该套接字不需要连接(如 UDP
    套接字)。这意味着若把这样的套接字设置为非阻塞,写操作将不阻塞并返回一个正值;
  2. 该链接的写半部关闭。对这样的套接字的写操作将产生 SIGPIPE 信号;
  3. 使用非阻塞式 connect 的套接字已建立连接,connect 已经连接失败;
  4. 其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回 -1,同时把 errno 设置成确切的错误条件;

若一个套接字存在带外数据 或仍处于 带外标记,那么它有异常条件待处理。注:当某个套接字发生错误时,它将由 select 标记为既可读又可写。

在前面《基于 TCP 套接字编程的分析》我们知道,当杀死服务器子进程时,同时客户端阻塞于 fgets 调用。客户端 TCP 已经接收到来自服务器子进程的 FIN 报文段,当服务器 TCP 接收到来自客户端的数据时,因为先前打开的套接字的进程已经通过 kill 函数终止,于是响应一个
RST 。然而客户端阻塞于从标准输入读入过程,并没有收到 RST,因此 readline 返回 0(表示 EOF ),则客户端此时并未预期收到 EOF ,则以出错信息”server terminated prematurely“退出。若要避免这种情况,可以使用 I/O 多路复用解决。则使用 select 函数修改前面文章的客户端处理函数 str_cli 函数。这样服务器进程一终止,客户端立即得到通知。

客户端处理函数修改后具有以下的功能:

  1. 若对端 TCP 发送数据,那么套接字变为可读,并且 read 返回一个大于 0 的值;
  2. 若对端 TCP 发送一个 FIN ,那么套接字变为可读,并且 read 返回 0(即 EOF 字符);
  3. 若对端 TCP 发送一个 RST,那么套接字变为可读,并且 read 返回 -1,而 errno 中含有确切的错误代码;
#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1;
	fd_set		rset;
	char		sendline[MAXLINE], recvline[MAXLINE];

	FD_ZERO(&rset);/* 初始化 */
	for ( ; ; ) {
		FD_SET(fileno(fp), &rset);/* 打开标准文件指针 fp 描述符位 */
		FD_SET(sockfd, &rset);/* 打开套接字 sockfd 位 */
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		Select(maxfdp1, &rset, NULL, NULL, NULL);

		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */
			if (Readline(sockfd, recvline, MAXLINE) == 0) /* 若返回时套接字可读,则先读入从服务器回射的文本,并显示到标准输出 */
				err_quit("str_cli: server terminated prematurely");
			Fputs(recvline, stdout);
		}

		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
			if (Fgets(sendline, MAXLINE, fp) == NULL) /* 若返回时标准输入可读,就先用 fgets 读入文本行,再用 writen 把它写到套接字中 */
				return;		/* all done */
			Writen(sockfd, sendline, strlen(sendline));
		}
	}
}

shutdown 函数

终止网络连接通常使用 close 函数,但是 close 有自己的缺陷,我们可以使用 shutdown 函数来避免这些缺陷:

  1. close 把描述符的引用计数减 1,仅在该计数变为 0 时才关闭套接字。然而使用 shutdown 函数可以不管引用计数就激发 TCP 的正常连接终止序列;
  2. close 终止读和写两个方向的数据传送,而 shutdown 函数可以只关闭读或写的一端,或两端都关闭;
/*
 * 函数功能:关闭套接字上的输入或输出;
 * 返回值:若成功则返回0,若出错返回-1;
 * 函数原型:
 */
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/*
 * 说明:
 * sockfd表示待操作的套接字描述符;
 * how表示具体操作,取值如下:
 * (1)SHUT_RD     关闭读端,即不能接收数据
 * (2)SHUT_WR     关闭写端,即不能发送数据
 * (3)SHUT_RDWR   关闭读、写端,即不能发送和接收数据
 *
 */

str_cli 函数

这是最终的版本:

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, stdineof;
	fd_set		rset;
	char		buf[MAXLINE];
	int		n;

	stdineof = 0;/* 表示在主循环中 select 标准输入为可读 */
	FD_ZERO(&rset);
	for ( ; ; ) {
		if (stdineof == 0)
			FD_SET(fileno(fp), &rset);
		FD_SET(sockfd, &rset);
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		Select(maxfdp1, &rset, NULL, NULL, NULL);
<span style="white-space:pre">		</span>/* 当在套接字上读到 EOF 字符,若我们已在标准输入键入 EOF,则正常终止,若没有在标准输入键入 EOF,表示服务器已过早终止 */
		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */
			if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
				if (stdineof == 1)
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");
			}

			Write(fileno(stdout), buf, n);
		}
<span style="white-space:pre">		</span>/* 当在标准输入遇到 EOF,则关闭写端,即客户端不能向服务器发送数据 */
		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
			if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
				stdineof = 1;
				Shutdown(sockfd, SHUT_WR);	/* send FIN */
				FD_CLR(fileno(fp), &rset);
				continue;
			}

			Writen(sockfd, buf, n);
		}
	}
}

poll 函数

该函数与 select 函数类似,只是程序员接口不同。该函数不是为每个状态构造描述符集,而是构造一个 pollfd 结构数组,每个数组元素指定一个描述符编号以及对其所关心的状态。

/*
 * 函数功能:和select函数类似;
 * 函数原型:
 */
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
/*
 * 说明:
 * timeout == -1;   永远等待。
 * timeout == 0;    不等待,测试所有的描述符并立即返回。
 * timeout > 0;     等待timeout毫秒,当指定的描述符之一已经准备好,或指定的时间值已经超过时立即返回。
 */
struct pollfd{
int fd; /* file descriptor to check,or <0 to ignore */
short events; /* events of interest on fd */
short revents;  /* events that occurred on fd */
}; 

参考资料:

《Unix 网络编程》

时间: 2024-10-18 02:49:47

《网络编程》I/O 多路复用的相关文章

Python网络编程:IO多路复用

io多路复用:可以监听多个文件描述符(socket对象)(文件句柄),一旦文件句柄出现变化,即可感知. 1 sk1 = socket.socket() 2 sk1.bind(('127.0.0.1',8001)) 3 sk1.listen() 4 5 # sk2 = socket.socket() 6 # sk2.bind(('127.0.0.1',8002)) 7 # sk2.listen() 8 while True: 9 conn,address = sk.accept()#阻塞等待客户端

2017.07.12 Python网络编程之使用多路复用套接字I/O

1.在本章开始之前,需要先理解同步与异步,阻塞与非阻塞的区别: "阻塞"与"非阻塞"与"同步"与"异步"不能简单的从字面理解,提供一个从分布式系统角度的回答.1.同步与异步同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回.但是一旦调用返回,就得到返回值了.换句话

unix网络编程——I/O多路复用之epoll

1. 基本概念 当程序进行IO时,如果数据尚未准备好,那么IO将处于阻塞状态.当某个进程有多个打开的文件,比如socket,那么其后的所有准备好读写的文件将受到阻塞的影响而不能操作.不借助线程,单一进程无法在同一时间服务多个文件描述符.非阻挡式IO可以作为一个解决方案,但是效率并不高.首先进程需要不断发IO请求,其次,如果程序可以休眠,让出CPU将提高效率.多任务式IO是在其中任何一个文件描述符就绪时收到通知,此时IO将不会受到阻挡,其余时间处于休眠状态,将CPU资源让给别的进程. 为了实现I/

linux网络编程学习笔记之六 -----I/O多路复用服务端

多进程和多线程的目的是在于最大限度地利用CPU资源,当某个进程不需要占用太多CPU资源,而是需要I/O资源时,可以采用I/O多路复用,基本思路是让内核把进程挂起,直到有I/O事件发生时,再把控制返回给程序.这种事件驱动模型的高效之处在于,省去了进程和线程上下文切换的开销.整个程序运行在单一的进程上下文中,所有的逻辑流共享整个进程的地址空间.缺点是,编码复杂,而且随着每个逻辑流并发粒度的减小,编码复杂度会继续上升. I/O多路复用典型应用场合(摘自UNP6.1) select的模型就是这样一个实现

Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型

Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 SocketChannel 的关系 事件监听类型 字节缓冲 ByteBuffer 数据结构 场景 接着上一篇中的站点访问问题,如果我们需要并发访问10个不同的网站,我们该如何处理? 在上一篇中,我们使用了java.net.socket类来实现了这样的需求,以一线程处理一连接的方式,并配以线程池的控制

Linux网络编程——多路复用之epoll

目录 Linux网络编程--多路复用之epoll 基础API 实例一.epoll实现在线聊天 实例二.epoll实现在客户端断开后服务端能一直运行,客户端可以多次重连 Linux网络编程--多路复用之epoll ? epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,

物联网网络编程、Web编程综述

本文是基于嵌入式物联网研发工程师的视觉对网络编程和web编程进行阐述.对于专注J2EE后端服务开发的童鞋们来说,这篇文章可能稍显简单.但是网络编程和web编程对于绝大部分嵌入式物联网工程师来说是一块真空领域. 的确,物联网研发应该以团队协作分工的方式进行,所以有嵌入式设备端.网关.web前端.APP.后端开发等专属岗位.作为系统架构师,自然需要掌握各种岗位的关键技术.作为嵌入式工程师,掌握网络编程.web编程,能够极大地拓展自己的视野和架构思维,能够主动地对系统的各种协议和应用场景提出优化的见解

python网络编程socket (一)

提起网络编程,不同于web编程,它主要是C/S架构,也就是服务器.客户端结构的.对于初学者而言,最需要理解的不是网络的概念,而是python对于网络编程都提供了些什么模块和功能.不同于计算机发展的初级阶段,程序员走到今天,已经脱离了手工打造一切,要自己实现所有细节的年代.现在提倡的是不要重复造轮子,而是学习别人的轮子怎么用,只有那些有需求或能专研的人才去设计轮子甚至汽车,so,这是一个速成的年代. 因此,对于一个面向工作的python程序员,学习python的网络编程,其实学的就是那么几个模块,

Python(七)Socket编程、IO多路复用、SocketServer

本章内容: Socket IO多路复用(select) SocketServer 模块(ThreadingTCPServer源码剖析) Socket socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. 功能: sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0) 参数一:地址簇 socket.AF_INET IPv4(默认)