Unix系统I/O多路复用技术—select、poll、epoll总结

什么是多I/O的多路复用?

考虑一下这个问题,在一个程序中对两个fd进行阻塞读写,那么对任何一个fd进行阻塞读写,都会导致另一个fd没法处理,比如就算其已经有了数据也不能进行读;若采用非阻塞轮询方式进行处理,这种方法会导致cpu的负荷很大,cpu做了很多无用的轮询,若采用多进程、多线程方式开辟独立的线程分别操作一个fd,则进程、线程间的同步增加了代码的复杂性。

I/O 多路复用机制,单个线程通过记录跟踪每一个Socket (I/O流)的状态来同时管理多个 I/O 流。 发明它的原因,是尽量多的提高服务器的吞吐能力。

例如:nginx使用epoll接收请求的过程:ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

一、select机制

1. select函数介绍

在Unix中,select函数可以实现I/O的多路复用,传向select函数的参数告诉内核:

(1) 我们所关心的文件描述符;
(2) 对于每个描述符我们所关心的状态(是否可读,是否可写,或者是否这个描述符出现异常)
(3) 希望等待多长时间(可以永远阻塞,等待一个固定时间,或者不阻塞)

从select返回时,内核告诉我们

(1) 已经准备好的描述符的数量;
(2) 哪一个描述符已经准备好(读、写或异常条件)
使用这种返回值,我们就可以调用相应的I/O函数(一般为read或write),并确知该函数不会阻塞。

#include<sys/select.h>
int select(int maxfdpl, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *tvptr);
//正常返回准备就绪的描述符数目,超时返回 0,出错返回 -1

(1) 参数1:maxfdpl

是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。

考虑所有 3 个描述符集,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是第一个参数值。 也可将第一个参数设置为 FD_SETSIZE,这是 <sys/select.h> 中的一个常量,它指定最大描述符数(经常是 1024),但是对大多数应用程序而言,此值太大了。

(2) 参数2~4,readfds(读)、writefds(写) 和 exceptfds(异常) 是指向描述符集的指针。

这 3 个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个 fd_set 数据类型中。这种数据类型是用位图实现的,它为每一个可能的描述符分配了一位。我们可以认为它只是一个由很多二进制位构成的数组(根据maxfdpl进行遍历,而不是全部遍历,那样浪费时间)。

对于 fd_set 数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用以下 4 个宏。

FD_ZERO(fd_set *fdset);         //清除fdset中的所有位
FD_SET(int fd, fd_set *fdset);  //将fdset描述符集中的指定fd位置1
FD_CLR(int fd, fd_set *fdset);  //将fdset描述符集中的指定fd位清0
FD_ISSET(int fd, fd_set *fdset); //测试fdset描述符集中的指定fd位的值

在声明了一个描述符集之后,必须用 FD_ZERO 将这个描述符集置为 0,然后在其中设置我们关心的各个描述符的位具体操作如下所示:

fd_set rset;
int fd;
FD_ZERO (&rset);
FD_SET (fd, &rset);
FD_SET (STDIN_FILEND, &rset);

从 select 返回时,用 FD_ISSET 测试该集中的一个给定位是否仍为1,注意:select返回的时候,没有事件发生的fd被置零。

if (FD_ISSET (fd, &rset))
{
	....
}

select 的中间 3 个参数(指向描述符集的指针)中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。如果所有 3 个指针都是 NULL,则 select 提供了比 sleep 更精确的定时器。

(3) 参数 tvptr,它指定愿意等待的时间长度,单位为秒和微秒。

结构体 timeval 定义如下:

struct timeval{      
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    }

tvptr 的取值有以下 3 中情况:

tvptr == NULL

永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则 select 返回 -1,errno 设置为 EINTR。 

tvptr->tv_sec == 0 && tvptr->tv_usec == 0

根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞 select 函数的方法。

tvptr->tv_sec != 0 || tvptr->tv_usec != 0

等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时到期时还没有一个描述符准备好,则返回值是 0。(如果系统不提供微秒级的精度,则 tvptr->tv_usec 值取整到最近的支持值)与第一种情况一样,这种等待可被捕捉到的信号中断。

(4) 函数返回

select 有 3 个可能的返回值。

(i). 返回值 -1表示出错。

这是可能发生的,例如,在所指定的描述符一个都没准备好时捕捉到一个信号。在此情况下,一个描述符集都不修改。

错误码有:

EBADF  An invalid file descriptor was given in one of the sets.  (Perhaps a file descriptor that was already closed, or one on which an error has occurred.)
 
