关于I/O复用技术

I/O复用

如果一个或多个I/O条件满足(例如输入已经准备好被读)我们就能够被通知到,这样的能力被称为I/O复用,是由函数select和epoll支持的。

I/O复用网络应用场合

  1. 当客户端处理多个描述字
  2. 一个客户同时处理多个套接口
  3. 如果一个TCP服务器纪要监听套接口,又要处理连接套接口
  4. 如果一个服务器既要处理TCP又要处理UDP

select

 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);//清空描述字集合

作用:函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或者多个事件发生或经过某些指定的时间后才唤醒进程,提供了及时响应多个套接字的读写事件
参数:

  1. nfds:集合中最大的文件描述符+1
  2. readfds:要检查读事件的容器
  3. writefds:要检查写事件的容器
  4. timeout:超时时间
  5. 返回值:返回触发套接字的个数
  6. 中间的三个参数readset、writeset和exceptset指定要让我们内核测试读、写和异常条件所需的描述字,如果我们对某个条件不感兴趣,这三个参数都可以设置为空指针。
//timeout参数
struct timeval(
                long tv_sec;  //秒
                long tv_usec;//微秒
            );

timeout参数有三种可能

  1. 永远等待下去:仅在只有一个你描述字转备好I/O时才返回,为此,我们将timeout设置为空指针
  2. 等待固定时间:在描述字准备好时返回,但不超过由timeeval结构中指定的秒数和微秒数
  3. 根本不等待:检查描述字后立即返回,这称为轮询。定时器的值必须为0
    注意:fd_set参数:select使用描述字集,它一般输一个整数数组,每个数中的每一位对应一个描述字

使用流程:
关于I/O复用技术
客户端代码不变:

#include < sys/types.h>     
#include < sys/socket.h>
#include < netinet/in.h>    //sockaddr_in
#include < stdio.h>
#include < string.h>

//TCP
int main()
{
    int fd;
    int ret;
    int addrLen;
    char acbuf[20] = "";
    struct sockaddr_in serAddr = {0};
    struct sockaddr_in myAddr = {0};

    //1.socket();
    fd = socket(PF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    //2.连接connect() 服务器的地址
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(1234);
    serAddr.sin_addr.s_addr = inet_addr("192.168.159.5");
    ret = connect(fd,(struct sockaddr *)&serAddr,sizeof(struct sockaddr_in));
    if(ret == -1)
    {
        perror("connect");
        return -1;
    }

    //获取自己的地址
    addrLen = sizeof(struct sockaddr_in);
    ret = getsockname(fd,(struct sockaddr *)&myAddr,&addrLen);
    if(ret == -1)
    {
        perror("getsockname");
        return -1;
    }
    printf("client---ip: %s , port: %d\n",\
                inet_ntoa(myAddr.sin_addr),ntohs(myAddr.sin_port));
    //3.通信
    while(1)
    {
        printf("send: ");
        fflush(stdout);
        scanf("%s",acbuf);
        if(strcmp(acbuf,"exit") == 0)
        {
            break;
        }
        write(fd,acbuf,strlen(acbuf));
    }

    //4.close()
    close(fd);
    return 0;
}

服务端代码如下:

 #include < sys/types.h>     
    #include < sys/socket.h>
    #include < netinet/in.h>    //sockaddr_in
    #include < stdio.h>
    #include < string.h>
    #include < signal.h>
    #include < sys/select.h>
    #include < unistd.h>
    #include < sys/time.h>
    //TCP
    int main()
    {
        int fd;
        int clientfd;
        int ret;
        pid_t pid;

        int i;
        int maxfd;          //当前最大套接字
        int nEvent;
        fd_set set = {0};   //监听集合
        fd_set oldset = {0};    //存放所有要监听的文件描述符
        struct timeval time = {0};

        int reuse = 0;
        char acbuf[20] = "";
        char client_addr[100] = "";
        struct sockaddr_in addr = {0};  //自己的地址
        struct sockaddr_in clientAddr = {0};    //连上的客户端的地址
        int addrLen = sizeof(struct sockaddr_in);

        signal(SIGCHLD,SIG_IGN);

        //1.socket()
        fd = socket(PF_INET,SOCK_STREAM,0);
        if(fd == -1)
        {
            perror("socket");
            return -1;
        }

        //会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
        //由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
            {
            perror("setsockopet error\n");
            return -1;
            }

        //2.bind()
        addr.sin_family = AF_INET;
        addr.sin_port = htons(1234);
        addr.sin_addr.s_addr = inet_addr("192.168.159.5");
        ret = bind(fd,(struct sockaddr *)&addr,addrLen);
        if(ret == -1)
        {
            perror("bind");
            return -1;
        }

        //3.listen()
        ret = listen(fd,10);
        if(ret == -1)
        {
            perror("listen");
            return -1;
        }

        //创建监听集合
        FD_ZERO(&oldset);
        FD_SET(fd,&oldset);
        //maxfdp1:当前等待的最大套接字。比如:当前fd的值=3,则最大的套接字就是3
        //所以每当有客户端连接进来,就比较一下文件描述符
        maxfd = fd;
        //select
        //select之前,set放的是所有要监听的文件描述符;{3,4,5}
        //select之后,set只剩下有发生事件的文件描述符。{3}

        while(1)
        {
            set = oldset;
            printf("before accept.\n");
            time.tv_sec = 5;
            nEvent = select(maxfd + 1,&set,NULL,NULL,&time);    //返回文件描述符的个数(即事件的个数)
            printf("after accept.%d\n",nEvent);
            if(nEvent == -1)
            {
                perror("select");
                return -1;
            }
            else if(nEvent == 0)    //超时
            {
                printf("time out");
                return 1;
            }
            else
            {           
                //有事件发生
                //判断是否是客户端产生的事件
                for(i = 0 ; i <= maxfd ; i++)
                {
                    if(FD_ISSET(i,&set))
                    {
                        if(i == fd)
                        {
                            clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
                            FD_SET(clientfd,&oldset);
                            printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
                            if(clientfd > maxfd)
                            {
                                maxfd = clientfd;
                            }
                        }
                        else
                        {
                            memset(acbuf,0,20);
                            if(read(i,acbuf,20) == 0) //客户端退出
                            {
                                close(i);
                                //还要从集合里删除
                                FD_CLR(i,&oldset);
                            }
                            else
                                printf("receive: %s\n",acbuf);
                        }
                    }
                }
            }
        }
        return 0;
    }

epoll

epoll用到的函数有以下几个:

 #include <sys/epoll.h>
       int epoll_create(int size);//创建epoll
       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//操作函数
       int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);   

