Linux网络编程---I/O多路复用 之 select


实现一个基本的流式套接字客户端/服务器通信程序,客户端和服务器按如下步骤交互:
(1)客户端向服务器发出日期时间请求字符串,如:%D %Y %A %T等。
(2)服务器从网络接收到日期时间请求字符串后,根据字符串格式生成对应的日期时间值返回给客户端。

在前面我们用了单进程和多进程来简单实现这个程序,通过多进程会比单进程占用更多的系统资源,所以希望单进程也能具有为多个客户端同时提供服务的能力,通过Linux系统提供的select函数的I/O多路复用机制,能较好得满足这样的要求。

I/O多路复用通常在如下网络应用场景中得到广泛的应用:
(1)客户端同时处理多个文件描述符(例如交互输入和网络套接字)。
(2)客户端要同时处理过个网络套接字。
(3)TCP服务器同时处理监听套接字和连接套接字。
(4)服务器同时处理TCP和UDP.

select 函数:

   下图为select()函数的介绍

   

    select函数根据希望进行的文件操作对文件描述符进行了分类,这里对文件描述符的处理主要涉及4个宏函数,如下表2所示

    

     另外,select()函数中的 timeout 是一个 struct timeval类型的指针,该结构体如下所示:

     

    可以看到,这个时间结构体的精确度可以设置到微秒级,这对于大多数的应用而言已经足够了。

    使用select()函数的过程可概括为:先调用FD_ZERO() 将指定的fd_set清零,然后调用宏FD_SET()将需要测试的fd加入fd_set,接着调用函数select测试fd_set中的所有fd,最后用用宏FD_ISSET()检查某个fd在函数select调用后,相应位是否仍然为1。在执行完对相关文件描述符的操作后,使用FD_CLR来描述符集。



再改写上面程序之前,我们先简单熟悉select函数的用法,下面程序实现如下简单功能:

(1)服务器接收客服端发送的数据,并发回至客服端,客服端显示出接收的数据
(2)客服端退出后,服务器给予提示
(3)服务器可手动退出,若服务器退出,客服端自动退出

//服务器
//使用方法: ./server ip port
#include  
#include  
#include  
#include  
#include  
#include 
#include 
#include 
#include 
#include 
#define maxs 1024

void do_task(int *clientfd,int maxsfd,fd_set* rset, fd_set* allset)
{
    int nread;
    char buf[maxs];
    for (int i=0; imaxfd)
                maxfd=connfd;
            
            FD_SET(connfd,&allset); //添加连接套接字到文件描述符集合中
            if (--nread<=0) //如果只返回一个可读的,则不用往下执行了
                continue;
        }
        
        if (FD_ISSET(STDIN_FILENO,&rset)) //可输入
        {
            scanf("%s",quit);
            if (strcasecmp(quit, "QUIT")==0) //是否退出
            {
                close(listenfd);
                return 0;
            }
        }
        do_task(clientfd,maxfd,&rset,&allset); //执行客户请求
    }
    return 0;
}


//客服端
//使用方法:./client ip port
#include  
#include  
#include  
#include  
#include  
#include 
#include 
#include 
#include 
#include 
#define maxs 1024

void do_task(int sockfd)
{
    fd_set rset,allset;
    FD_ZERO(&allset);
    char buf[maxs];
    int nread;
    //将输入描述符和套接字描述符加入集合中
    FD_SET(STDIN_FILENO,&allset);
    FD_SET(sockfd,&allset);
    int maxfd=STDIN_FILENO>sockfd? STDIN_FILENO :sockfd;
    while (1)
    {
        rset=allset;
        if (select(maxfd+1, &rset, NULL, NULL, NULL)==-1) //阻塞式调用select
        {
            perror("select error");
            continue;
        }
        if (FD_ISSET(sockfd,&rset)) //套接字可读
        {
            nread=read(sockfd, buf, sizeof(buf)); //接收服务器发送数据
            if (nread==0) //服务器出错
            {
                printf("server close the connection\n");
                break;
            }
            else if(nread==-1)
            {
                perror("read error");
                break;
            }
            else
            {
                //显示数据
                write(STDOUT_FILENO, "get server send data:",21);
                write(STDOUT_FILENO, buf, nread);
            }
        }
        if(FD_ISSET(STDIN_FILENO,&rset)) //当前可输入
        {
            
            gets(buf);
            if (strlen(buf)==0)
                continue;
            if(strcasecmp(buf, "quit")==0)
            {
                printf("client exit...\n");
                break;
            }
            
            write(sockfd, buf, strlen(buf));  //发送输入数据
            printf("send to server ok\n");
        }
    }
}
int main(int argc,char ** argv)
{
    int sockfd;
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    //设置服务器信息
    inet_aton(argv[1], &server.sin_addr);
    server.sin_port=htons(atoi(argv[2]));
    server.sin_family=AF_INET;
    sockfd=socket(AF_INET, SOCK_STREAM, 0);
    if (connect(sockfd, (struct sockaddr*)&server, sizeof(server))<0)
    {
        perror("connect error");
        return -1;
    }
    do_task(sockfd);
    close(sockfd);
    return 0;
}