EINTR  A signal was caught; see signal(7).
EINVAL nfds is negative or the value contained within timeout is invalid.
ENOMEM unable to allocate memory for internal tables.

(ii). 返回值 0 表示返回时没有描述符准备好。

若指定的描述符一个都没准备好,指定的时间就过去了,那么就会发生这种情况。此时,所有描述符集都会置 0。

(iii).一个正返回值说明了已经准备好的描述符数。

该值是 3 个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3 个描述符集中仍旧打开的位是对应于已准备好的描述符。

对于“准备好”的含义要作一些更具体的说明:

(1) 若对读集(readfds)中的一个描述符进行的 read 操作不会阻塞,则认为此描述符是准备好的。

(2) 若对写集(writefds)中的一个描述符进行的 write 操作不会阻塞,则认为此描述符是准备好的。

(3) 若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。(现在,异常条件包括:在网络连接上到达指定波特率外的数据,或者在处于数据包方式的伪终端上发生了某些条件)。

对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

2. select函数使用注意事项:

(1) 可监控的文件描述符个数取决与sizeof(fd_set)的值;

(2) 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3) 由2可知,select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生,必须要使用FD_ISSET查询一遍,否则select执行不正常)。

(1) select的超时参数,select每次返回后都得重新装载值,否则下次select就变为非阻塞;

(2) 若select成功捕获到有个fd,那必须使用FD_ISSET宏进行检查(就是监控一个fd也得调用这个),如果不调用,那么下次select就不正常(捕获不到这个fd的变化)

3.select 实现原理

select的实现依赖于设备的驱动函数poll,poll的功能是检查设备的哪条条流可用(一个设备一般有三条流,读流,写流,设备发生异常的异常流),如果其中一条流可用,返回一个mask(表示可用的流),如果不可用,把当前进程加入设备的流等待队列中,例如读等待队列、写等待队列,并返回资源不可用。

select正是利用了poll的这个功能,首先让程序员告知自己关心哪些io流(用文件描述符表示,也就是上文的readfds、writefds和exceptfds),并让程序员告知自己这些流的范围(也就是上文的nfds参数)以及程序的容忍度(timeout参数),然后select会把她们拷贝到内核,在内核中逐个调用流所对应的设备的驱动poll函数,当范围内的所有流也就是描述符都遍历完之后,他会检查是否至少有一个流发生了,如果有,就修改那三个流集合,把她们清空,然后把发生的流加入到相应的集合中,并且select返回。如果没有,就睡眠,让出cpu,直到某个设备的某条流可用,就去唤醒阻塞在流上的进程,这个时候,调用select的进程重新开始遍历范围内的所有描述符。

步骤:

1、拷贝nfds、readfds、writefds和exceptfds到内核;

2、遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll函数

3、检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4;

4、select返回;

5、select阻塞当前进程,等待被流对应的设备唤醒,当被唤醒时,执行2。或者timeout到期,执行4;

4. 实例

编写一个程序,使用select捕获标准输入,若有则打印字符到控制台,3s内未能捕捉到标准输入,则打印超时;

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<string.h>

int main()
{
    fd_set rset;             //创建可读描述符集
    int fd = STDIN_FILENO;   //标准输入描述符

    struct timeval timeout;  //设置超时

    while(1)
    {

        timeout.tv_sec = 3;   //必须多次装载,否则下次就是非阻塞
        timeout.tv_usec = 0;

        FD_ZERO(&rset);    //清空rset
        FD_SET(fd,&rset);  //置位rset描述符集合中fd所对应的位

        int re = select(fd+1,&rset,NULL,NULL,&timeout);
        if(re==0)  //3S超时
        {
            printf("3 seconds timeout\n");
        }
        if(re<0)   //出错,select被中断
        {
            perror("select error");
        }
        else       //正常返回
        {
            if(FD_ISSET(fd,&rset))  //这个必不可少,否则select不正常
            {
                char buf[512] = {0};
                read(STDIN_FILENO,buf,sizeof(buf)-1);
                printf("out:%s\n",buf);
            }
        } 
    }
    return 0;
}

编译运行,可见程序确实使用select实现了对标准输入fd的捕捉。

5.select优缺点总结

与多进程/多线程服务器进行对比 它的优点在于: 
         (1)不需要建立多个线程、进程就可以实现一对多的通信;
         (2)可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多;
         (3)select() 的可移植性更好,在某些Unix系统上不支持poll() ;
         (4)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒;

