高性能服务器编程之I/O复用---select

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_43949535/article/details/102736849

2019年10月25日09:30:07


注:I/O复用技术对于我们开发高性能服务器至关重要,这一部分内容主要详见《Linux高性能服务器编程》的第9章

背景说明

IO复用技术使得程序能够 同时监听多个文件描述符,这对提高程序的性能至关重要。Linux下实现I/O复用的系统调用主要有select、poll、epoll。一般情况,网络程序会在下面的几种情形下使用到I/O复用技术:

  • client程序要同时处理多个socket。如 非阻塞connect技术
  • client程序要同时处理用户输入和网络连接。如 聊天室程序
  • TCP服务器要同时处理监听socket和连接socket。这是I/O复用使用最多的场合,我下面给大家展示的代码示例 也是这种情形
  • 服务器要同时处理TCP请求和UDP请求。如 回射服务器
  • 服务器要同时监听多个端口,或者处理多种服务。如 xinetd服务器

注:I/O复用虽然可以同时监听多个文件描述符,但它本身是阻塞的。 详细原因,下面解释。而且当多个文件描述符同时就绪时,假如不采取额外的措施,程序就只能按顺序依次进行处理其中的这每一个文件描述符,这使得服务器程序看起来像是在串行工作。而若要实现并发,只能使用多线程或多进程等编程手段。

这里首先说明的是:select。其用途:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。

select API

select系统调用 函数原型如下:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

下面详细分析其每一个参数:

  • nfds:指定了被监听的文件描述符的总数。它通常被设置为select监听的文件描述符:readfds,writefds,exceptfds这三个描述符集中的最大描述符值(即编号) 加1,因为文件描述符是从0开始计数的。这主要是基于提高底层效率设计的。
  • readfds、writefds和exceptfds:分别指向可读、可写和异常事件对应的文件描述符集合。 应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符,轮询等待这些描述符有事件产生。select调用返回时,内核将修改 它们来通知应用程序哪些文件描述符已经就绪。这3个参数是fd_set结构指针类型。fd_set结构体的定义如下:在这里插入图片描述
  • 如上图所示:fd_set结构体仅包含一个整形数组(在这里,我们可以认为是32个元素),该数组的每个元素的每一位(bit)标记一个文件描述符(于是这样的位就有1024个)。fd_set能容纳文件描述符的数量由FD_SETSIZE(即1024)指定,所以这就限制了select能同时处理的文件描述符的总量。
  • 而且因为位操作过于繁琐,我们应该使用下面的一系列来访问fd_set结构体中的位:
#include<sys/select.h>
void FD_ZERO(fd_set *set); /* 清除fdset的所有位 */
void FD_SET(int fd, fd_set *set); /* 设置fdset的位fd */
void FD_CLR(int fd, fd_set *set); /* 清除fdset的位fd */
int  FD_ISSET(int fd, fd_set *set); /* 测试fdset的位fd是否被设置 */
  • timeout:这个参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout的值是不确定的。timeval结构体的定义如下:
struct timeval
{
     long    tv_sec;     //秒数
     long    tv_usec;    //微秒数
};

由以上定义可见,select给我们提供了一个微秒级的定时方式:

  • 若是给timeval的这两个成员分量都赋值0,则select将立即返回
  • 若是给此处的timeout传入NULL,则select将一直阻塞,直到某个文件描述符就绪。

select成功返回就绪(可读、可写和异常)的文件描述符的总数;而如果在超时时间内没有任何描述符就绪,select返回0;select失败返回-1并设置errno。如果在select等待期间,程序收到信号,则select立即返回-1,并设置errno为EINTR。

文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常,这对于select的使用非常关键。

在网络编程中,下列情况下socket可读:

  • socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  • socket通信的对方关闭连接。此时对该socket读操作将返回0。
  • 监听socket上有新的连接请求。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

在网络编程中,下列情况下socket可写:

  • socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞写该socket,并且写操作返回的字节数大于0。
  • socket的写操作被关闭。对 写操作被关闭的socket 执行写操作将触发一个SIGPIPE信号。
  • socket使用非阻塞connect连接成功或者失败(超时)之后。
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