下面是改写的时间服务器程序,貌似有点麻烦:

/*
 TCP服务器(单进程)
 用法:./server port
*/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define BUFSIZE 1024
static void bail(const char *on_what)
{
    fputs(strerror(errno),stderr);
    fputs(": ",stderr);
    fputs(on_what, stderr);
    fputc('\n',stderr);
    exit(1);
}
struct sockfd_opt //处理每个socket描述符的结构体
{
    int fd;    //描述符
    int (*do_task)(struct sockfd_opt *p_so); //回调函数
    
};
std::listList; //链表元素
fd_set rfds,orfds; //文件描述符集合
int maxfd;   //文件描述符的最大取值
//向客户端发回日期时间
int send_reply(struct sockfd_opt *p_so)
{
    char reqBuf[BUFSIZE];//接收缓存
    char dtfmt[BUFSIZE];//日期-时间结果字符串
    time_t td; //当前时间和日期
    struct tm tm;
    long z;
    if ((z=read(p_so->fd, reqBuf,sizeof(reqBuf)))<0)
    {
       //关闭当前套接字描述符
        close(p_so->fd);
       //注意:这里必须从orfds集合中删除此关闭的文件描述符,以方便下次调用select
        FD_CLR(p_so->fd,&orfds);
       //从套接字选项链表中删除当前选项p_so;
        List.remove(p_so);
       //若读操作返回-1且不是RST分段
        if (z<0 && (errno|=ECONNRESET))
            bail("read()");
    }
    else
    {
        reqBuf[z]=0;
        time(&td);
        tm=*localtime(&td);
        strftime(dtfmt, sizeof(dtfmt), reqBuf, &tm);
        
        //向客户端发回结果
        z=write(p_so->fd, dtfmt,strlen(dtfmt));
        if (z<0)
            bail("write()");
    }
    
    return 0;
}