与多进程/多线程服务器进行对比 它的缺点在于
         (1)数据拷贝性能损耗:每次调用select都需要把所有FD集合从用户态拷贝到内核态,开销随着FD数目线性增长;
         (2)获取就绪事件性能损耗:调用select返回后,需要遍历所有FD集合来寻找就绪FD,开销随着FD数目线性增长,时间复杂度O(N);
         (3)select支持的文件描述符数量太小了,默认是1024;

二、poll机制

1.从select到poll的改进

(1) poll 函数类似于 select,但是函数参数结构有所不同。回忆一下select接口,select需要我们指定文件描述符的最大值,然后select会遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll函数,也就是说这个范围内存在一些不是我们感兴趣的文件描述符,cpu做了一些无用功,跟select不同的是,poll不再告知内核一个范围,而是通过struct pollfd结构体数组精确的告知内核用户关心哪些文件描述符(流)

(2) select一次可以监测 FD_SETSIZE 数量大小的描述符,poll一次可以监测的描述符数量并没有限制,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。

(3) 虽然 poll 函数是 system V 引入进来支持 STREAMS 子系统的,但是 poll 函数可用于任何类型的文件描述符

2.poll函数

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
//返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1.

(1) 参数 fdarray

一个 pollfd 类型的数组,每个数组元素指定一个描述符 编号以及我们对该描述符感兴趣的条件。

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events(由内核设定) */
           };

 应将每个数组元素的 events 成员设置为上图所示值的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。返回时,revnets 成员由内核设置,用于说明每个描述符发生了哪些事件。

注意,poll 没有更改 events 成员。这与 select 不同,select 修改其参数以指示哪个描述符已准备好了(所以select每次得重装载 FD_SET)。

上图中的前 4 行测试的是可读性,接下来的 3 行测试的是可写性,最后 3 行测试的是异常条件。最后 3 行是由内核在返回时设置的。即使在 events 字段中没有指定这 3 个值,如果是相应条件发生,在 revents 中也会返回它们。

当一个描述符被挂断(POLLHUP)后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。

POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT |POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM |POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM。

(2) 参数 nfds

指定fdarray 数组中元素的个数。

(3) 参数 timeout

指定的是我们愿意等待多长时间,如同 select 一样,有 3 种不同的情形。

(i) timeout == -1

永远等待。(某些系统在 <stropts.h>中定义了常量 INFTIM,其值通常是 -1)当所指定的描述符中的一个已准备好,或捕捉到一个信号时返回。如果捕捉到一个信号,则 poll 返回 -1,errno 设置为 EINTR。

(ii) timeout == 0

不等待测试所有描述符并立即返回。这是一种轮询系统的方法,可以找到多个描述符的状态而不阻塞 poll 函数。

(iii) timeout > 0

等待 timeout 毫秒当指定的描述符之一已准备好,或 timeout 到期时还没有一个描述符准备好,则返回值是 0.(如果系统不提供毫秒级精度,则 timeout 值取整到最近的支持值)。

(4) 函数返回

成功时,poll()返回结构体中 revents 域不为 0 的文件描述符个数;

超时时,如果在超时前没有任何事件发生,poll() 返回 0;

失败时,poll()返回 -1,并设置 errno 为下列值之一:

EBADF         一个或多个结构体中指定的文件描述符无效。
EFAULTfds   指针指向的地址超出进程的地址空间。
EINTR      请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds  参数超出PLIMIT_NOFILE值。
ENOMEM       可用内存不足,无法完成请求。

3.poll 原理

poll的功能和select的功能一样,poll的底层原理也和select差不多,就不多说了。

4.实例

编写一个程序,使用poll捕获标准输入,若有则打印字符到控制台,3s内未能捕捉到标准输入,则打印超时;

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<poll.h>
#include<string.h>

int main()
{
    struct pollfd fdarray[1] = {0}; //定义pollfd数组

    fdarray[0].fd = STDIN_FILENO;//标准输入描述符
    fdarray[0].events = POLLIN|POLLPRI;  //监控可读事件

    while(1)
    {
           int re = poll(fdarray,1,3000);  //监控fdarray数组中所有描述符,超时时间3s
            if(re < 0)  //被中断
            {
                perror("poll is interrupted!");
                exit(1);
            }
            else if(re == 0)  //超时
            {
                printf("3 seconds timeout\n");
            }
            else  //正常返回
            {
                char buf[512]={0};
                if(fdarray[0].revents && (POLLIN|POLLPRI))   //进行判断
                {
                    read(STDIN_FILENO,buf,sizeof(buf)-1);  //从标准输入中读取数据
                    write(STDOUT_FILENO,"read:",strlen("read:"));//将数据输出到标准输出  
                    write(STDOUT_FILENO,buf,strlen(buf));   
                }
            }
            
    }
    return 0;
}

