文章目录
前言
学习完多进程、多线程,相比较之下,当有多个并发连接请求时,多进程或多线程模型需要为每个连接创建一个进程或者线程,而这些进程或线程中大部分是被阻塞起来的。如果这时候服务器端要连接1000个客户端,则需要创建1000个进程或线程,且不说在进程、线程之间切换所消耗的时间片,单单是每个进程、线程之间的创建,就已经耗费了大量的资源。而使用I/O多路复用时,处理多个连接只需要1个线程监控就绪状态,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
在本文中,我们先来谈谈select、poll、epoll多路复用中的select多路复用
一、同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)
在学习select多路复用之前,我们先来了解几个概念
在Linux下进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock) 四种调用方式:
- 同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
- 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作在没有接收完数据或者没有得到结果之前不会返回,需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
简单来讲就是:对于一个事件,
同步知道这个事件什么时候会发生
异步不知道这个时间什么时候会发生
对于一个消息
阻塞:只要有消息来,会一直通知你,有消息没处理
非阻塞:有消息来了,只通知你一次,错过了这次通知,后面就需要自己去轮询了
在Linux下进行网络编程时,服务器端编程经常需要构造高性能的IO模型,常见的IO模型有五种:
- 同步阻塞IO(Blocking IO)
- 同步非阻塞IO(Non-blocking IO)
- IO多路复用(IO Multiplexing)
- 信号驱动IO(signal driven IO)
- 异步IO(Asynchronnous IO)
二、select 多路复用
select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
1、select() 函数
select 有个概念叫集合
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select() 函数的作用是监视三组独立【三个集合】的文件描述符,将相应集合的 fd 修改成实际发生了相应事件的 fd 保留在集合中,并返回有多少个 fd 有事件发生。解析如下:
- 参数1(int nfds): 待测试的 fd 的总个数,它的值是待测试的最大文件描述符+1 因为在select() 函数中,内核是从0开始扫描文件描述符的,假如现在需要监测的文件描述符是3、6、9,那么此时最大的文件描述符是9,那么内核就会从0扫描到9,共扫描10个文件描述符,即是max(3,6,9)+1=10
- 参数2(fd_set *readfds): 指定要让内核测试读条件的 fd 集合,如果不需要或不关心此类事件的话,可设置为NULL
- 参数3(fd_set *writefds): 指定要让内核测试写条件的 fd 集合,如果不需要或不关心此类事件的话,可设置为NULL
- 参数4(fd_set *exceptfds): 指定要让内核测试异常条件的 fd 集合,如果不需要或不关心此类事件的话,可设置为NULL
- 参数5(struct timeval *timeout): 设置select的超时时间,如果设置为NULL则永不超时
- 返回值:select() 函数执行后,返回值是一个int类型的整数,这个整数代表着此时集合中有多少个 fd 有事件来【缓存不为空】,比如:当时只有 6 和 9 这两个 fd 有数据可读,那么 select() 返回的值就是 2
- 注意:select() 函数在执行的时候,会修改原本集合中的内容【fd】,把当前没有数据可读的 fd 移出集合,只保留当前有数据可读的 fd
- 比如:要监测 5,6,7,8,9 这几个文件描述符的读事件,第一件事就是要把它们通过 FD_SET() 添加到 rdset 集合中去,现在 rdset 这个集合里包含了 5,6,7,8,9 这5个 fd,调用 select() 函数,发现只有 6 和 9 这两个 fd 有读事件,那么该函数就会将 rdset 集合中当前没有读事件的fd(5,7,8)移出集合,保留 6,9 这两个 fd 在 rdset 集合中,方便后面在内核扫描 fd 的时候,通过 FD_ISSET() 来判断该 fd 是否有读事件需要处理
- 【如果现在还不是很理解的话,别担心,可以看完下面的,再将它们串到一起去理解,就好办很多了】
2、FD_ZERO() 宏
void FD_ZERO(fd_set *set);
FD_ZERO() 宏的作用就是将指定的文件描述符集(set 集合)清空【清空集合】
- 在对文件描述符集合进行设置前,必须对其进行初始化。因为不清空的话,由于在系统分配内存空间后,通常并不作清空处理,如果该集合是局部变量,则集合中会是随机值,所以结果是不可知的。
- 在对 set 集合进行更新的时候,也最好使用 FD_ZERO() 宏,将上一次因 select() 修改的集合清空,方便更新集合的内容
3、FD_SET() 宏
void FD_SET(int fd, fd_set *set);
FD_SET() 宏的作用就是将指定的文件描述符 fd 添加到 set 集合中去。
4、FD_CLR() 宏
void FD_CLR(int fd, fd_set *set);
FD_CLR() 宏的作用就是将指定的文件描述符 fd 从 set 集合中删除。
5、FD_ISSET() 宏
int FD_ISSET(int fd, fd_set *set);
FD_ISSET() 宏的作用就是判断指定描述符 fd 是否在 set 集合中
- 一般 FD_ISSET() 会在 select() 后面搭配使用,因为调用了 select() 之后,会将相应集合中有实际事件发生的 fd 保留下来,其余的移出 set 集合,此时 set 集合里存放的都是实际有事件发生的,所以只需要在 select() 之后使用 FD_ISSET() 来判断该 fd 在不在 set 集合中,即可知道该 fd 是否有事件发生。
三、具体代码以及程序浅析
1、具体代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <ctype.h>
/* 定义socket可排队个数 */
#define BACKLOG 32
/* 此宏用来计算数组的元素个数 */