7. C++通过select的方式实现高性能网络服务器

什么是阻塞IO/非阻塞IO

阻塞IO指的是用户程序将IO请求提交后,无需等待IO操作的完成,而是可以继续处理别的事情。
所谓阻塞IO,是指以事件触发的机制来对IO操作进行处理。
与多进程和多线程技术相比,阻塞I/O技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
我们举个例子吧:
假如说我们要去读一个数据,我们要发生系统调用,然后去询问IO是否准备好了,
在这里插入图片描述
对于阻塞I/O,如果没有准备好的话,他就会一直等,一直等,直到他准备好了。然后再进行下一步操作。
在这里插入图片描述
对于这个非阻塞I/O,发起系统调用后,不是一直等,而是立即返回一个没准备好的I/O。
在这里插入图片描述
然后过一段时间这个用户态会再次询问是否准备好了。如果还是没有准备好,还是会立即返回,知道准备好了。
在这里插入图片描述
然后在一些空挡里面,这个CPU是可以干其他的事情的。

  1. 阻塞IO
    为了完成IO发起IO调用,若IO事件没有就绪,则一直等待,直到IO就绪,开始数据拷贝。
    优点:流程最为简单,使用复杂度非常低。
    缺点:IO效率较低,对资料利用不足。
    在这里插入图片描述
    非阻塞IO
    为了完成IO发起IO调用,若IO事件没有就绪,则调用直接返回(返回后,先进行一些其他任务,之后再重新发起IO调用)。
    优点:不等待IO就绪,而是直接立即返回,可以继续对其他描述符进行IO操作,充分利用资源,效率相较于阻塞IO有所提高。
    缺点:IO不够实时,且通常需要循环进行操作,增加了一定的复杂度。
    在这里插入图片描述

以select方式实现高性能网络服务器

  • 遍历文件描述符集中的所有描述符,找出有变化的描述符
    所以这个select是一个半自动,需要我们遍历所有的

  • 对于侦听的socket和数据处理的socket要区别对待

  • socket必须设置为非阻塞方式工作

重要的API

重要结构体:

sockaddr_in是一个结构体,用于表示Internet地址和端口号。
它定义在头文件<netinet/in.h>中,通常与套接字(socket)API一起使用。
 sockaddr_in结构体包含以下字段:
 sin_family:地址族,通常设置为AF_INET表示IPv4协议。
 sin_port:端口号,以网络字节序表示。
 sin_addr:IP地址,以网络字节序表示。
 sin_zero:填充字段,通常设置为0。
1.函数接口
	(1)select 
	   int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
	   功能:
			监听文件描述符集合
	   参数:
			nfds:最大文件描述符个数+1
			readfds:读文件描述符集合
			writefds:写文件描述符集合
			exceptfds:其余文件描述符集合,也就是异常的文件描述符集合
			timeout:设定超时时间,也就是多长时间询问一次,如果没有这个的话,阻塞的就会一直阻塞
			NULL:永远等待,直到有数据发生
	   返回值:
			成功返回产生事件的文件描述符个数
			失败返回-1 	
2. void FD_CLR(int fd, fd_set *set);
	功能:将fd从文件描述符集合中移除		
3. int  FD_ISSET(int fd, fd_set *set);
	功能:判断fd是否仍在文件描述符集合中		
4. void FD_SET(int fd, fd_set *set);
	功能:将fd加入文件描述符集合			
5. void FD_ZERO(fd_set *set);
	功能:将文件描述符集合清0 
6. flag fcntl(fd,F_SETFL/F_GETFL, flag)
这个可以将文件描述符设置成阻塞或者非阻塞。

fcntl 函数的基本原型:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fd: 需要进行操作的文件描述符。
cmd: 需要执行的操作命令,可以是 F_GETFL(获取文件状态标志)、F_SETFL(设置文件状态标志)等。
arg: 根据不同的命令可能需要的参数。

常见的 fcntl 命令:
F_DUPFD:
复制文件描述符,返回一个新的文件描述符。
F_GETFD:
获取文件描述符标志。
F_SETFD:
设置文件描述符标志。
F_GETFL:
获取文件状态标志。
F_SETFL:
设置文件状态标志。
F_GETLK:
获取记录锁的信息。
F_SETLK:
设置或释放记录锁。
F_SETLKW:
阻塞地设置或释放记录锁。

示例用法:
获取和设置文件状态标志:
// 获取文件状态标志
int flags = fcntl(fd, F_GETFL);
// 设置文件状态标志为非阻塞模式
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

代码的思路就是:首先将socket_fd设置成异步非阻塞,然后设置一个fd_set fd_sets;为文件集。
然后先将其全部清零,然后将一开始创造的监听的socket,加入到文件描述符集合fd_sets中