编译运行,可见程序确实使用poll实现了对标准输入fd的捕捉。 

三、epoll机制

参照:【Linux学习】epoll详解 (apue和unix网络编程上都没介绍)

1. 什么是epoll?

epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

select一次可以监测 FD_SETSIZE 数量大小的描述符,FD_SETSIZE 通常是一个在 libc 编译时指定的小数字。

poll一次可以监测的描述符数量并没有限制,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。

epoll 没有这些固定限制,也不执行任何线性扫描。因此它可以更高效地执行和处理大量事件。

2. epoll 的接口

epoll的接口函数总共才3个,分别如下:

#include <sys/epoll.h>
int epoll_create(int size);
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);

(1) epoll_create(int size)

创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大。这个参数不同于 select 中的第一个参数,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 Linux 下如果查看 /proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽

(2) epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个参数是epoll句柄,epoll_create()的返回值。

第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd。

第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

//保存触发事件的某个文件描述符相关的数据(是和联合数据类型,与具体使用方式有关)
 
typedef union epoll_data_t {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
 //感兴趣的事件和被触发的事件
struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3). epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

等待事件的产生,类似于 select 调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create 时的 size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。

3. epoll的工作模式

epoll有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:是 epoll 缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

4. epoll的优点

1.支持一个进程打开大数目的socket描述符(FD)

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2.IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3.使用mmap加速内核与用户空间的消息传递

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的

5. 实例

使用epoll监听服务端套接字描述符和与客户端的连接描述符,接收客户端的信息并打印在标准输出。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netdb.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<sys/epoll.h>
#define MAXLINE 1024
#define MAXEVENT 64
//打开并返回监听描述符(搜索主机中所有可用做接受TCP连接的套接字地址,并尝试bind,并尝试listen,成功则返回)(此函数与IPV4/6协议无关)
int open_listfd(char *port)   
{
	struct addrinfo hints, *listp, *p;  
	int listenpd, optval = 1;

	//获得主机中可作为服务端套接字地址的候选列表
	memset(&hints, 0x00,sizeof(struct addrinfo));  
	hints.ai_socktype = SOCK_STREAM;   //限制地址为TCP模式
	hints.ai_flags |= AI_PASSIVE;   	//指明返回的套接字地址用来作为接收连接
	hints.ai_flags |= AI_ADDRCONFIG; //指明返回的套接字地址是IPV4还是IPV6要和主机配置相同
	hints.ai_flags |= AI_NUMERICSERV; //强制服务参数为十进制端口
	getaddrinfo(NULL,port,&hints,&listp);

	//遍历服务端套接字地址的候选列表,尝试bind
	for(p=listp;p;p=p->ai_next)
	{
		//创建socket描述符
		listenpd = socket(p->ai_family,p->ai_socktype,p->ai_protocol);  
		if(listenpd < 0) //套接字创建失败,尝试下一个
			continue;

		//避免bind函数在重复绑定同一套接字时出错
		setsockopt(listenpd,SOL_SOCKET,SO_REUSEADDR,(const char*)&optval,sizeof(int));

		//bind套接字
		if(bind(listenpd,p->ai_addr,p->ai_addrlen) == 0)
			break;   //bind成功

		//关闭此套接字,尝试下一个
		close(listenpd);  
	}

	//释放链表空间
	freeaddrinfo(listp);
	if(!p)   //如果没有找到可行套接字则返回
		return -1;

	//监听找到的套接字
	if(listen(listenpd,SOMAXCONN) < 0)  //监听失败
	{
		close(listenpd);
		return -1;
	}
	return listenpd;
}

//设置socket非阻塞
int set_sock_non_block(int sfd)
{
	int flags,s;
	//得到文件状态标志
	flags = fcntl(sfd ,F_GETFL ,0);
	if(flags == -1)
	{
		perror("fcntl error\n");
		return -1;
	}

	//设置文件状态标志
	flags |= O_NONBLOCK;
	s = fcntl(sfd,F_SETFL,flags);
	if(s==-1)
	{
		perror("fcntl error\n");
		return -1;
	}
	return 0;
}


