文章目录
1、I/O复用
1.1为何要引出I/O复用
在我们之前的一个关于TCP的博文中,我们有详细的介绍TCP服务器的编程流程,简单的,我们可以描述成以下的方式。
从上图的编程流程,可以总结出,里面红色的循环表示完成多次数据的收发,外面蓝色的循环代表着循环接受客户端的链接。但是,这样的编程流程似乎还是存在着些许的问题。
1、如果我们多个客户端连接,A建立连接后,B再次连接时,程序无法处理,因为只有一个进程。这样是串行处理,A处理完才可以处理B。这样我们就无法并发处理多个客户端的请求,效率低下。 就相当于我们不能一次接听两个人的电话,当有两个电话同时来了,你只能选择一个接听处理,处理完这个再去处理另一个。
2、客户端程序一直占用服务器,并没有实际的数据交互。相当于你接听了A的电话,但它一直不说话,你还必须等着。这就导致服务器一直被无用客户端占用,没有实际的数据交互,浪费时间,导致其他有业务逻辑额客户端一直等待
1.2I/O复用的概念
基于上述TCP服务器编程的缺陷,现在我们希望可以同时接听多个电话,哪个电话说话了,你再进行交互。
于是就引入I/O复用,它可以同时监听多个客户端描述符
。我们可以给定一种集合,,把listenfd、Afd、Bfd、Cfd存放在这个集合中,对集合中的所有文件描述进行统一进行监听,监听其是否有事件发生(读事件、写事件)。
这样就可以避免一个客户端一直占用,其他客户端得不到连接的情况。在下列情况下需要使用I/O复用技术。
- TCP服务器同时要处理监听套接字和连接套接字
- 服务器要同时处理TCP请求和UDP请求
- 程序要同时处理多个套接字
- 客户端程序要同时处理用户输入和网络连接
- 服务器要同时监听多个端口
注意!!
I/O复用虽然能同时监听多个文件描述符,但是它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序处理其中的每一个文件描述符。这使得服务器看起来好像是串行的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。
如取快递,所有物品在这放着监视着,当有人来取给他取,取的人多了就要排队。
【补充】
——并发和并行
- 并行:两个进程一起运行。并行需要硬件支持,多个CPU。
- 并发:两个进程根据时间片轮询运行,因为时间短,故给人的感觉就是一起运行。并发效率慢,但一个CPU即可
2、I/O复用技术之select
2.1select函数的功能和作用
下图是select函数完成I/O复用的描述图:
1、函数原型
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int select(int maxfd, fd_set readfds,fd_setwritefds, fd_set*exceptfds, struct timeval *timeout)
2、参数详解
1、maxfd
设置为所有监听的文件描述符的最大值+1。其目的是提高select底层的执行效率。
但是为何这样设置会提高低沉的执行效率。我们就要先来了解一下select底层是怎样把文件描述符(整型值)放入readfds等集合中去的呢?
- fd_set类型的结构简单描述如下:
typedef struct
{
long fds_bits[32];
}fd_set
- 如果用int/long来保存一个文件描述符,那么这个结构最多可以放8个,但int/long保存一位数据过于浪费,所以它进行了很巧妙的存储设计
fd_set类型的结构
是一个4字节的数组,数组大小为32位,换算成位就是:4832=1024位所以一个select最多存储1024位描述符,最大的文件描述符为1023。使用每一个比特位记录一个文件描述符,文件描述符的值在位移上表现.
例如,当fd = 4时,存储结构如下。
因为一般文件描述符都是1,2,3,4,5这样比较小的数字。所以文件描述符的在集合fdset里面的占位也不会全部占满1024位。所以设置所有监听的文件描述符的最大值+1可以大大的提高底层的执行效率。其底层的具体实现如下图所示:
2、三个参数的作用
readfds、writefds和exceptfds参数分别记录可读、可写、异常事件。
- 在select调用时,将用户关注的可读、可写、异常事件的文件描述符传递给内核。
比如说:如果用户都关注的是读事件,就将这几个文件描述符都设置到readfds里面去,因为其就代表的是读事件的文件描述符集合 - 在select返回时,内核通过在线修改的方式修改这三个变量,给用户空间传递有时间发生的文件描述符集合。将内核检测到的,有事件发生的,用这三个变量带出来
3.每次调用select时,都必须重新设置这三个fd_set类型的变量
3、timeout
指定的一个时间,定时时间。Select是在一定的时间内轮询检测描述符,如果超出此时间,select会返回0,表示时间到了还未检测到描述符就绪。
- 如果指定为NULL,表示永久阻塞。
- 也可以按照struct timeval time = {5, 0}的方式指定。其结构体表示如下
struct timeval
{
long tv_sec //秒数
long tv_usec;//微妙数
};
4、返回值
只是返回了就绪文件描述符的个数。
- 大于0返回有几个文件描述符有时间发生(就绪)
- ==0 超时时间到达,就没有就绪的文件描述符
- == -1select调用出错
并没有具体指明哪几个文件描述符就绪。所以要用一定的方法来了解具体的哪几个就绪。具体要用到的对fd_set结构体操作的函数如下图所示:
FD_ZERO ( fd_set *set )//清空fdset的所有位
FD_SET ( int fd,fd_set *set )//设置fdset的所有位
FD_CLR ( int fd,fd_set *set);//清除fdset的位fd
int FD_ISSET( int fd,fd_set *set );//测试fdset的位fd是否被设置
就是当select调动的时候,这个结构体里面是用户填充的内容,返回时是内核修改的内容。如果用户给fd_set传了10个事件描述符,那么返回时可能fd_set只有3个,所以我们在select返回时,不仅要看select的值,我们还要用函数Int FD_ISSET(int fd,fd_set*fdset);来检测结构体中的描述符是否被修改即是否有事件发生,所以传入地址,可以让内核进行修改。在线修改示意图:
2.2I/O复用select的特点
1、优点
- select是最常见的IO模型,可以对多个客户端进行监听处理。
- 解决了TCP只能串行处理客户端的问题,实现了并发处理。
2、缺点
- select能监听的文件描述符个数受限于FD_SET结构体,一般为1024,解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率。
- 每次调用select,需要将fd描述符集合从用户态拷贝到内核态,开销很大
- fd_set结构体用户态保存所有文件描述符,调用select后,返回的是就绪文件描述符。即一个结构体保存了两种状态的文件描述符,故每一次调用select之前都要重新初始化fd_set。
2.3代码实现I/O复用select
在代码实现之前,我们来理清楚一下逻辑,我们在此实现的代码只捕捉可读事件,用数组Filefd来保存用户的描述符。每次select之前都应该重新初始化fd_set readfds的值。
来了一个描述符我们就将其添加到数组中,一个链接数据发送完成需要删除这个文件描述符,所以需要数组初始化,添加,删除函数。
我们把处理事件的函数单独实现,收到客户端发来的数据,服务端回复个ok。具体的流程图如下: