目录
-
1. 五种IO模型
-
1.1 阻塞IO
- 在内核将数据准备好之前,系统调用会一直等待.所有的套接字,默认都是阻塞方式。
-
1.2 非阻塞IO
- 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO的返回,需要判断系统调用函数的返回值,来判断当前函数是否将IO功能完成。
- 1.没完成:一般会搭配循环继续调用(IO功能没有完成),但是这样对于CPU的资源也是巨大的浪费。
- 2.完成了:内核将数据准备好了,拷贝回来了(IO功能完成)
-
1.3 信号驱动I0
- 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行I0操作。这里例如我们对于僵尸进程(僵尸进程就是子进程先于父进程退出,子进程的退出信息没有人回收)的处理可以配合信号当子进程退出的时候我们自定义信号的处理方式。调用wait函数来回收子进程。
-
1.4 异步IO
- 由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。好比说钓鱼,信号IO就是相当于鱼竿上面帮了一个铃铛,铃铛响了的时候通知你来将鱼竿收起然后钓鱼,而异步IO则是帮你钓鱼,就是帮你找了一个人当他调好了鱼之后来通知你取鱼。
-
1.5 IO多路转接
- 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
- 多路多路转接IO可以帮我们同时监控多个事件。
- 总结:
- 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
-
2. 多路转接IO(select)
-
2.1 接口
- int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- nfds :取值为最大文件描述符的数值+1,作用是:控制select的轮询监控范围。也收到事件集合fd_set的限制。这里fd_set事件集合就是一个数组,这个数组的大小为16个long int而linux下long int大小为8个字节,这里数组当作位图使用那么就会有16*8*8个bit位,那么这里select最大的监控文件描述符的个数为1024。而且这个位图是从0开始到1023。
- 这里有一个问题:那么一个程序能不能创建1024个文件描述符呢?这要看系统的限制我们输入ulimit -a可以查看到系统最大的open files为100001个文件描述符。
- 对于nfds的总结:
- 1.事件集合在内核当中是以数组定义的,但是使用方式是位图
- 2.位图的大小取决于内核宏二_FD_SETSIZE。
- 3.目前的位图的大小为1024比特位,所以select只能监控0~1023号文件描述符
- readfds :读事件集合!
- writefds : 写事件集合√execptfds :异常事件集合、
- timeout:
- 传递NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
- 传递0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 传递特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回,秒/微秒NULL。
- 返回值:
- 成功:返回就绪文件描述符的个数
- 失败:返回-1.
-
2.2 select使用事件集合方式
-
2.2.1 接口:
- void FD_CLR(int fd,fd_set *set) ;
- 作用:将fd从事件集合set当中去除掉,本质就是将fd对应的比特位置为0
- int FD_ISSET(int fd,fd_set *set) ;
- 作用:判断fd文件描述符,是否在set集合当中,本质是判断fd对应的比特位是否为0
- 返回值:
- 0:表示fd不在set当中
- 1:表示fd在set当中
- void FD_SET(int fd,fd_set *set) ;
- 作用:设置fd文件描述符到set事件集合当中,本质是将fd对应的比特位设置为1
- void FD_ZERO(fd_set *set) ;
- 作用:清空事件集合.本质是将set当中所有的比特位都置为0
-
2.3 select使用方式:
- 1..select共有三个事件集合:读事件集合,写事件集合,异常事件集合。
- 2.当需要关注某个文件描述符的某个事件,则将来个文件描述符添加到对应的事件集合当中。例如:关注(0号文件描述符的读事件,则将0号文件描述符添加到读事件集合当中readfds。
- 3.如果不关注某种事件,则给select传递参数的时候,传递NULL。
-
2.4 select的返回值:
- 1.返回值为就绪的文件描述符的个数
- 2.就绪的文件描述符存储在事件集合当中返回给调用者
- 注意: iselect会将未就绪的文件描述符从事件集合当中去除掉,因此,再次监控的时候需要重新添加,这里的意思就是说:当我们用select监控多个文件描述符的时候那么此时select就会扫描一遍事件集合的位图,如果我们这里监控三个文件描述符,而此时就绪的文件描述符只有一个,那么select扫描当前事件集合之后会将那两个未就绪的文件描述符从事件集合中抹去,也就是将相应的bit位,置为0。所以我们下次还要监控这三个文件描述符的时候就要将事件集合当中这三个文件描述符对应的bit位都置为1.这里我觉得是因为当select监控完事件集合当中还要检测那些事件就绪了,所以这里才要将没有就绪的事件从事件集合中移除。
- 3.返回值的事件集合的特性一定要注意,因为会去除未就绪的文件描述符!! !
-
2.5 select编程示例:
- 这里们在读事件集合中加入0号文件描述符和我们创建的侦听套接字描述符。让select帮我们监控就绪事件,但是我们对于侦听套接字不做处理,然后用telnet连接我们的测试demo。看看会发生什么。
- 用telnet连接
- 我看运行结果:
- 可以看到疯狂的打印listen_sockfd already,这是因为监听套接字监听到端口连接之后那么就相当于等待的资源已经到来,那么select就会通知处理,但是我们并没有对于就绪事件进行处理,那么这里就会疯狂的通知。而这里打印出来的fd 0 not ready不是因为0号文件描述符没有就绪,而是第一次select监控之后就将其从就绪事件集合中移除。所以除了第一次的打印是因为0号文件描述符的就绪事件没有就绪,其他都是因为已经从就绪文件集合中去除。
- 这里我们测试一下select会不会将未就绪的文件描述符从事件集合中去除。
- 我们先让程序跑起来,然后输入一个n让0号文件描述符就绪。然后我们在用telnet连接当前的测试demo
- 可以看到毫无反应,所以我们得出结论,那就是在select监控的时候返回时会将未就绪事件从事件集合中去除
- 那么我们要想每次都监控这两个文件描述符的话就需要每次当select返回的时候事件集合中都存在这两个文件描述符。
- 来看运行结果,没有出现对于telnet连接无响应,也没有疯狂打印。
-
3. 用select来实现单个线程接收多个客户端
- 我们知道如果没有多路转接io的话那么就要用多线程来处理接收多个客户端连接服务端。那么这里我们就可以实现用一个线程来实现接收多个客户端的连接。
- 这里我们来实现一下:首先我们将select封装为一个类方便我们操作。
- 然后写一个服务端程序:
- 我们来用客户端来连接
- 这里来连接两个客户端可以发现疯狂打印,而且数据都黏在了一起,这就是TCP粘包,因为数据都发送过来存储到TCP缓冲区中,我们一次性读取1023个字节那么势必会发生TCP粘包问题。