int main(int argc,char* argv[])
{
	if(argc!= 2)
	{
		perror("parameter error\n");
		exit(1);
	}
	//套接字相关
	int listenfd,connfd;
	socklen_t clientlen;
	struct sockaddr_storage clientaddr;    //sockaddr_storage能保证存放任意大的地址
	char client_hostname[MAXLINE], client_port[MAXLINE];
	listenfd = open_listfd(argv[1]);
	if(listenfd < 0)
	{
		perror("can not create listenfd\n");
		exit(1);
	}
	int re = set_sock_non_block(listenfd);
	if(re<0)
	{
		perror("set_sock_non_block error\n");
		exit(1);
	}

	//epoll相关
	int epoll_fd;
	struct epoll_event *event_ptr;
	struct epoll_event event;
	//1 创建epoll句柄
	epoll_fd = epoll_create(MAXEVENT);  
	
	//2 注册epoll事件
	event.data.fd = listenfd; 
	event.events = EPOLLIN|EPOLLET;  //读入,边缘触发
	re = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listenfd,&event);
	if(re<0)
	{
		perror("epoll_ctl error\n");
		exit(1);
	}

	//3 等待epoll事件
	event_ptr = (struct epoll_event *)calloc(MAXEVENT,sizeof(struct epoll_event));
	while(1)
	{
		int re_num,i;
		re_num = epoll_wait(epoll_fd,event_ptr,MAXEVENT,-1);  //阻塞方式等待
		for(i=0;i<re_num;i++)  //遍历所有变化的被监控fd
		{
			//此fd发生错误,或者套接字尚未准备好读取
			if((event_ptr[i].events&EPOLLERR)||(event_ptr[i].events&EPOLLHUP)||(!(event_ptr[i].events&EPOLLIN)))
			{
				perror("epoll error\n");
				close(event_ptr[i].data.fd);
				continue;
			} 
			//是监听套接字可读,这意味着至少1个请求(当多个连接同时到来且尚未处理时,epoll的返回数量少于实际值)
			else if(event_ptr[i].data.fd==listenfd)
			{
				//强制循环接收
				while(1)
				{
					clientlen = sizeof(struct sockaddr_storage);
					connfd = accept(listenfd,(sockaddr *)&clientaddr,&clientlen);  //接收
					if(connfd == -1)
					{
						if((errno == EAGAIN)||(errno == EWOULDBLOCK))   //已经读取完毕
						{
							break;
						}
						else
						{
							perror("accept error\n");
							break;
						}
					}

					re = getnameinfo((sockaddr *)&clientaddr,clientlen,client_hostname,MAXLINE,client_port,MAXLINE,0);
					if(re == 0)  //成功则打印客户端信息
					{
						printf("a new client connect: (%s,%s)\n",client_hostname,client_port);
					}
					//设置连接描述符为非阻塞
					re = set_sock_non_block(connfd);
					if(re == -1)
					{
						perror("set connectfd unblock failed\n");
					}
					
					//将连接描述符注册到epoll中
					event.events = EPOLLIN|EPOLLET; //监控可读,边缘触发
					event.data.fd = connfd;
					re = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,connfd,&event);
					if(re == -1)
					{
						perror("add connectfd into epoll failed\n");
						exit(1);
					}
					//close(connfd);
				}
			}
			else  //监控到连接描述符可读(同样要循环读,直到读取完毕,在边缘触发模式下再次收到数据不会重新通知)
			{
				int done = 0;
				while(1)
				{
					char buf[MAXLINE] = {0};
					ssize_t rnum = read(event_ptr[i].data.fd,buf,MAXLINE);
					if(rnum == -1)  //出现异常
					{
						if(errno != EAGAIN)
						{
							perror("read error\n");
							done = 1;
						}
						printf("read done!\n");
						break;  //如果errno == EAGAIN,表示已经读取完毕
					}
					else if(rnum == 0)   //read返回值为0,则表示源端关闭
					{
						done = 1;
						break;
					}
					//将收到的字符打印到标准输出
					re = write(STDOUT_FILENO,buf,strlen(buf));
					if(re == -1)
					{
						perror("write error\n");
					}
				}
					//如果连接断开或出错,关闭该连接描述符
					if(done)
					{
						printf("close connection on descriptor %d\n",event_ptr[i].data.fd);
						//关闭描述符将使epoll从被监视的描述符集中删除它
						close(event_ptr[i].data.fd); 
					} 
			}
		}
	}
	free(event_ptr);
	//4 关闭epoll句柄
	close(epoll_fd);

	return 0;
}

编译运行sever.cpp,并使用telnet与之交互,如下:


参考:

1. 《UNIX环境高级编程》

2. 博客 https://blog.csdn.net/qq_29350001/article/details/72417019

3. 博客 https://blog.csdn.net/xiajun07061225/article/details/9250579

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值