zoukankan      html  css  js  c++  java
  • Linux网络编程(六)

    网络编程中,使用多路IO复用的典型场合:
    1.当客户处理多个描述字时(交互式输入以及网络接口),必须使用IO复用。
    2.一个客户同时处理多个套接口。
    3.一个tcp服务程序既要处理监听套接口,又要处理连接套接口,一般需要用到IO复用。
    4.如果一个服务器既要处理TCP,又要处理UDP,一般也需要用到IO复用。
    5.如果一个服务器要处理多个服务或者多个协议,一般需要用到IO复用。
    linux提供了select、poll、epoll等方法来实现IO复用,三者的原型如下:
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

     函数参数说明:

         select            slect的第一个参数nfdsfdset集合中最大描述符值加1fdset是一个位数组,其大小限制为

    __FD_SETSIZE1024),位数组的每一位代表其对应的描述符是否需要被检查。

    select的第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出

    数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需重新初始化

    fdset。

    timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。

         poll     pollselect不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的

    events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。

    poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的

    每个描述符进行poll,相比处理fdset来说,poll效率更高。

     

    poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。

         epoll epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait

    检查事件,epoll_wait的第二个参数用于存放结果。

     

    epollselectpoll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会

    与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发

    生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。

     

    epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll

    select那样进行轮询检查。


    select、poll、epoll比较:

    select

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1.单个进程可监视的fd数量被限制。

    2.需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    3.对socket进行扫描时是线性扫描。

    poll

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

    大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

    poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    epoll

    epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。

    在前面说到的复制问题上,epoll使用mmap减少复制开销。

    还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

    进程所能打开的最大连接数:

    select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
    poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
    epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

    FD剧增后带来的IO效率问题:

    select 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
    poll 同上
    epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

    消息传递方式

    select 内核需要将消息传递到用户空间,都需要内核拷贝动作
    poll 同上
    epoll epoll通过内核和用户空间共享一块内存来实现的。

    Linux网络编程(五)中用select实现了多路IO复用,如果用poll来实现的话。代码如下:

    服务器端功能:

    使用单进程为多个客户端服务,接收到客户端发来的一条消息后,将该消息原样返回给客户端。首先,建立一个监听套接字来接收来自客户端的连接。每当接收到一个连接后,将该连接套接字加入客户端套接字数组,通过poll实现多路复用。每当poll返回时,检查pollfd数组的状态。并进行相应操作,如果是新的连接到来,则将新的连接套接字登记到pollfd数组,如果是已有客户端连接套接字变为可读,则对相应客户端进行响应。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/types.h>
    #include <netdb.h>
    #include <errno.h>
    #include <poll.h>
    #define OPEN_MAX 1113
    #define SERV_PORT 2048
    #define LISTENQ  32
    #define MAXLINE 1024
    int
    main(int argc, char **argv)
    {
        int                    i, maxi,listenfd, connfd, sockfd;
        int                    nready;
        ssize_t                n;
        char                buf[MAXLINE];
        socklen_t            clilen;
        struct sockaddr_in    cliaddr, servaddr;
        struct pollfd client[OPEN_MAX];
        if((listenfd = socket(AF_INET, SOCK_STREAM,0))==-1){
            fprintf(stderr,"Socket error:%s
    a",strerror(errno));
            exit(1);
        }
        /* 服务器端填充 sockaddr结构*/ 
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family      = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port        = htons(SERV_PORT);
    
         /* 捆绑listenfd描述符  */ 
        if(bind(listenfd,(struct sockaddr*)(&servaddr),sizeof(struct sockaddr))==-1){
            fprintf(stderr,"Bind error:%s
    a",strerror(errno));
            exit(1);
        }
       /* 监听listenfd描述符*/
        if(listen(listenfd,LISTENQ)==-1){
            fprintf(stderr,"Listen error:%s
    a",strerror(errno));
            exit(1);
        }
        client[0].fd=listenfd;
        client[0].events=POLLRDNORM;/*等待普通数据可读*/
        maxi = 0;                    /*client数组索引*/
        for (i = 1; i < FD_SETSIZE; i++)
            client[i].fd = -1;            /* -1代表未使用*/
    
        for ( ; ; ) {
            if((nready = poll(client, maxi+1, -1))<0){/*永远等待*/
                fprintf(stderr,"poll Error
    ");
                exit(1);
            }
            if (client[0].revents & POLLRDNORM){/*有新的客户端连接到来*/
                clilen = sizeof(cliaddr);
                if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr,&clilen))<0){
                    fprintf(stderr,"accept Error
    ");
                    continue;
                }
                char des[sizeof(cliaddr)];
                inet_ntop(AF_INET, &cliaddr.sin_addr, des, sizeof(cliaddr));
                printf("new client: %s, port %d
    ",des,ntohs(cliaddr.sin_port));
                for (i = 0; i < OPEN_MAX; i++)
                    if (client[i].fd < 0) {
                        client[i].fd = connfd;    /*保存新的连接套接字*/
                        break;
                    }
                if (i == OPEN_MAX){
                    fprintf(stderr,"too many clients");
                    exit(1);
                }
                client[i].events=POLLRDNORM;    /*设置新套接字的普通数据可读事件*/    
                if (i > maxi)
                    maxi = i;                /*当前client数组最大下标值*/
                if (--nready <= 0)
                    continue;                /*可读的套接字全部处理完了*/
            }
            for (i = 1; i <= maxi; i++) {    /*检查所有已经连接的客户端是否有数据可读*/
                if ( (sockfd = client[i].fd) < 0)
                    continue;
                if (client[i].revents & (POLLRDNORM|POLLERR)){
                    if ((n = read(sockfd, buf, MAXLINE)) == 0){/*客户端主动断开了连接*/
                        close(sockfd);
                        client[i].fd = -1;/*设置为-1,表示未使用*/
                    } else if(n<0){/*小于0,是出错的节奏*/
                        if(errno==ECONNRESET){/*客户端发送了reset分节*/
                            close(sockfd);
                            client[i].fd = -1;
                        }
                        else{
                            fprintf(stderr,"read error");
                            exit(1);
                        }
                    }
                    else
                        write(sockfd, buf, n);
                    if (--nready <= 0)
                        break;                /*可读的套接字全部处理完了*/
                }
            }
        }
    }

    因为客户端代码只需要同时处理来标准输入是否可读以及socket是否可读两路IO,因此仍然使用select时的客户端程序。

    本程序(客户端)功能:
    1.向服务器发起连接请求,并从标准输入stdin获取字符串,将字符串发往服务器。
    2.从服务器中接收字符串,并将接收到的字符串输出到标准输出stdout.
    =========================================================================
    问题:由于既要从标准输入获取数据,又要从连接套接字中读取服务器发来的数据。
    为避免当套接字上发生了某些事件时,程序阻塞于fgets()调用,由于这两只
    需要处理两路IO,因此程序客户端程序仍然使用select实现多路IO复用,或等
    待标准输入,或等待套接口可读。这样一来,若服务器进程终止,客户端能马上得到通知。
    =========================================================================
    对于客户端套接口,需要处理以下三种情况:
    1.服务器端发送了数据过来,套接口变为可读,且read返回值大于0
    2.服务器端发送了一个FIN(服务器进程终止),套接口变为可读,且read返回值等于0
    3.服务器端发送了一个RST(服务器进程崩溃,且重新启动,此时服务器程序已经不认
    识之前建立好了的连接,所以发送一个RST给客户端),套接口变为可读,且read返回-1
    错误码存放在了errno

    代码如下:

    //使用多路复用select的客户端程序
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <sys/types.h>
    #include <netdb.h>
    #define SERV_PORT 2048 
    #define MAXLINE 1024
    #define max(x,y) (x)>(y) ? (x):(y)
    void str_cli(FILE *fp, int sockfd);
    int
    main(int argc, char **argv)
     {
        int     sockfd;
        struct sockaddr_in servaddr;
        if (argc != 2){
            fprintf(stderr,"usage: tcpcli <IPaddress>
    a");
            exit(0);
        }
        if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
            fprintf(stderr,"Socket error:%s
    a",strerror(errno));
            exit(1);
        }
        /*客户程序填充服务端的资料*/
          memset(&servaddr,0,sizeof(servaddr));
          servaddr.sin_family=AF_INET;
          servaddr.sin_port=htons(SERV_PORT);
          if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
                fprintf(stderr,"inet_pton Error:%sa
    ",strerror(errno));
                exit(1);
          }
           /* 客户程序发起连接请求*/ 
          if(connect(sockfd,(struct sockaddr *)(&servaddr),sizeof(struct sockaddr))==-1){
                fprintf(stderr,"connect Error:%sa
    ",strerror(errno));
                exit(1);
          }
         str_cli(stdin, sockfd);     /*重点工作都在此函数*/
         exit(0);
     }
    
    void
    str_cli(FILE *fp, int sockfd)
    {
        int            maxfdp1, stdineof;
        fd_set        rset;/*用于存放可读文件描述符集合*/
        char        buf[MAXLINE];
        int        n;
        stdineof = 0;/*用于标识是否结束了标准输入*/
        FD_ZERO(&rset);
        while(1){
            if (stdineof == 0)
                FD_SET(fileno(fp), &rset);
            FD_SET(sockfd, &rset);
            maxfdp1 = max(fileno(fp), sockfd) + 1;
            if(select(maxfdp1, &rset, NULL, NULL, NULL)<0){/*阻塞,直到有数据可读或出错*/
                fprintf(stderr,"select Error
    ");
                exit(1);
            }
            if (FD_ISSET(sockfd, &rset)) {    /*套接口有数据可读*/
                if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
                    if (stdineof == 1)
                        return;        /*标准输入正常结束*/
                    else
                        fprintf(stderr,"str_cli: server terminated prematurely");
                }
                write(fileno(stdout), buf, n);/*将收到的数据写到标准输出*/
            }
            if (FD_ISSET(fileno(fp), &rset)) {  /*标准输入可读*/
                if ( (n = read(fileno(fp), buf, MAXLINE)) == 0) {
                    stdineof = 1;
                    /*向服务器发送FIN,告诉它,后续已经没有数据发送了,但仍为读而开放套接口,注意这里使用了shutdown,而不是close*/
                    if(-1==shutdown(sockfd, SHUT_WR)){
                        fprintf(stderr,"shutdown Error
    ");
                    }
                    FD_CLR(fileno(fp), &rset);
                    continue;
                }
                write(sockfd, buf, n);
            }
        }
    }

    关于close与shutdown的区别:
    close()将描述字的访问计数减1,仅在访问计数为0时才关闭套接字。
    shutdown()可以激发TCP的正常连接终止系列,而不管访问计数。
    close()终止了数据传输的两个方向:读、写。

  • 相关阅读:
    [算法] 堆栈
    [刷题] PTA 02-线性结构3 Reversing Linked List
    java IO流 (八) RandomAccessFile的使用
    java IO流 (七) 对象流的使用
    java IO流 (六) 其它的流的使用
    java IO流 (五) 转换流的使用 以及编码集
    java IO流 (四) 缓冲流的使用
    java IO流 (三) 节点流(或文件流)
    java IO流 (二) IO流概述
    java IO流 (一) File类的使用
  • 原文地址:https://www.cnblogs.com/jjdiaries/p/3422343.html
Copyright © 2011-2022 走看看