在网络编程中,下列情况下socket处理异常情况:

  • socket上接收到带外数据。

使用select处理示例

从STDIN获取键盘输入

下面的这个代码主要就是展示:select方法的使用方法,程序从键盘读取输入的数据(即标准输入stdin——其文件描述符为0),超时时间timeout设置为5s。它只有在输入就绪时才读取键盘。也即:执行这个程序时,每隔5s检测没有文件描述符就绪即打印一个 time out 超时信息。而如果在键盘上键入字符,则就会从标准输入中读取数据并打印出来这个数据的内容。用select调用来检查标准输入的状态,程序通过事先设置的超时时间,即每隔5秒打印一条 time out 超时信息(这是通过select系统调用返回0来判断的),具体的程序效果如下:
在这里插入图片描述
详细代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/select.h>
#include <sys/time.h>

#define SIDIN 0//一般 :标准输入的文件描述符为0
int main()
{
    int fd=SIDIN;//于是 fd为标准输入文件描述符 0
    fd_set fdset;//fd_set结构体 变量(里面有1024位)

    while(1)
    {
        FD_ZERO(&fdset);//清空集合 全部置0
        FD_SET(fd,&fdset);//把文描fd添加到集合中

        struct timeval tv={5,0};//每次阻塞5秒:即在标准输入stdin上最多等待5s
        
        //select返回值为n,表示有n个状态发生变化的描述符
        int n=select(fd+1,&fdset,NULL,NULL,&tv);//这里只是监听了 可读事件
        
        if(n== -1)//select失败返回-1而且会设置errno
        {
            perror("select error");
            continue;
        }
        else if(n==0)//select为0表示文件描述符在5s内都没有变化
        {
            printf("time out\n");//于是就打印出超时信息
            continue;
        }
        else 
        {
        	//通过FD_ISSET方法判断参数fd指向的文件描述符是否是
        	//由参数fdset指向的fd_set结构体集合中的一个元素 
            if(FD_ISSET(fd,&fdset))
            {
            	//既然是被修改过,则从标准输入stdin读取数据到buff中
                char buff[128]={0};
                read(fd,buff,127);
                printf("read:%s\n",buff);
            }
        }
    }
}

基于select实现的服务器 连接多客户端

之前实现的服务器—多客户端的方式都是借助于 多线程和多进程的方式,而我们这里的实现方式:借助于select调用来同时处理多个客户端即可。

具体实现为:服务器可以让select系统调用同时检查监听socket和客户的连接socket。一旦select调用指示有活动发生,就可以用FD_ISSET来遍历一下所有可能的文件描述符,以检查确定是哪个文件描述符上面有事件发生。具体的程序效果如下:在这里插入图片描述
select实现了多客户访问的详细代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/select.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define MAX 10

void fds_init(int fds[])//fds集合初始化
{
    int i=0;
    for(;i<MAX;++i)
    {
        fds[i]=-1;//全置为 -1无效的文描
    }
}

void fds_add(int fds[],int fd)//向集合fds中添加文件描述符fd
{
    int i=0;
    for(;i<MAX;++i)
    {
        if(fds[i]==-1)
        {
            fds[i]=fd;//找到个位置就放进去
            break;
        }
    }
}
void fds_del(int fds[],int fd)//在集合fds中删除文件描述符fd
{
    int i=0;
    for(;i<MAX;++i)
    {
        if(fds[i]==fd)
        {
            fds[i]=-1;
            break;
        }
    }
}

//专门创建sockfd函数
int create_socket()
{
	//创建监听套接字(socket描述符),指定协议族ipv4,字节流服务传输
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        return -1;
    }

	//socket专用地址信息进行注册
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));

    saddr.sin_family = AF_INET;
    saddr.sin_port= htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

	//命名套接字,将socket专用地址信息绑定到socket描述符上
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res == -1)
    {
        return -1;
    }
    
    listen(sockfd,5);//创建启动监听队列
    return sockfd;
}

