什么是IO多路复用?
一种网络通信的手段,IO多路复用可以同时监测多个文件描述符,且这个过程是阻塞的,当检测有文件描述符就绪,程序的阻塞就会解除,就可以通过这些就绪的文件描述符进行通信。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:select、poll、epoll。
select:
该函数是跨平台的,检测文件描述符的读写状态
这三个指针是指向三个文件描述符集合的指针。
参数介绍:
- nfds:是在这三个文件描述符集合中找出一个最大的文件描述符再+1
- 内核中需要线性遍历这些集合中的文件描述符,该值是循环结束的条件
- 该参数在windows中无效,指定为-1即可
- readfds:读集合一般都是需要进行检测的,这样才能知道哪些文件描述符可以接收数据
- writefds:内核只检测这个集合中的文件描述符对应的写缓冲区,如果不使用该参数可以用NULL指定
- exceptfds:内核只检测这个集合中的文件描述符是否有异常状态,如果不使用该参数可以用NULL指定
- timeout:是表示检测文件描述符的时间,超过该时间函数返回。超长时长,用来解除select函数的阻塞
- 等待固定时长:如果函数检测不到就绪的文件描述符,在指定时长过后就会解除堵塞
- 如果设定为NULL:检测不到就绪的文件描述符就会一直阻塞
- 不等待时长:检测不到就绪的文件描述符就会直接返回,不会阻塞
select函数返回值:
- >0:返回集合中就绪的文件描述符的个数
- =0:超时,没有检测到就绪的文件描述符
- <1:函数调用失败
fd_set类型:
fd_set类型的数据拥有1024个字节,也就是128个比特位,这个可以把他想象成一个整型的数组,大小为32。 fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。
- 如果集合中的标志位为0表示不检测这个文件描述符状态
- 如果集合中的标志位为1表示检测这个文件描述符状态
内核如何更新集合中的标志位
- 假设需要检测读集合是否有文件描述符触发
- 传入内核中的集合在此之前做初始化之后再传入
- 内存中的数据拷贝到内核中
- 内核检测拷贝的数据,检测完后,把满足条件的文件描述符重新写入到该内存中(可以理解为内核需要更新fd_set中的数据)
- 把文件描述符中fd3,fd5,fd6,fd8,fd9,fd10,fd11设置到fd_set文件描述符集合中
- 再通过select函数将文件描述符集合传递给内核
- 内核把传入的集合拷贝一份(数据拷贝的过程)
- 内核基于拷贝出来的集合做线性检测
- 内核通过拷贝的集合与文件描述符表做对比
- 文件描述符表的每个文件描述符具有两个缓冲区:读和写的缓冲区
- 检测读缓冲区是否有数据
- 内核基于拷贝的该线性表把需要检测的文件描述符的读缓冲区检验一遍
- 把满足条件的文件描述符再重新写入指定的内存中
写集合也是一样的道理
以上函数能够实现把文件描述符存储到fd_set这个集合中/把已经存储的文件描述符从集合中删除等对该表的一系列增删查改
处理并发问题
处理流程的步骤:
- 创建套接字
- 绑定本地IP端口
- 建立监听
- 对需要检测的文件描述符做初始化(前三步完成后拥有一个监听的文件描述符)
- 监听的文件描述符用于检测是否有客户端连接
- 若客户端有连接请求,文件描述符会把发送进来存储到用于监听的文件描述符的读缓冲区中
- 创建一个fd_set的读集合并初始化(把标记为初始化为0)
- 再通过fd_set把监听的文件描述符对应的标志位设置为1
- 调用select进行检测
- select调用一次只检测一次
- 需要多次检测需要写入到循环中
- 返回值为-1或者0,即可做异常处理
- >0:文件描述符如果是监听的描述符,大于0表示有新的客户端连接到达
- 调用accept与客户端建立连接(不会阻塞,内核已检测完并知道文件描述符中是有数据的)得到通信的文件描述符
- 把得到的文件描述符放入读集合中,如果不进行下一轮的检测,服务端则不知道客户端有新的数据到达
- 检测到通信的文件描述符,调用recv接收数据
- 返回值:
- <0:客户端断开连接,调用close关闭通信的套接字,紧接着调用fd_clr函数删除
- >0:接收到数据,调用send函数发送数据