目录
I/O复用技术
select函数
设置文件描述符
指定监视范围
设置超时
I/O复用服务器端的实现
由服务器创建多个进程来实现并发的做法有时会带来一些问题,比如:内存上的开销、CPU的大量占用等,这些因素会消耗掉服务器端有限的计算资源、进而影响程序之间的执行效率。那么,有没有方法可以在不创建额外进程的条件下实现并发呢?当然有,那就是I/O复用技术。
I/O复用技术
I/O复用指的是通过单个线程记录一个或多个I/O流的状态,并对不同状态下的I/O流进行协调,使进程不阻塞于某个特定的I/O调用过程中,从而将有限资源最大化利用。
引用自一段情景材料,方便大家能更好地理解这个技术背后的思想:
假设你是一个机场的空管,你需要管理到你机场的所有的航线,包括进港,出港,有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。
你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机,从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
- 很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
- 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后,基本上就成菜市场了。
- 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。
现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?他们用这个东西:
这个东西叫 flight progress strip,每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。这个东西现在还没有淘汰哦,只是变成电子的了而已。是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个 Sock(I/O 流),空管当成你的服务端 Sock 管理代码的话:
- 第一种方法就是最传统的多进程并发模型:每进来一个新的 I/O 流会分配一个新的进程管理。
- 第二种方法就是 I/O 多路复用:单个线程,通过记录跟踪每个 I/O 流(sock)的状态,来同时管理多个 I/O 流 。
参考资料:IO 多路复用是什么意思? - 罗志宇
select函数
select是I/O复用技术中比较经典且常用到的一个函数(还有poll、epoll),在使用上,需要引入<sys/select.h>和<sys/time.h>两个头文件。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd , fd_set * readset , fd_set * writeset , fd_set * exceptset , const struct timeval * timeout);
//成功时返回大于0的值,失败时返回-1。其余情况为返回发生事件(监视项)的文件描述符数量
/* 监视项 */
// 1.接收数据的套接字
// 2.传输数据(无阻塞)的套接字
// 3.发生异常的套接字
/* 参数含义 */
// maxfd: 监视对象文件描述符数量
// readset: 将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// writeset: 将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// exceptset: 将所有关注"是否发生异常"的文件描述符注册至fd_set型变量,并传递其地址值。
// timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递超时(time-out)消息。
使用select函数时,一般遵循以下流程步骤:
设置文件描述符
在调用select函数之前,我们需要声明监视事件,并用数据类型为fd_set的变量来记录监视事件下的每个文件描述符的状态。如图所示,fd_set以数组的形式记录每个文件描述符的状态:
以图中fd0、fd2为例,值为0,代表着所指向的文件描述符不是被监视对象;反之,fd1和fd3值为1,则是被监视对象。
在对fd_set数组操作时,有些宏可以帮助我们简化代码的编写工作,下列所示为与对fd_set操作相关的宏:
- FD_ZERO(fd_set * fdset): 将fd_set变量的所有位初始化为0 。
- FD_SET(int fd , fd_set * fdset): 在参数fdset指向的变量中注册文件描述符fd的信息。
- FD_CLR(int fd , fd_set * fdset): 从参数fdset指向的变量中清除文件描述符fd的信息。
- FD_ISSET(int fd , fd_set * fdset): 若参数fdset指向的变量中包含文件描述符fd的信息,则返回"真",用于对select调用结果的验证。
宏对应的操作含义如图所示:
指定监视范围
即设置select函数中的第一个参数 maxfd,其值用来标记限制对监视事件中的文件描述符最大监视数。
设置超时
当监视的文件描述符未发生变化时,select函数会导致进程发生阻塞。为了避免这种情况发生,我们可以通过设置超时(select函数中的最后一个参数timeout)来防止这种情况的发生。
timeout的结构体定义如下:
struct timeval
{
long tv_sec; // 秒
long tv_usec; // 毫秒
}
//timeval.tv_sec=5,timeval.tv_usec=500; 代表设置超时等待周期为5秒500毫秒
I/O复用服务器端的实现
io_echoserver.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 1024
void Sender_error(char *message);
int main(int argc, char *argv[])
{
int port, serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
printf("Please input the port of socket that you want to create:\n");
scanf("%d", &port);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
{
Sender_error((char *)"Sock creation error");
}
else
{
// 注意:serv_sock初始化成功后值为0
fd_max = serv_sock;
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(port);
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
{
Sender_error((char *)"Bind() error");
}
if (listen(serv_sock, 5) == -1)
{
Sender_error((char *)"Listen error");
}
// 对监测项的文件描述符作初始化赋0操作
FD_ZERO(&reads);
// 注册serv_sock套接字信息至reads变量中
FD_SET(serv_sock, &reads);
while (1)
{
// cpy_reads用来记录文件描述符变化
cpy_reads = reads;
// 设置超时等待周期为5s
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 只关注接收数据的套接字,不对传输数据、出现异常的套接字进行监视
// fd_num用来记录发生监视事件的文件描述符数量
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
{
break;
}
if (fd_num == 0)
{
continue;
}
for (i = 0; i < fd_max + 1; i++)
{
// 判断cpy_reads中是否含有文件描述符i的信息
if (FD_ISSET(i, &cpy_reads))
{
// 若当前只有服务器端套接字,则尝试接收客户端请求
if (i == serv_sock)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
// 将客户端的套接字文件描述符信息注册至reads中
FD_SET(clnt_sock, &reads);
if (fd_max < clnt_sock)
{
// 增加监视数上限(因为有新的客户端套接字加入)
fd_max = clnt_sock;
}
printf("Connected client: %d \n", clnt_sock);
}
else
{
// 接收数据
str_len = read(i, buf, BUF_SIZE);
// 无数据,则关闭对应套接字
if (str_len == 0)
{
// 清除reads变量中文件描述符i的信息
FD_CLR(i, &reads);
close(i);
printf("Closed client: %d \n", i);
}
else
{
write(i, buf, str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
void Sender_error(char *message)
{
puts(message);
exit(1);
}
运行结果: