本章将讨论实现并发服务器的第二种办法,基于I/O复用的服务器端构建。
I/O复用它允许单个进程或线程同时处理多个输入/输出(I/O)操作,而无需为每个I/O操作创建一个独立的线程或进程。这种技术可以显著提高应用程序的效率和性能,特别是在需要处理大量并发连接的场景下。
1.多进程服务器端的缺点
基于进程的并发服务器在创建进程时,需要付出极大的代价,因为需要大量的运算和内存空间,而且由于每个进程具有独立的内存空间,相互间交换数据的方法也很复杂。
如果使用I/O复用技术,则可以在不创建进程的情况下同时向多个客户端进行服务。
2.基于I/O复用的服务器端
上图左边时多进程服务器端模型,右边是I/O复用服务器端模型,可以看出后者无论连接多少个客户端,提供服务的进程只有1个
3.理解select()
函数
select()
函数是一种用于I/O复用的系统调用,它允许程序同时监视多个文件描述符(包括套接字),以确定哪些文件描述符已经准备好进行读取或写入操作。select() 函数在Linux系统和Windows系统中都支持,下面是以Linux系统为例,Windows系统示例放在最后。
①select()
函数基本语法
//发生错误时返回-1;
//发生关注的事件返回时,返回该事件的文件描述符(大于0)
int select(
int maxds, //设置位监视的文件描述符中最大的描述符加1。例如,如果你监视的文件描述符范围是0到5,则maxfds应该设置为6。
fd_set *readfds, //将所有关注“是否有可读数据”的文件描述符注册到fd_set,并传递其地址值
fd_set *writefds,//将所有关注“是否有可写数据”的文件描述符注册到fd_set,并传递其地址值
fd_set *exceptfds, //将所有关注“是否发生异常”的文件描述符注册到fd_set,并传递其地址值
struct timeval *timeout//设置select() 调用的超时时间,为了防止调用select()函数后,陷入无限阻塞
);
//struct timeval的结构体定义
struct timeval{
long tv_sec; //秒数
long tv_usec; //毫秒数
}
②select()
函数的调用过程
select()
函数的调用流程如下图所示,本文将逐步介绍
- 设置文件描述符:
select()
可以监视多个文件描述符,将需要监视的文件描述符集中为一个集合fd_set
,集中时也要按照读、写、异常进行分为三类,fd_set
结构如下图所示:
设置位为1则代表时监视对象,所以上图中fd1和fd3的是监视对象。在fd_set
变量中用下列宏进行操作:
FD_ZERO(fd_set* fdset); //将所有fd_set变量的所有位设置位0
FD_SET(int fd, fd_set* fdset); //将fd_set中指定变量的位设置为1
FD_CLR(int fd, fd_set* fdset); //将fd_set中指定变量的位设置为0
FD_ISSET(int fd, fd_set* fdset); //如果文件描述符在集合中,返回非零值;如果不在集合中,返回0。
-
指定监视范围: 即
select()
函数中的第一个参数maxds
, 要监视的文件描述符中最大的描述符加1。例如,如果你监视的文件描述符范围是0到5,则maxfds应该设置为6,因为文件描述分值是从0开始 -
设置超时: 如果监视的文件描述符一直没有变化,则会陷入阻塞状态,所以需要设置超时时间,告知服务器超时消息。将秒数填入
tv_sec
,将毫秒数填入tv_usec
-
调用select()函数: 调用 select() 函数,函数会阻塞,直到有文件描述符发生变化或者超时。
-
查看调用结果: 返回-1则发生错误;返回一个整数值,则代表有多少个文件描述符准备好了I/O操作,可以进行读取操作了,下图是变化
调用函数前,告诉select()函数,需要监视fd1、fd2、fd3,当调用函数后,首先是将监视的文件描述符的位初始化为0,然后监视到fd1和fd3发生变化,所以fd1和fd3的位又变成了1。
③select()函数示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
int main(){
fd_set reads,temps;// 定义文件描述符集合,reads用于存储关注是否可读的文件描述符,temps用于存储select函数返回时准备好的文件描述符
int result,str_len;// result用于存储select函数的返回值,str_len用于存储读取的字节数
char buff[1024];
struct timeval timeout;// 定义一个timeval结构,用于设置select函数的超时时间
FD_ZERO(&reads);// 清空reads集合
FD_SET(0,&reads);// 将文件描述符为0的设置为监视状态,文件描述符0通常代表标准输入(stdin),它是一个特殊的文件描述符,用于从键盘接收输入。
while (1)
{
temps=reads;/// 复制reads集合到temps集合
timeout.tv_sec=5;
timeout.tv_usec=0;
result=select(1,&temps,0,0,&timeout);//1表示有1个文件被监视,temps表示被监视“是否可读”的文件描述符集合,0表示“不关注可写”文件描述符,0表示“不关注异常”文件描述符,timeout表示超时时间
if(result==-1){// 如果select函数返回-1,表示出错
puts("select error");
break;
}
else if(result==0){// 如果select函数返回0,表示超时
puts("time-out");
}
else{// 如果select函数返回大于0,表示有文件描述符准备好了
if(FD_ISSET(0,&temps)){// 检查文件描述符0是否在temps集合中
str_len=read(0,buff,1024);// 从文件描述符0读取数据到buff缓冲区,最多1024字节
buff[str_len]=0;
printf("message from console: %s",buff);
}
}
}
return 0;
}
键盘输入有被正常监视到,如果不输入,5秒后会输出超时信息