目录
一、典型IO模型
1.1 阻塞IO
1.2 非阻塞IO
1.3 信号驱动I0
1.4 IO多路转接
1.5 异步IO
多路转接的作用和意义
二、多路转接IO模型(select)
2.1 接口
2.2 接口当中的事件集合: fd_set
2.2 select使用事件集合(位图)的方式
2.2.1 接口
2.3 select使用方式:
2.4 select的返回值:(有点反人类)
2.5 select的测试代码:
三、 用select来实现单个线程接收多个客户端
一、典型IO模型
1.1 阻塞IO
在内核将数据准备好之前,系统调用会一直等待,直到数据的到来,拷贝完成数据后,IO调用才会返回
-
1.2 非阻塞IO
- 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO的返回,需要判断系统调用函数的返回值,来判断当前函数是否将IO功能完成。
- 1.没完成:一般会搭配循环继续调用(IO功能没有完成),但是这样对于CPU的资源也是巨大的浪费。
- 2.完成了:内核将数据准备好了,拷贝回来了(IO功能完成)
-
1.3 信号驱动I0
- (就像是鱼竿上面绑了个铃铛)
- 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行I0操作。这里例如我们对于僵尸进程(僵尸进程就是子进程先于父进程退出,子进程的退出信息没有人回收)的处理可以配合信号当子进程退出的时候我们自定义信号的处理方式。调用wait函数来回收子进程
-
1.4 IO多路转接
- 我们首先来看一个场景
上面那种无脑创建进程、线程的方法是不能处理大量客户端的情况的。那怎么来解决呢?
需要使用多路转接
多路转接IO模型可以帮我们同时监控多个文件描述符
有了多路转接,就可以不用无脑的创建线程或者进程了
就好比钓鱼的时候雇人去钓鱼,如果说哪个鱼竿上鱼了,让他别动,我们去享受把鱼拽上来的快感
-
1.5 异步IO
- 由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。好比说钓鱼,信号IO就是相当于鱼竿上面帮了一个铃铛,铃铛响了的时候通知你来将鱼竿收起然后钓鱼,而异步IO则是帮你钓鱼,就是帮你找了一个人当他调好了鱼之后来通知你取鱼
同步IO和异步IO的最大区别就是:
异步IO啥事也不管,只是告诉你我要做这件事情,接下来这件事情由你来做(你怎么多久是你的事情),做完你再告诉我
多路转接的作用和意义
1、作用:IO多路转接可以帮助我们同时等待多个文件描述符的就绪状态,多路转接IO模型可以帮助我们同时监控多个文件描述符
2、多路转接IO模型,就是程序高并发的基础
高并发: 程序可以处理大量的客户端请求
3、扩展:一个好的程序,需要考量高并发,也需要考量高可用
高可用:就说不管程序遇到什么糟糕场景,都是可以使用的,比如程序挂掉了(备份进程)(守护进程)
二、多路转接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 : 事件集合
- readfds : 读事件集合
- writefds: 写事件集合
- exceptfds:异常事件集合
timeout : 表示工作方式
- 阻塞方式 : NULL
- 非阻塞 : 0
- 带有超时时间的监控:传递struct timeval 这样的结构体对象
- 返回值:
- 成功:返回就绪文件描述符的个数
- 失败:返回-1,并且设置了errno,程序员就可以通过perror这个函数查看失败原因
2.2 接口当中的事件集合: fd_set
第一印象:
fd_set : 是一个结构体,结构体当中有一个数组,名字为fds_bits
探究:
- 1. 数据的元素类型是什么?
- 2.数据的元素个数为多少?
我们可以发现,数组的大小只有16个,这就很奇怪了,如果一个元素存放一个文件描述符数值,那最多也才16个,这肯定是不对的,太少了,一个进程随随便便就会有很多个文件描述符。
那到底是什么回事呢?
事件集合的使用方式并不是按照数组的方式来使用的,而是按照比特位(位图)的方式使用的
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.就绪的文件描述符存储在事件集合当中返回给调用者
但是:没有就绪的文件描述符呢?也放在事件集合当中吗?
肯定不会,因为select监控成功完事件集合之后,还是要判断哪些文件描述符就绪了,为了能够判断出来,就需要将未就绪的除掉
所以,如果select监控成功之后,再次需要监控时,需要将上一次未就绪的文件描述符给他放到事件集合当中去
-
2.5 select的测试代码:
代码:
之前是阻塞在scanf的等待读过程,我们看看用到了select进行监控,这个进程阻塞在哪里:
-
三、 用select来实现单个线程接收多个客户端
- 我们知道如果没有多路转接io的话那么就要用多线程来处理接收多个客户端连接服务端。那么这里我们就可以实现用一个线程来实现接收多个客户端的连接。
改造前:
accept 放到while 循环的外面,这个进程一辈子只能接收一个客户端的连接
accept 放到while 循环的外面,这个进程一辈子只能和客户端联系一次(但是可以和多个客户端发送数据)
改造后:
思路: select 帮助我们监控listen_sockfd(一个),new_sockfd(多个)
当select监控成功之后,我们针对不同的文件描述符做出不同的操作
listen_sockfd (读事件) :accept
new_sockfd (读事件):recv
服务端:
客服端:
运行一下观察结果:
可以看到,一个线程就能处理多态客户端,属实是神奇!!!
四、select的优缺点
4.1优点:
4.2缺点