zoukankan      html  css  js  c++  java
  • Linux下select的用法--实现一个简单的回射服务器程序

    1、先看man手册

    SYNOPSIS
           /* According to POSIX.1-2001 */
           #include <sys/select.h>
           /* According to earlier standards */
           #include <sys/time.h>
           #include <sys/types.h>
           #include <unistd.h>
           int select(int nfds, fd_set *readfds, fd_set *writefds,
                      fd_set *exceptfds, struct timeval *timeout);
           void FD_CLR(int fd, fd_set *set);
           int  FD_ISSET(int fd, fd_set *set);
           void FD_SET(int fd, fd_set *set);
           void FD_ZERO(fd_set *set);
        
        // 关于第5个参数
            struct timeval {
                   long    tv_sec;         /* seconds */
                   long    tv_usec;        /* microseconds */
               };

    2. 函数说明:可以同时监控多个文件描述符是否发生了读写或者异常。(有点像windows下的waitformultipleobjects,可以同时等待多个事件)
    参数说明:
    1)nfds:要监控的文件描述符的最大值加1,这个值不能错。
    2)readfds:指向fd_set的指针。这是一个集合,专门用于监视读取数据的。所有需要监控读取数据的描述符都需要放进这个集合中。比如你需要监控4描述符的读取数据,就把4放进这个集合之中。
    3)writefds:同上,这里是专门监视写的集合
    4)exceptfds:同上,这里是专门监视异常的集合
    5)timeout:超时。指向的timeval 结构体。
    如果参数设为NULL,则select是阻塞的。
    如果不为空,则表示超时时间(当结构体里面的成员都设为0时,表示不阻塞,立即返回)。


    重要说明:
    第2-4个参数是输入输出参数,select返回时会将哪个文件描述符放到这个集合中。
    比如我们监控了fd=5的描述符的读取数据操作,当发生了读取操作时,select则会返回,通过第二个参数可以获取5发生了读操作。用FD_ISSET();实现。 
    返回值:
    <0:表示出错
    >0:等到了我们需要的事件
    =0:表示超时了

    3.fd_set说明
    这是一个文件描述符的集合。
    有几个宏可以对这个集合进行操作。
    void FD_CLR(int fd, fd_set *set);  // 将集合中指定的fd移除
    int FD_ISSET(int fd, fd_set *set); // 判断fd是否在set集合中
    void FD_SET(int fd, fd_set *set); // 将fd插入集合中
    void FD_ZERO(fd_set *set); // 将集合里面的数据清空

    4.实例1:这个例子用来监控标准输入:

    #include<sys/select.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include<stdio.h>
    int fun()
    {
        int ret = 0, nready = 0;
        fd_set rset;
        FD_ZERO(&rset); // 清空操作
        int fd_stdin = fileno(stdin);
        int maxfd = fd_stdin;
        char readbuf[1024] = {0};
        struct timeval tv;
        tv.tv_sec = 10;
        tv.tv_usec = 0;
        while(1)
        {
            FD_SET(fd_stdin, &rset); // 需要重新设置,因为select每次返回会改变里面的值
            //nready = select(maxfd+1, &rset, NULL, NULL, &tv);// 有超时
            nready = select(maxfd+1, &rset, NULL, NULL, NULL);
            if(nready <= 0)
            {
                //printf();
                break;
            }
            if(FD_ISSET(fd_stdin, &rset)) // rset也是输出参数,这里判断是否是fd_stdin发生了读操作
            {
                int i = read(fd_stdin, readbuf, sizeof(readbuf));
                //printf("recv STDIN read:%d
    ", i);
                if(i > 0)
                {
                    printf("STDIN buf:%s
    ", readbuf);
                }
                memset(readbuf, 0, sizeof(readbuf));
            }
        }
        
        printf("fun() ---, nready = %d
    ", nready);
    }
    int main()
    {
        printf("stdin:%d, stdout:%d, stderr:%d
    ", fileno(stdin), fileno(stdout), fileno(stderr)); // 这个是其他知识    
        fun();
    }

    5.实例2(回射服务器):
    我实现了一个socket服务器和客户端程序。server可以直接在终端中发送数据给client。client可以在终端中显示,也可以用标准输入直接发送回去。
    所以在client的程序中,用select同时监控server的socket和标准输入,当有任意一个发生读取数据时都进行处理。
    server.c 接收client的连接,并且收到client的数据原封不动的发送回去。可以接收多个client的连接。能同时处理多个client发送的数据。

    先看server.c:

    #include<sys/types.h>
    #include<sys/socket.h>
    #include<sys/select.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<stdlib.h>
    #include<stdio.h>
    #include<string.h>
    #include<errno.h>
    
    #define CLIENTCOUNT 100
    int main(int argc, char **argv)
    {
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if(listenfd < 0)
        {
            perror("socket");
            return -1;
        }   
        unsigned short sport = 8080;
        if(argc == 2)
        {
            sport = atoi(argv[1]);
        }
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        printf("port = %d
    ", sport);
        addr.sin_port = htons(sport);
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");  
        if(bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
        {
            perror("bind");
            return -2;
        }
        if(listen(listenfd, 20) < 0)
        {
            perror("listen");
            return -3;
        }
        struct sockaddr_in connaddr;
        int len = sizeof(connaddr);
        
        int i = 0, ret = 0;
        int client[CLIENTCOUNT];
        for(i = 0; i<CLIENTCOUNT; i++)  // 将连接上client的描述符放到一个数组里,这里对数组进行初始化。-1表示空闲
            client[i] = -1;
        fd_set rset;
        fd_set allset;
        FD_ZERO(&rset);    
        FD_ZERO(&allset);    
        FD_SET(listenfd, &allset);
        int maxfd = listenfd;
        int nready = 0;
        char buf[1024] = {0};
        /* 
          这里很巧妙的是用到了两个fd_set。因为rset会发生改变。我们管理处理好allset就好了。
          每次循环开始都将allset赋值给rset。
          当有client连接时,加入到allset中。有client关闭时就从allset中移除 
        */
        while(1)
        {
            rset = allset;
            nready = select(maxfd+1, &rset, NULL, NULL, NULL);
            if(nready == -1)
            {
                perror("select");
                    return -3;
            }
            if(nready == 0)
                continue;
            if(FD_ISSET(listenfd, &rset)) // 这里用来监听 监听套接字,就是listenfd
            {
                int conn = accept(listenfd, (struct sockaddr*)&connaddr, &len);
                if(conn < 0)
                {
                    perror("accept");
                    return -4;
                }
                char strip[64] = {0};
                char *ip = inet_ntoa(connaddr.sin_addr);
                strcpy(strip, ip);
                printf("new client connect, conn:%d,ip:%s, port:%d
    ", conn, strip,ntohs(connaddr.sin_port));
                
                FD_SET(conn, &allset); // 将连接上的套接字加入到allset中去
                if(maxfd < conn) // update maxfd
                    maxfd = conn;    
    
                int i = 0;
                for(i = 0; i<CLIENTCOUNT; i++)
                {
                    if(client[i] == -1)
                    {
                        client[i] = conn; // 将conn存进client数组中取去
                        break;
                    }
                }
                if(i == CLIENTCOUNT)
                {
                    printf("to many client connect
    ");
                    exit(0);
                }
                if(--nready <= 0)
                    continue;
            }
    
            for(i = 0; i < CLIENTCOUNT; i++) // 这里处理客户端发送过来的数据,逐个处理所有的套接字
            {
                if(client[i] == -1)
                    continue;
                if(FD_ISSET(client[i], &rset)) 
                {
                    ret = read(client[i], buf, sizeof(buf));
                    if(ret == -1)
                    {
                        perror("read");
                        return -4;
                    }
                    else if(ret == 0) // 表示client关闭了
                    {
                        printf("client close remove:%d
    ", client[i]);
                        FD_CLR(client[i], &allset); // 在allset中移除它
                        close(client[i]); // 同时也要关闭
                        client[i] = -1;  // 要在这里移除,这步非常重要。
                    }
                    
                    // fputs(buf, stdout);
                    printf("client%d:%s
    ", client[i], buf);
                    write(client[i], buf, sizeof(buf)); // 发送数据给client
                    memset(buf, 0, sizeof(buf));
                    if(--nready <= 0)
                        continue;
                }
            }        
        }
    
        close(listenfd);
        return 0;
    }

    关于while(1)循环里面的理解:
    1)先将listenfd加入监听队列
    2)如果有client连接,就将连接套接字加入allset
    3)然后重新监听rset(allset会赋值给它)。这样就相当于监听了client和listenfd
    4)如果select又返回,若是listenfd,则重复上面的步骤
    5)如果是client发来了数据,read返回不为0则打印出来,再发送回去。若read返回0,则表示client关闭了。就把对应文件描述符从allset中移除。同时关闭它。也要更新那个数组client。如果不更新for循环会出错

    接着来看客户端:
    client.c:

    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<sys/select.h>
    
    #include<stdlib.h>
    #include<stdio.h>
    #include<string.h>
    
    void select_test(int conn)
    {
        int ret = 0;
        fd_set rset;
        FD_ZERO(&rset);
    
        int nready;
        int maxfd = conn;
        int fd_stdin = fileno(stdin);
        if(fd_stdin > maxfd)
        {
            maxfd = fd_stdin;
        }
    
        int len = 0;
        char readbuf[1024] = {0};
        char writebuf[1024] = {0};
        while(1)
        {
            FD_ZERO(&rset);
            FD_SET(fd_stdin, &rset);
            FD_SET(conn, &rset);
            nready = select(maxfd+1, &rset, NULL, NULL, NULL); // 同时监听标准输入和服务器。
            if(nready == -1)
            {
                perror("select");
                exit(0);
            }
            else if(nready == 0)
            {
                continue;    
            }
    
            if(FD_ISSET(conn, &rset)) // 服务器来了数据
            {
                ret = read(conn, readbuf, sizeof(readbuf));
                if(ret == 0)
                {
                    printf("server close1
    ");
                    break;
                }
                else if(-1 == ret)
                {
                    perror("read1");
                    break;
                }    
    
                fputs(readbuf, stdout);
                memset(readbuf, 0, sizeof(readbuf));
            }    
            
            if(FD_ISSET(fd_stdin, &rset)) // 标准输入来了数据就发送给server
            {    
                read(fd_stdin, writebuf, sizeof(writebuf));        
                len = strlen(writebuf);
                ret = write(conn, writebuf, len);
                if(ret == 0)
                {
                    printf("server close3
    ");
                    break;
                }
                else if(-1 == ret)
                {
                    perror("write");
                    break;
                }
    
                memset(writebuf, 0, sizeof(writebuf));    
            }
        }
    }
    
    int sockfd = 0;
    int main(int argc, char **argv)
    {
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0)
        {
            perror("socket");
            return -1;
        }
            
        unsigned short sport = 8080;
        if(argc == 2)
        {
            sport = atoi(argv[1]);
        }
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        printf("port = %d
    ", sport);
        addr.sin_port = htons(sport);
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
        {
            perror("connect");
            return -2;
        }
    
        struct sockaddr_in addr2;
        socklen_t len = sizeof(addr2);
        if(getpeername(sockfd, (struct sockaddr*)&addr2, &len) < 0)
        {
            perror("getsockname");
            return -3;
        }
    
        printf("Server: port:%d, ip:%s
    ", ntohs(addr2.sin_port), inet_ntoa(addr2.sin_addr));
    
        select_test(sockfd);
    
        close(sockfd);
        return 0;
    }

    运行:

    先运行服务器程序,再启动客户端程序,通过标准输入给server发送数据。server会回射回来。

    这个服务器可以同时处理多个客户端的数据。

    6. 用select可以实现在单进程中同时处理多个文件描述符的事件。

    7.读、写、异常事件发生的条件

    可读:
    1)套接口缓冲区有数据可读
    2)连接的读一半关闭,即接收到FIN段,读操作返回0
    3)如果是监听套接口,已完成连接队列不为空时
    4)套接口上发生了一个错误待处理,错误可以同getsetopt指定SO_ERROR选项来获取

    可写:
    1)套接口发送缓冲区有空间容纳数据
    2)连接的写一半关闭,即接收到RET段,再次调用write操作
    3)套接口上发生了一个错误待处理,错误可以同getsetopt指定SO_ERROR选项来获取

    异常:
    1)套接口存在带外数据

  • 相关阅读:
    CSS练习
    关于进度模型和进度计划
    信息管理系统/记录管理系统/配置管理系统
    分析技术在PMP中的应用
    渐进明细的几个点
    android手机内的通讯录数据库
    用FileExplorer查看android手机中的数据库
    VCard介绍
    org.apache.http.client.methods.HttpGet 转到定义找不到源代码
    Android : Your APK does not seem to be designed for tablets.
  • 原文地址:https://www.cnblogs.com/xcywt/p/8087677.html
Copyright © 2011-2022 走看看