int main()
{
    int fds[MAX];
    fds_init(fds);//初始化文件描述符集合fds

    int sockfd=create_socket();
    assert(sockfd != -1);

    fds_add(fds,sockfd);//首先把sockfd添加到文件描述符集合fds中

    fd_set fdset;//fd_set结构体 变量(里面有1024位)
    while(1)
    {
        int maxfd=-1;
        int i=0;
        
        FD_ZERO(&fdset);//fdset初始化为空集合,清除fdset上的所有位
        for(;i<MAX;++i)//这里循环遍历以找到最大的文件描述符
        {
            if(fds[i]!=-1)
            {
                FD_SET(fds[i],&fdset);//设置fdset的fds[i]位
                if(fds[i]>maxfd)
                {
                    maxfd=fds[i];
                }
            }
        }

        struct timeval tv={5,0};//每次阻塞5秒
        
        //select成功时返回就绪文件描述符的总数为n
        int n=select(maxfd+1,&fdset,NULL,NULL,&tv);
        if(n==-1)//select失败返回-1并设置errno
        {
            perror("select error");
            continue;
        }
        else if(n==0)//select为0表示文件描述符在5s内都没有变化
        {
            printf("time out\n");//于是就打印出超时信息
            continue;
        }
        else//n有效 遍历所有可能的文件描述符,以检查是哪个上面有事件发生。
        {
            int i=0;
            for(;i<MAX;++i)
            {
                if(fds[i]==-1)//无效的  直接忽略
                {
                    continue;
                }
                //有效的文描
                if(FD_ISSET(fds[i],&fdset))//fdset的fds[i]位已经set,即有数据
                {
                	//下面是监听队列中有连接待处理,使用accept取出一个连接
                	//这说明正有一个客户端试图建立连接,此时就直接可以调用accept
                	//而不用担心发生阻塞的问题
                    if(fds[i]==sockfd)
                    {
                        struct sockaddr_in caddr;
                        int len=sizeof(caddr);
						
						//接收一个套接字已建立的连接,得到连接套接字c值
                        int c=accept(sockfd,
                        			(struct sockaddr*)&caddr,&len);
                        if(c<0)
                        {
                            continue;
                        }
                        printf("accept c=%d\n",c);
                        //同理:将连接套接字c,添加到fds文件描述集合中 监听
                        fds_add(fds,c);
                    }
                    else//else 没有新连接,直接使用recv接收客户端数据
                    {
                        char buff[128]={0};
                        //recv用来接收客端数据
                        int n=recv(fds[i],buff,5,0);
                        //接收服务器端的数据是0,说明客户端已经关闭
                        if(n<=0)
                        {
                            close(fds[i]);//先将文件描述符fds[i]关闭
                            fds_del(fds,fds[i]);//移除出fds数组
                            printf("one client over\n");

                            continue;
                        }
                        else
                        {
                        	//打印客户端发来的数据,并向客户端发送回复
                            printf("read(%d)=%s\n",fds[i],buff);
                            send(fds[i],"over",4,0);
                        }
                    }
                }
            }
        }
    }
}

select的特点小结

上面也说过了,select: 关注可读、可写 、异常事件。其记录每种事件的结构 fd_set (在数组按位来记录关注的文件描述符上的事件),但是每次最多可以监听1024个文件描述符,并且其最大值1023。毕竟底层只是一个int型32位数组大小。

此外,select函数返回时,通过传递的结构体变量将结果带回 (就绪的和未就绪的文件描述符),主要由内核修改用户变量。全混在一起的话,每次都必须循环探测哪些文件描述符是就绪的 时间复杂度为O(n);每次调用select之前都必须重新设置三个结构体变量(上面的代码里面的FD_ZERO方法)。

用户传入的文件描述符和内核返回的文件描述符都是通过select的参数实现的,所以每次调用select之前必须重新设置结构体,内核会将所有的文件描述符返回,所以用户探测就绪文件描述符的时间复杂度O(n)。这其中的用户态和内核态交互,内核修改后再传递回用户态。在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43949535/article/details/102736849