使用流程:
关于I/O复用技术
服务端代码如下:

#include <sys/types.h>     
        #include <sys/socket.h>
        #include <netinet/in.h> //sockaddr_in
        #include <stdio.h>
        #include <string.h>
        #include <signal.h>
        #include <sys/epoll.h>

        //epoll
        //epoll_wait() epoll_creat() epoll_ctl()

        //TCP
        int main()
        {
            int fd;
            int clientfd;
            int ret;
            pid_t pid;

            int i;
            int epfd;
            int nEvent;
            struct epoll_event event = {0};
            struct epoll_event rtl_events[20] = {0};    //事件结果集

            int reuse = 0;
            char acbuf[20] = "";
            char client_addr[100] = "";
            struct sockaddr_in addr = {0};  //自己的地址
            struct sockaddr_in clientAddr = {0};    //连上的客户端的地址
            int addrLen = sizeof(struct sockaddr_in);

            signal(SIGCHLD,SIG_IGN);

            //1.socket()
            fd = socket(PF_INET,SOCK_STREAM,0);
            if(fd == -1)
            {
                perror("socket");
                return -1;
            }

            //会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
            //由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
            if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
                {
                perror("setsockopet error\n");
                return -1;
                }

            //2.bind()
            addr.sin_family = AF_INET;
            addr.sin_port = htons(1234);
            addr.sin_addr.s_addr = inet_addr("192.168.159.5");
            ret = bind(fd,(struct sockaddr *)&addr,addrLen);
            if(ret == -1)
            {
                perror("bind");
                return -1;
            }

            //3.listen()
            ret = listen(fd,10);
            if(ret == -1)
            {
                perror("listen");
                return -1;
            }

            epfd = epoll_create(1000);  //同时监听的文件描述符
            event.data.fd = fd;
            event.events = EPOLLIN;  //读
            epoll_ctl(epfd,EPOLL_CTL_ADD,fd, &event);
            while(1)
            {
        //      nEvent = epoll_wait(epfd,rtl_events,20,-1);  //-1:阻塞    0:非阻塞
                nEvent = epoll_wait(epfd,rtl_events,20,5000);
                if(nEvent == -1)
                {
                    perror("epoll_wait");
                    return -1;
                }
                else if(nEvent == 0)
                {
                    printf("time out.");
                }
                else
                {
                    //有事件发生,立即处理
                    for(i = 0; i < nEvent;i++)
                    {
                        //如果是 服务器fd
                        if( rtl_events[i].data.fd == fd )
                        {
                            clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
                            //添加
                            event.data.fd = clientfd;
                            event.events = EPOLLIN;  //读
                            epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&event);
                            printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
                        }
                        else
                        {
                            //否则 客户端fd 
                            memset(acbuf,0,20);
                            ret = read(rtl_events[i].data.fd,acbuf,20);
                            printf("%d\n",ret);
                            if( ret == 0) //客户端退出
                            {
                                close(rtl_events[i].data.fd);
                                //从集合里删除
                                epoll_ctl(epfd,EPOLL_CTL_DEL,rtl_events[i].data.fd,NULL);
                            }
                            else
                                printf("receive: %s\n",acbuf);
                        }

                    }
                }
            }

            return 0;
        }
上一篇:epoll总结


下一篇:I/O多路复用中的水平触发和边缘触发