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\n\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\n\a",strerror(errno)); exit(1); } /* 监听listenfd描述符*/
    if(listen(listenfd,LISTENQ)==-1){ fprintf(stderr,"Listen error:%s\n\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\n"); exit(1); } if (client[0].revents & POLLRDNORM){/*有新的客户端连接到来*/ clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr,&clilen))<0){ fprintf(stderr,"accept Error\n"); continue; } char des[sizeof(cliaddr)]; inet_ntop(AF_INET, &cliaddr.sin_addr, des, sizeof(cliaddr)); printf("new client: %s, port %d\n",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>\n\a"); exit(0); } if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){ fprintf(stderr,"Socket error:%s\n\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:%s\a\n",strerror(errno)); exit(1); } /* 客户程序发起连接请求*/ 
      if(connect(sockfd,(struct sockaddr *)(&servaddr),sizeof(struct sockaddr))==-1){ fprintf(stderr,"connect Error:%s\a\n",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\n"); 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\n"); } FD_CLR(fileno(fp), &rset); continue; } write(sockfd, buf, n); } } }

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

你可能感兴趣的:(linux)