//文件描述符集合清0
FD_ZERO(&fd_sets);
//将第一个socket_fd加入到文件描述符集合
FD_SET(socket_fd,&fd_sets);

然后创建一个accept_fds[FD_SIZE]表示每个连接的socket,然后将accept_fds[i]加入到fd_sets中去。并且更新max_fd。

for(int i=0;i<FD_SIZE;i++){
            if(accept_fds[i]!=-1){//判断这个是有效的socket
                //判断是否大于最大的文件描述符
                if(accept_fds[i]>max_fd){
                    max_fd=accept_fds[i];
                }
                //将这个accepts_fds[i]加入到fd_sets中去
                FD_SET(accept_fds[i],&fd_sets);
            }
        }

然后用select进行监听

//fd_sets读的,写的,超时的,时间不关心
events=select(max_fd+1,&fd_sets,NULL,NULL,NULL);

然后判断符合条件的events有多少个。如果events等于0的话。首先要判断是新来了一个连接的socket,还是已有的socket需要发送消息。如果是新来一个socket的话,那就在accept_fds中找一个空位置插进去。

//判断socket_fd是否在fd_sets集合中,判断是一个新的连接还是一个数组,判断是否是侦听的这个socket触发的事件
if(FD_ISSET(socket_fd,&fd_sets)){
    for(int i=0;i<FD_SIZE;i++){//找一个空槽
        if(accept_fds[i]==-1){
            curpos=i;
            break;
        }
    }
    socklen_t addr_len=sizeof(struct sockaddr);
    accept_fd = accept(socket_fd,
                       (struct sockaddr *)&remoteaddr,
                       &addr_len);
    //设置成非阻塞
    //创建了socket之后我们要设置成异步的
    flags = fcntl(accept_fd,F_GETFL,0);
    //然后设置成非阻塞
    fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);
    //将这个接收的插入到槽中
    accept_fds[curpos]=accept_fd;
}

如果不是新来的话,那就是accept_fds[i]的事件,那么进行文件的收发

//对于每一个accept_fds[i]进行读数据
for(int i=0;i<FD_SIZE;i++){
    if(accept_fds[i]!=-1&&FD_ISSET(accept_fds[i],&fd_sets)){
        memset(in_buff, 0, sizeof(in_buff));
        //接收消息
        ret = recv(accept_fds[i],(void *)in_buff,MESSAGE_LEN,0);
        if(ret==0){
            close(accept_fds[i]);
            break;
        }
        std::cout<<"recv:"<<in_buff<<std::endl;
        //返回消息
        send(accept_fds[i],(void*)in_buff,MESSAGE_LEN,0);
    }
}

总的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
//端口
#define PORT 8888
#define MESSAGE_LEN 1024
#define FD_SIZE 1024

