网络编程之三种I/O复用方式

TCP服务器在与客户端完成建立连接,并在完成整个交互过程(完成与服务器多次的收发数据)之后再断开连接,所以在web服务器的代码中,在收发数据时(recv和send上)再加上一个while循环,用于解决同一个客户端的多次收发数据请求。但当多个客户端同时向服务器发出请求时,当前的代码模式依旧无法满足要求,所以引入了I/O复用,可以使程序同时监听多个文件描述符。

一、引入I/O复用的原因

1.TCP服务器同时要处理监听套接字和链接套接字;
2.服务器要同时处理TCP请求和UDP请求等多个请求;
3.程序要同时处理多个套接字;
4.客户端程序要同时处理用户输入和网络连接;
5.服务器要同时接听多个端口。

二、I/O复用需要注意的点

1.I/O复用虽然可以同时监听多个文件描述符,但是其本质是阻塞的。
2.当多个文件描述符同时就绪时,若不采取额外措施,程序就只能按照顺序依次处理其中的每一个文件描述符,这使服务器看起来像是串行工作的;
3.若想提高并发处理的能力,可以配合使用多线程或多进程。

三、select—#include<sys/select.h>

1.fd_set结构

#define  __FD_SESIZE 1024//
typedef  long  int  _fd_mask;//
#define  _NFDBITS  (8*(int)sizeof(_fd_mask))//32
typedef  struct
{
    
    
	#ifdef _USE_XOPEN//
	_fd_mask  fds_bits[FD_SESIZE /_NFDBITS];//32的数组
	#define  FDS_BITS(set)((set)->fds_bits)//
	#else//
	_fd_mask  _fds_bits[FD_SESIZE /_NFDBITS];//
	#define  FDS_BITS(set)((set)->fds_bits)//
	#endif
}fd_set;//

包含了32个元素(1024位)的long int数组;用于记录文件描述符,一个比特位记录一个文件描述符,文件描述符的值在位移上呈现
2.函数原型:

int select(int maxfd,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

(1)maxfd:指定被监听的文件描述符的总数,一般指定为监听队列中文件描述符的最大值+1。为了提高select底层的执行效率,这样可以使得内核不需要每次都在fd_set中全部扫描一遍。
(2)readfds:指向可读事件对应的文件描述符集合,
(3)writefds:指向可写事件对应的文件描述符集合
(4)exceptfds:指向异常事件对应的文件描述符集合
(5)timeout:在指定时间内进行轮巡,查看新的文件描述符;指定为NULL时代表永久阻塞。即一直等待,不进行轮巡。
3.方法解析:
(1)当同一事件可读也可写时,则在readfds和writefds两个集合中都要添加;
(2)在线修改,其返回值确定有多少个就绪的文件描述符;
(3)每次调用select之前,都必须重新设置各个结构体变量,然后在内核中将新的结构体拷贝过来
4.通过以下宏可以访问fd_set结构中的位

FD_ZERO(fd_set  *fdset)//清除fdset中的所有位
FD_SET(int  fd, fd_set  *fdset)//设置fdset中的位fd
FD_CLR(int  fd, fd_set  *fdset)//清除fdset的位fd
int  FD_ISSET(int  fd, fd_set  *fdset)//测试fdset的位fd是否被设置

5.select方法返回就绪个数,但是并不返回哪个就绪,所以在用户执行时还是需要在进行一次查询,以查询哪个文件描述符就绪;

四、poll—#include<poll.h>

1.函数原型

int  poll(struct  pollfd  fds[], unsigned int nfds, long  timeout);//成功返回就绪文件描述符的总数,超时返回0,失败返-1

(1)struct pollfd fds[]:结构体数组,用于指定文件描述符
(2)unsigned int nfds: 数组长度
(3)long timeout:设置的接收时间,单位为毫秒,-1则代表不轮巡永久阻塞
2.struct pollfd的结构

{
    
    
	int  fd;//用户设置关注的描述符
	short  events;//指定用户关注的事件类型
	short  revents;//由内核填充就绪的事件类型
};

3.工作流程
(1)由用户定义pollfd数组,调用poll时传入;
(2)若有事件发生,则由内核对revents填充,然后需要进行轮巡查看。
4.poll与select对比
(1)同是返回就绪的文件描述符总数,都需要再进行一次轮巡,其时间复杂度为O(n);
(2)将用户关注的事件类型和就绪事件类型分开,这样在调用poll时就不需要重新填充事件类型了,因为events是不变的,只有用户去改;
(3)用short类型记录类型,就可以记录很多的事件类型;
(4)用户数组是由用户自己定义,增加了自由性。

五、epoll—#include<sys/epoll.h>

1.是Linux系统上专有的一种I/O复用机制,与select和poll方法也有较大差异,epoll是一组方法,由内核直接维护,在内核态存在,而上述两种方法是在用户态存在,每调用一次则需拷贝两次数据,一次在调用时,一次在返回时,所以其效率并不是很高。
2.select方法的回调机制:在每个文件描述符就绪时会绑定一个回调函数,在事件状态改变时回调函数会触发并通知内核,所以回调机制适用于关注的事件多,但触发的概率小的情况,反之轮巡机制适合关注的事件少但触发的概率高的类型
3.struct epoll_event结构

struct epoll_event
{
    
    
	short events;//事件类型,支持poll的事件类型,名称前加"E"即可。
	union epoll_data_t  data;//一般用以记录文件描述符
};
//事件类型中有两个额外的事件:EPOLLLET和EPOLLONESHOT

4.union联合体结构

typedef  union  epoll_data//
{
    
    
	void  *ptr;//
	int  fd;//指定文件描述符
	uint32_t  u32;//
	uint64_t  u64;//
}epoll_data_t;//

5.epoll_create();

int  epoll_create(int size);

创建内核事件表(用户关注的所有文件描述符以及关注的事件类型),成功返回内核事件表的标识符,失败返回-1
(1)size:指定内核事件表的大小,个数,由于底层使用红黑树,这个参数已经不重要了
6.epoll_ctl();

int  epoll_ctl(int  epfd, int  fd, struct  epoll_event  *events);

管理内核事件表,增删改
(1)epfd:创建的内核事件表的文件描述符
(2)op:指定操作方式
①EPOLL_CTL_ADD:添加
②EPOLL_CTL_MOD:修改
③EPOLL_CTL_DEL:删除
(3)fd:要在内核事件表中操作的文件描述符
(4)event:指定事件(需要在用户态指定,只有在添加的时候会拷贝一次,所以效率较高)
7.epoll_wait();

int  epoll_wait(int  epfd, struct  epoll_event  events[], int maxevents, int  timeout);

(1)epfd:创建的内核事件表的文件描述符
(2)events:是一个用户数组,在调用时由内核在返回时自动填充有事件就绪的文件描述符和就绪的事件类型。(因此用户程序检索就绪事件的时间复杂度为O(1))
(3)maxevents:数组的长度,指定了一次epoll_wait最多返回的就绪事件数量
(4)timeout:轮巡时间,以毫秒为单位,-1为NULL。
(5)函数调用成功返回就绪个数,失败返回-1,超时返回0;
8.epoll对文件描述符的两种工作模式
(1)LT:是默认的工作模式,此模式下epoll相当于一个效率较高的poll,LT模式下事件通知后可以不立即处理,但下次epoll_wait将会继续通知此事件,直至被处理;
(2)ET:当往epoll内核事件中注册一个EPOLLET事件时,epoll将以ET模式工作,ET模式是epoll的高效工作模式,epoll_wait报告一个事件后,内核将立即处理此事件,因为后续epoll_wait将不再报告此事件;
①EPOLLONESHORT:即使使用ET模式,事件还是可能触发多次,当一个进程读取完数据后,又发来新的数据时,就有可能使一个socket被两个进程同时处理,使用EPOLLONESHORT则可以使事件被一个进程触发一次,一般使用在多进程中。

后续会对epoll和poll的源码进行分析。

猜你喜欢

转载自blog.csdn.net/qq_45132647/article/details/107741585