而使用I/O多路复用时,处理多个连接只需要1个线程监控就绪状态,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
在本文中,我们先来谈谈select、poll、epoll多路复用中的select多路复用
在学习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 有个概念叫集合
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 是否有读事件需要处理
- 【如果现在还不是很理解的话,别担心,可以看完下面的,再将它们串到一起去理解,就好办很多了】
void FD_ZERO(fd_set *set);
FD_ZERO() 宏的作用就是将指定的文件描述符集(set 集合)清空【清空集合】
- 在对文件描述符集合进行设置前,必须对其进行初始化。因为不清空的话,由于在系统分配内存空间后,通常并不作清空处理,如果该集合是局部变量,则集合中会是随机值,所以结果是不可知的。
- 在对 set 集合进行更新的时候,也最好使用 FD_ZERO() 宏,将上一次因 select() 修改的集合清空,方便更新集合的内容
void FD_SET(int fd, fd_set *set);
FD_SET() 宏的作用就是将指定的文件描述符 fd 添加到 set 集合中去。
void FD_CLR(int fd, fd_set *set);
FD_CLR() 宏的作用就是将指定的文件描述符 fd 从 set 集合中删除。
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 是否有事件发生。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 定义socket可排队个数 */
#define BACKLOG 32
/* 此宏用来计算数组的元素个数 */
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
int socket_server_init(char *servip, int port);
int main(int argc, char **argv)
{
int listen_fd = -1;
int client_fd = -1;
int rv = -1;
int port;
int max_fd = -1;
fd_set rdset;
int fds_array[1024];
int i;
int found;
char buf[1024];
/* 用来确认程序执行的格式是否正确,不正确则退出并提醒用户 */
if (argc < 2)
{
printf("Program Usage: %s [Port]\n", argv[0]);
}
//将端口参数赋给参数变量
//由于命令行传参进来是字符串类型,所以需要atoi转换为整型
port = atoi(argv[1]);
/* 创建listen_fd,这里封装了一个函数 */
if ((listen_fd = socket_server_init(NULL, port)) < 0)
{
printf("socket_server_init failure\n");
return -1;
}
printf("listen listen_fd[%d] on port[%d]\n", listen_fd, port);
/* 将数组中所有元素设置为-1,表示为空 */
for (i=0; i<ARRAY_SIZE(fds_array); i++)
{
fds_array[i] = -1;
}
fds_array[0] = listen_fd;
while (1)
{
/* 将rdset集合的内容清空 */
FD_ZERO(&rdset);
max_fd = 0;
/* 把数组中的fd放入集合 */
for (i=0; i<ARRAY_SIZE(fds_array); i++)
{
if (fds_array[i] < 0)
continue;
FD_SET(fds_array[i], &rdset);
max_fd = fds_array[i]>max_fd ? fds_array[i] : max_fd;
}
/* 开始select */
if ((rv = select(max_fd+1, &rdset, NULL, NULL, NULL)) < 0)
{
printf("select() failure;%s\n", strerror(errno));
goto cleanup;
}
else if (rv == 0)
{
printf("select() timeout\n");
continue;
}
/* 有消息来了 */
/* 判断是不是listen_fd的消息 */
if (FD_ISSET(fds_array[0], &rdset))
{
/*
* accept()
* 接受来自客户端的连接请求
* 返回一个client_fd与客户通信
*/
if ((client_fd = accept(listen_fd, (struct sockaddr *)NULL, NULL)) < 0)
{
printf("accept new client failure: %s\n", strerror(errno));
continue;
}
/*
* 在把client_fd放到数组中的空位中
* (元素的值为-1的地方)
*/
found = 0;
for (i=0; i<ARRAY_SIZE(fds_array); i++)
{
if (fds_array[i] < 0)
{
fds_array[i] = client_fd;
found = 1;
break;
}
}
/*
* 如果没找到空位,表示数组满了
* 不接收这个新客户端,关掉client_fd
*/
if (!found)
{
printf("accept new client[%d], but full, so refuse\n", client_fd);
close(client_fd);
}
printf("accept new client[%d]\n", client_fd);
} /* end of server message */
else /* 来自已连接客户端的消息 */
{
for (i=0; i<ARRAY_SIZE(fds_array); i++)
{
/* 判断fd是否有效,并且查看当前fd是否在rdset集合中 */
if (fds_array[i] < 0 || !FD_ISSET(fds_array[i], &rdset))
continue;
/* 清空buf,以便存放读取的数据 */
memset(buf, 0, sizeof(buf));
if ((rv = read(fds_array[i], buf, sizeof(buf))) <= 0)
{
printf("read data from client[%d] failure or get disconnected, so close it\n", fds_array[i]);
close(fds_array[i]);
fds_array[i] = -1;
continue;
}
printf("read %d Bytes data from client[%d]: %s\n", rv, fds_array[i], buf);
/* 将小写字母转为大写 */
for (int j=0; j<rv; j++)
{
if (buf[j] >= 'a' && buf[j] <= 'z')
buf[j] = toupper(buf[j]);
}
/* 将数据发送到客户端 */
if ((rv = write(fds_array[i], buf, rv)) < 0)
{
printf("write data to client[%d] failure: %s\n", fds_array[i], strerror(errno));
close(fds_array[i]);
fds_array[i] = -1;
continue;
}
printf("write %d Bytes data to client[%d]: %s\n", rv, fds_array[i], buf);
} /* end of for(i=0; i
} /* end of client message */
} /* end of while(1) */
cleanup:
close(listen_fd);
return 0;
} /* end of main function */
/*
* Socket Server Init Function
* 创建 listen_fd,bind绑定ip和端口,并监听
*/
int socket_server_init(char *servip, int port)
{
int listen_fd = -1;
int rv = 0;
int on = 1;
struct sockaddr_in servaddr;
/*
* socket(),创建一个新的sockfd
* 指定协议族为IPv4
* socket类型为SOCK_STREAM(TCP)
*/
if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create listen_fd failure: %s\n", strerror(errno));
return -1;
}
//设置套接字端口可重用,修复了当Socket服务器重启时“地址已在使用(Address already in use)”的错误
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/*
* bind(),将服务器的协议地址绑定到listen_fd
*/
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
/* 若传的ip地址为空 */
if (!servip)
{
/* 监听所有网卡的ip */
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
/* 将点分十进制的ip地址转为32位整型传入结构体 */
if (inet_pton(AF_INET, servip, &servaddr.sin_addr) <= 0)
{
printf("inet_pton() failure: %s\n", strerror(errno));
rv = -2;
goto cleanup;
}
}
if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("bind listenfd[%d] on port[%d] failure: %s\n", listen_fd, port, strerror(errno));
rv = -3;
goto cleanup;
}
/*
* listen()
* 监听listen_fd的端口,并设置最大排队连接个数
*/
if (listen(listen_fd, BACKLOG) < 0)
{
printf("listen listen_fd[%d] on port[%d] failure: %s\n", listen_fd, port, strerror(errno));
rv = -4;
goto cleanup;
}
cleanup:
if (rv < 0)
close(listen_fd);
else
rv = listen_fd;
return rv;
}
这份代码的流程如下:
- 1、通过封装的函数 int socket_server_init(char *servip, int port); 去创建listen_fd,并且 bind() 绑定了协议族、IP和端口,最后去 listen() 开始监听这个端口,所有步骤都成功则返回 listen_fd,失败则返回出错,并关闭 listen_fd
- 2、将用来存放 fd 的数组 fds_array 内容全部置为 -1,表示该位置为空,可存放 fd
- 3、进入 while 循环
- 4、将集合清空,根据数组内容,将数组中有效的 fd 都放进集合中,更新集合内容,并判断其中的最大的 fd 是多少,方便内核扫描。
- 5、开始 select()
- 返回值 <0 出错,则关闭服务器
- 返回值 =0 超时,则结束本轮循环,进入下一轮循环【但由于代码中超时设置了NULL,所以不会发生超时这种情况】
- 返回值 >0 有事件发生,即可进行事件判断
- ⑥、因为按照功能区分,分为服务器端和客户端两大块,通过 FD_ISSET(fds_array[0], &rdset) 判断服务器端是否有数据可读
- 若是 listen_fd 有数据可读,说明有新客户端来连接服务器了
- 这时就可以进行 accept() 接收新客户端
- accept() 过后,还要判断数组中还有没有空位【值为-1的元素】可存放与新客户端通信的 client_fd
- 若有空位,则将 client_fd 存入数组中
- 若无空位,则将 client_fd 关闭,断开与该客户端的连接
- 若是 client_fd 有数据可读,说明有客户端发消息过来了
- 虽然说在 select() 后,已经将集合中有实际发生事件的 fd 保留了下来,但这时候我们还无法直接知道是哪几个,需要去遍历数组来和此时集合中的 fd 进行对比
- 使用 (fds_array[i] < 0 || !FD_ISSET(fds_array[i], &rdset)) 这两个条件去进行筛选,continue 跳过数组中 “值为 -1 和 不在集合中的fd ”,去处理在集合中的 fd 的数据
- 读数据,若读出错或连接断开,则将该 fd 移出数组【将数组中对应元素的值置为-1】,关闭与该客户端的连接
- 写数据,若写出错,则将该 fd 移出数组【将数组中对应元素的值置为-1】,关闭与该客户端的连接
以上是对Linux select多路复用的一些理解,如有写的不好的地方,还请各位大佬不吝赐教。