int main(int argc,char* argv[]){

    int ret=-1;
    int on=1;
    int backlog=10;//缓冲区大小

    int socket_fd,accept_fd;

    pid_t pid;

    int flags;

    int max_fd=-1;//最大的文件描述符
    int curpos=-1;//临时变量
    int events=0;//select中符合条件的返回值

    fd_set fd_sets;//读的文件集

    int accept_fds[FD_SIZE]={-1,};

    struct sockaddr_in localaddr,remoteaddr;

    char in_buff[MESSAGE_LEN]={0,};

    //这个是侦听的那个
    socket_fd=socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd==-1){
        std::cout<<"Failed to create socket!"<<std::endl;
        exit(-1);
    }


    //创建了socket之后我们要设置成异步的
    flags = fcntl(socket_fd,F_GETFL,0);
    //然后设置成非阻塞
    fcntl(socket_fd,F_SETFL,flags | O_NONBLOCK);

    //这是max_fd=创建的第一个socket
    max_fd=socket_fd;

    ret=setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
    if(ret==-1){
        std::cout<<"Failed to set socket options!"<<std::endl;
    }

    localaddr.sin_family=AF_INET;//地址族
    localaddr.sin_port=htons(PORT);//端口号
    localaddr.sin_addr.s_addr=INADDR_ANY;//这个就是0
    bzero(&(localaddr.sin_zero), 8);

    ret= bind(socket_fd,(struct sockaddr *)&localaddr,sizeof(struct sockaddr));//绑定
    if(ret==-1){//绑定失败
        std::cout<<"Failed to bind addr!"<<std::endl;
        exit(-1);
    }

    ret = listen(socket_fd,backlog);//第二个是缓冲区大小,因为同一时间只能处理一个,其他都放在缓冲区
    if(ret==-1){
        std::cout<<"failed to listen socket!"<<std::endl;
        exit(-1);
    }
    while(1){//等待连接
        //文件描述符集合清0
        FD_ZERO(&fd_sets);
        //将第一个socket_fd加入到文件描述符集合
        FD_SET(socket_fd,&fd_sets);

        for(int i=0;i<FD_SIZE;i++){
            if(accept_fds[i]!=-1){//判断这个是有效的socket
                //判断是否大于最大的文件描述符
                if(accept_fds[i]>max_fd){
                    max_fd=accept_fds[i];
                }
                //将这个accepts_fds[i]加入到fd_sets中去
                FD_SET(accept_fds[i],&fd_sets);
            }
        }

        //fd_sets读的,写的,超时的,时间不关心
        events=select(max_fd+1,&fd_sets,NULL,NULL,NULL);

        if(events<0){//函数调用失败
            std::cout<<"Failed to use select!"<<std::endl;
        }else if(events==0){
            std::cout<<"timeout...!"<<std::endl;
        }else if(events){
            //判断socket_fd是否在fd_sets集合中,判断是一个新的连接还是一个数组,判断是否是侦听的这个socket触发的事件
            if(FD_ISSET(socket_fd,&fd_sets)){
                for(int i=0;i<FD_SIZE;i++){//找一个空槽
                    if(accept_fds[i]==-1){
                        curpos=i;
                        break;
                    }
                }
                socklen_t addr_len=sizeof(struct sockaddr);
                accept_fd = accept(socket_fd,
                                   (struct sockaddr *)&remoteaddr,
                                   &addr_len);
                //设置成非阻塞
                //创建了socket之后我们要设置成异步的
                flags = fcntl(accept_fd,F_GETFL,0);
                //然后设置成非阻塞
                fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);
                //将这个接收的插入到槽中
                accept_fds[curpos]=accept_fd;
            }

            //对于每一个accept_fds[i]进行读数据
            for(int i=0;i<FD_SIZE;i++){
                if(accept_fds[i]!=-1&&FD_ISSET(accept_fds[i],&fd_sets)){
                    memset(in_buff, 0, sizeof(in_buff));
                    //接收消息
                    ret = recv(accept_fds[i],(void *)in_buff,MESSAGE_LEN,0);
                    if(ret==0){
                        close(accept_fds[i]);
                        break;
                    }
                    std::cout<<"recv:"<<in_buff<<std::endl;
                    //返回消息
                    send(accept_fds[i],(void*)in_buff,MESSAGE_LEN,0);
                }
            }
        }
    }
    close(socket_fd);//侦听的那个
    return 0;
}

客户端也是之前的。
在这里插入图片描述

设置select超时时间

对于上面的代码我们没有设置select超时时间,也就是超时一直等待,直到处理到。那么怎么设置select超时时间呢?

struct timeval{
	long tv_sec;//秒
	long tv_usec;//微妙
}

一般设置为500ms。

select 函数

select 函数输入参数的意义

  • 我们关心的文件描述符

  • 对每个文件描述符我们关心的状态(读,写,异常)

  • 我们要等待的时间(永远(NULL)、一段时间(500ms),不等待(0))

从select函数得到的信息

  • 已经做好准备的文件描述符的个数
  • 对于读、写、异常,哪些文件描述符准备好了

理解select模型

  • 理解select模型的关键在于理解fd_set类型,这个fd_set可以理解为一个bit_set,里面的每一位都代表一个文件描述符。
  • 其实fd set就是多个整型字的集合,每个bit代表一个文件描述符。
  • FD_ZERO表示将所有位置置为0
  • FD_SET是将fd_set中的某一位置1
  • select函数执行后,系统会修改fd_set中的内容
  • select函数执行后,应用层要得新设置fd_set 中的内容

模型图

首先我们要侦听这三个文件描述符,是否会发生读事件。
在这里插入图片描述
当系统发现有改变的话,系统就会监听到,然后select就会返回。
在这里插入图片描述

在这里插入图片描述

select优缺点

优点:
1)单进程让服务端拥有基本的一对多响应能力。
2)实现较为简单。(模型比较轻量)
3)SELECT跨平台能力较强。(各个系统兼容)
4)如果网络IO监听模型对时间精度有要求,select可以满足需要,支持微妙级别定时监听。
缺点:
1)SELECT受fd_set监听集合类型的影响, 最大监听数为1024。(不能满足服务端高并发需求)
2)SELECT监听采用的轮询方式,(随着轮询数量的增加 ,IO处理性能呈线性下降),轮询模型可能导致事件处理不及时。
3)SELECT启动监听时传入监听集合, 监听到就绪后内核修改为就绪集合,该就绪集合无法作为监听集合再次使用,所以用户必须将传入传出进行分离,比较麻烦。
4)SELECT监听到就绪后只返回就绪的数量,没有返回谁就绪,开发者需要自行遍历查找就绪的socket并处理, 开销较大,比较耗时。
5)SELECT每轮监听,都要向内核空间重新拷贝监听集合, 将集合中设置的socket,挂载到等待队列中设置监听, 这种做法会导致大量重复的拷贝开销与挂载开销。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值