//接收TCP连接
int creat_conn(struct sockfd_opt *p_so)
{
    struct sockaddr_in client; //客户端ip地址
    int conn_fd;
    socklen_t sin_size;
    sin_size=sizeof(client);
    if ((conn_fd=accept(p_so->fd, (struct sockaddr*)&client, &sin_size))==-1)
    {
        fprintf(stderr,"Accept error:%s\a\n",strerror(errno));
        exit(1);
    }
    fprintf(stdout,"server got connection from %s:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
    if(List.size()+1==FD_SETSIZE) //若连接超过了最大值
    {
        fprintf(stderr,"too many clients!\n");
        return -1;
    }
    
    if ((p_so=(struct sockfd_opt*)malloc(sizeof(sockfd_opt)))==NULL)
    {
        perror("malloc");
        return -1;
    }
    p_so->fd=conn_fd;
   //设置当前套接字选项的回调函数
    p_so->do_task=send_reply;
    List.push_back(p_so);   //将此套接字加入队列
    
   //将此已连接套接字加入到orfds集合中
    FD_SET(conn_fd,&orfds);
    if (conn_fd>maxfd)
        maxfd=conn_fd;
    return 0;
}

//初始化监听套接字选项
int init(int sk)
{
    sockfd_opt *p_so;
    if ((p_so=(struct sockfd_opt*)malloc(sizeof(sockfd_opt)))==NULL)
    {
        perror("malloc");
        return -1;
    }
   //设置监听套接字选项的回调函数
    p_so->do_task=creat_conn;
    p_so->fd=sk;
   //将监听套接字选项加入到链表尾
    List.push_back(p_so);
    //清空orfds
    FD_ZERO(&orfds);
   //将监听套接字加入到orfds集合
    FD_SET(sk,&orfds);
    return 0;
    
}

int main(int argc,char *argv[])
{
    int listen_fd; //用于监听的套接字描述符
    struct sockaddr_in server;
    int port;
    socklen_t optlen;
    port=atoi(argv[1]);
    if((listen_fd=socket(PF_INET,SOCK_STREAM, 0))==-1)
        bail("socket()");
   //设置套接字选项
    int opt;
    optlen=sizeof(opt);
    int ret=setsockopt(listen_fd,SOL_SOCKET, SO_REUSEADDR, &opt, optlen);
    if (ret)
        bail("setsockopt()");
   //服务器监听地址准备
    memset(&server, 0, sizeof(server));
    server.sin_family=PF_INET;
    server.sin_addr.s_addr=htonl(INADDR_ANY);
    server.sin_port=htons(port);
   //绑定服务器到监听套接字
    if((bind(listen_fd, (struct sockaddr*)&server, sizeof(server)))==-1)
        bail("bind()");
    //开始监听
    if(listen(listen_fd,5)==-1)
        bail("listen()");
    maxfd=listen_fd;
    if (init(listen_fd))
        bail("init()");
    printf("server is waiting for acceptance of new client\n");
    struct sockfd_opt *p_so;
    
    for (; ; )
    {
        //恢复rfds为最初设置的orfds,准备下次select的调用
        rfds=orfds;
        //执行select
        int n=select(maxfd+1, &rfds,NULL, NULL,NULL);
       //在返回的n个可读的文件描述符中进行迭代
        for(;n>0;n--)
        {
            
            std::list::iterator it=List.begin();
            while (it!=List.end())
            {
                p_so=*it;
                //检查此套接字文件描述符是否被置位
                if(FD_ISSET(p_so->fd,&rfds))
                {
                    p_so->do_task(p_so);
                }
                ++it;
            }
        }
    }
    return 0;
}


服务端程序没有针对每个客户端连接请求调用fork创建子进程,而是通过select来管理多个连接请求,另外通过引入struct sockfd_opt这个结构体将所有套接字的处理统一对do_task这个函数指针的调用,从而可以通过遍历链表的形式,简化主程序的控制逻辑。但是这样效率不高,后面改进。
//TCP客户端
/*
 
 用法:./client hostname port
 
 说明:本程序使用TCP连接和TCP服务器通信,当连接建立后,向服务器发送如下格式字符串
 
 格式字符串示例:
 (1) %D
 (2) %A %D %H:%M:%S
 (3) %A
 (4) %H:%M:%S
 (5)...
 
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define BUFSIZE 1024
#define backlog 128 //等待队列大小
static void bail(const char *on_what)
{
    fputs(strerror(errno),stderr);
    fputs(": ",stderr);
    fputs(on_what, stderr);
    fputc('\n',stderr);
    exit(1);
}
int main(int argc,char *argv[])
{
    int sockfd;
    char rcvBuf[BUFSIZE];
    char reqBuf[BUFSIZE];
    struct sockaddr_in server;
    struct hostent *host;
    int port;
    long z;
    
    fd_set rfds,orfds;
    int ret,maxfd=-1;
    if ((host=gethostbyname(argv[1]))==NULL)
        bail("gethostbyname()");
    if ((port=atoi(argv[2]))<0)
    {
        fprintf(stderr,"get port error");
        exit(1);
    }
   //创建客户端套接字文件描述符
    if ((sockfd=socket(PF_INET,SOCK_STREAM, 0))==-1)
        bail("socket()");
    
    
   //创建服务器地址
    memset(&server, 0, sizeof(server));
    server.sin_family=PF_INET;
    server.sin_port=htons(port);
    server.sin_addr=*((struct in_addr*)host->h_addr);
   //连接到服务器
    if (connect(sockfd, (struct sockaddr*)&server, sizeof(server))==-1)
        bail("connect()");
    
   //清空文件描述符集合orfds
    FD_ZERO(&orfds);
    
   //标准输入加入orfds集合
    FD_SET(STDIN_FILENO,&orfds);
    
    maxfd=STDIN_FILENO;
    
   //把当前客户端套接字文件描述符sockfd加到orfds
    FD_SET(sockfd,&orfds);
    
    if (sockfd>maxfd)
        maxfd=sockfd;
    
    printf("connected to server:%s\n",inet_ntoa(server.sin_addr));
    for (; ; )
    {
        rfds=orfds; //重新恢复用于检测的可读描述符集
        printf("\nEnter format string(^D or 'quit' to exit):");
        fflush(stdout);
        
       //调用select开始等待可读文件描述符集可用
        
        ret=select(maxfd+1, &rfds,NULL, NULL,NULL);
        if (ret==-1)
        {
            printf("select:%s",strerror(errno));
            break;
        }
        else
        {
            //检测套接字描述符sockfd
            if (FD_ISSET(sockfd,&rfds))
            {
                //读取服务器应答
                if((z=read(sockfd, rcvBuf,sizeof(rcvBuf)))==-1)
                    bail("read");
                //若服务器关闭连接
                if(z==0)
                {
                    fprintf(stderr,"server has closed the socket.\n");
                    fprintf(stderr,"press enter to exit...\n");
                    getchar();
                    break;
                }
                //显示当前日期和时间
                printf("result from %s port %u:\n\t '%s'\n",inet_ntoa(server.sin_addr),ntohs(server.sin_port),rcvBuf);
                
            }
            else if(FD_ISSET(STDIN_FILENO,&rfds))//若标准输入可读
            {
                if(!fgets(reqBuf,BUFSIZE, stdin))
                {
                    //用户按ctrl+D组合键结束输入,eof
                    printf("\n");
                    break;
                }
                z=strlen(reqBuf);
                //去掉用户输入字符串中的换行符
                if(z>0 && reqBuf[--z]=='\n')
                    reqBuf[z]=0;
                if (z==0)
                    continue;
                //用户输入quit退出
                if (!strcasecmp(reqBuf,"QUIT"))
                    break;
                //将格式字符串发送到服务器
                z=write(sockfd, reqBuf, strlen(reqBuf));
                if (z<0)
                    bail("write()");
            }
            
        }
        
    }
    
    close(sockfd);
    return 0;
}


在调用select函数时,timeout参数设置为NULL,表示select函数将永远阻塞,知道标准输入或网络套接字变为可读为止,于是该程序可以同时监视标准输入和网络套接字,当其中一个文件描述符变为可读时,select函数都可以立即返回进行后续操作,例如在启动服务器程序和客户端程序后,如果按ctrl+C退出服务器程序后,客户端会立即返回。



你可能感兴趣的:(【Linux网络编程】)