Linux网络编程-网络io与select,poll,epoll

问题

1、select、poll、epoll分别是什么?

2、有什么区别?

1.主机之间的通信离不开网络,沟通方式可以用TCP、UDP,广播等

TCP,socket连接:

首先了解下socket是怎么使用的

socket主要有以下函数:socket,listen,connect,bind,accept,send,sendto,recv,recvfrom,close,shutdown

网络中的进程都是由socket进行通信的,在linux和windows环境下的头文件主要是:

#include <sys/socket.h>和#include <winSock2.h>

下面介绍每个函数的具体使用方法和功能。

1.socket

int socket(int domain, int type, int protocol),这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。

domain,函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族,常用AF_INET。

type,函数socket()的参数type用于设置套接字通信的协议类型,主要有SOCKET_STREAM(流式套接字)(Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输)、SOCK——DGRAM(数据包套接字)(支持UDP连接(无连接状态的消息))等。

protocol,函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

errno,函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得,头文件#include <errno.h>

返回值,如果成功返回非负描述符,不成功返回-1

2.bind

int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen), 当socket函数返回一个描述符时,只是存在于其协议族的空间中,并没有分配一个具体的协议地址(这里指IPv4/IPv6和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。

sockfd是socket函数返回的描述符;

myaddr指定了想要绑定的IP和端口号,均要使用网络字节序-即大端模式;

addrlen是前面struct sockaddr(与sockaddr_in等价)的长度。

为了统一地址结构的表示方法,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。例如:

struct sockaddr_in servaddr;

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);

bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))

通常服务器在启动的时候都会绑定一个众所周知的协议地址,用于提供服务,客户就可以通过它来接连服务器;而客户端可以指定IP或端口也可以都不指定,未分配则系统自动分配。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。sockfd的分配是一个bigmap的做法

3.listen

int listen(int sockfd, int backlog),返回0表示成功,-1表示失败

函数listen仅被tcp服务器调用,做两件事情:

1.当函数socket创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口转换成被动套接口,指示内核应接受指向此套接口的连接请求。调用函数listen导致套接口从CLOSED状态转换到LISTEN状态。

2.函数的第二个参数规定了内核为此套接口排队的最大连接个数。

一般来说,此函数应在调用幻术socket和bind之后,调用函数accept之前调用。

对于给定的监听套接口,内核要维护两个队列:

1.未完成连接队列,为每个这样的SYN分别开设一个条目:已由客户发出并到达服务器,服务器正在等待完成相应的TCp三次握手过程,这些套接口都处于SYN-RCVD状态;

2.已完成连接队列:为每个已完成TCP三次握手过程的客户开设一个条目。这些套接口都处于ESTABLISHED状态。

两个队列之和数量不得超过backlog。

4.connect

int connect(int sockfd,conststruct sockaddr *addr, socklen_t addrlen)返回0成功,-1表示失败。

通过此函数建立于TCP服务器的连接,实际是发起三次握手过程,仅在连接成功或失败后返回。参数sockfd是本地描述符,addr为服务器地址,addrlen是socket地址长度。

UDP的connect函数,结果与tcp调用不相同,没有三次握手过程。内核只是记录对方的ip和端口号,他们包含在传递给connect的套接口地址结构中,并立即返回给调用进程。

5.accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)返回值非负描述符表示成功,-1表示失败。

6.send

size_t send(int sockfd, const void *buf, size_t len, int flags) 返回值成功返回成功拷贝至发送缓冲区的字节数(可能小于len),-1表示失败;

其中:

sockfd:发送端套接字描述符(非监听描述符)

buf:应用要发送数据的缓存 (#define MAXLNE 4096 char buf[MAXLNE];)

len:实际要发送的数据长度

flag:一般设置为0

每个TCP套接口都有一个发送缓冲区,它的大小可以用SO_SNDBUF这个选项来改变。调用send函数的过程,实际是内核将用户数据拷贝至TCP套接口的发送缓冲区的过程:若len大于发送缓冲区大小,则返回-1;否则,查看缓冲区剩余空间是否容纳得下要发送的len长度,若不够,则拷贝一部分,并返回拷贝长度(指的是非阻塞send,若为阻塞send,则一定等待所有数据拷贝至缓冲区才返回,因此阻塞send返回值必定与len相等);若缓冲区满,则等待发送,有剩余空间后拷贝至缓冲区;若在拷贝过程出现错误,则返回-1。关于错误的原因,查看errno的值。

如果send在等待协议发送数据时出现网络断开的情况,则会返回-1。注意:send成功返回并不代表对方已接收到数据,如果后续的协议传输过程中出现网络错误,下一个send便会返回-1发送错误。TCP给对方的数据必须在对方给予确认时,方可删除发送缓冲区的数据。否则,会一直缓存在缓冲区直至发送成功(TCP可靠数据传输决定的)。

7.recv

size_t recv(int sockfd,void *buf, size_t len,int flags)

其中:

sockfd:接收端套接字描述符;

buf:指定缓冲区地址,用于存储接收数据;

len:指定的用于接收数据的缓冲区长度;

flags:一般指定为0

表示从接收缓冲区拷贝数据。成功时,返回拷贝的字节数,失败返回-1。阻塞模式下,recv/recvfrom将会阻塞到缓冲区里至少有一个字节(TCP)/至少有一个完整的UDP数据报才返回,没有数据时处于休眠状态。若非阻塞,则立即返回,有数据则返回拷贝的数据大小,否则返回错误-1,置错误码为EWOULDBLOCK。

8.close (头文件unistd.h)

close缺省功能是将套接字作“已关闭”标记,并立即返回到调用进程,该套接字描述符不能再为该进程所用:即不能作为read和write(send和recv)的参数,但是TCP将试着发送发送缓冲区内已排队待发的数据,然后按正常的TCP连接终止序列进行操作(断开连接4次握手-以FIN为首的4个TCP分节)。

9.shutdown

shutdown不仅可以灵活控制关闭连接的读、写或读写功能,而且会立即执行相应的断开动作(发送终止连接的FIN分节等),此时不论有多少进程共享此套接字描述符,都将不能再进行收发数据。

10.select

select的优缺点:对比多线程(一个请求一个线程,上限C10K)而言,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

select相关API介绍与使用

#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明:

maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间.

返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

以下介绍与select函数相关的常见的几个宏:

#include <sys/select.h> int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0 int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用 int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位 int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:

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

1.socket阻塞模式

所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回

2.socket非阻塞模式

所谓非阻塞方式non- block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高

深入理解select模型:

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

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

(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。

简单的socket通信:#include <errno.h>

#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
 
#define MAXLNE  4096

//8m * 4G = 128 , 512  一个pthread线程8M
//C10k
void *client_routine(void *arg) {
    int connfd = *(int *)arg;
    char buff[MAXLNE];
    while (1) {
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
            break;
        }
        
        //close(connfd);
    }
}

int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 #if 0   //这种方法,缺点是只能连接一个客户端使用
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0; //只能获取一次connfd,因此只有一个用户accept建立TCP连接
    }

    printf("========waiting for client's request========\n");
    while (1) {

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
#elif 0  //可以建立多个用户连接,但每个用户只能发送一次请求
    while (1) {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) { 
        //若已完成连接队列为空,会阻塞在这里
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
        printf("========waiting for client's request========\n");
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
#elif 0   //创建线程,为每个用户连接单独创建一个线程,优点是逻辑简单,缺点是大量的用户不适用,c1k已经不错了  while(1){    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	    return 0;
    }

    pthread_t threadid;
    pthread_create(&threadid, NULL, client_routine, (void*)&connfd);  }#else
    //select ,select第五个参数表示轮询时间,如果select第5的参数为NULL自带阻塞的功能,
    // 
    fd_set rfds, rset; //bitmap类型的

    FD_ZERO(&rfds);  //清空里面的数据
    FD_SET(listenfd, &rfds);//设置listenfd

    int max_fd = listenfd;

    while (1) {

        rset = rfds; //调用select,可能会修改rfds里面的值,因此一定要定义一个变量

        int nready = select(max_fd+1, &rset, NULL, NULL, NULL);//返回的表示就绪描述符的数目
        //第一个参数要比rfds中最大值+1,第二个参数读的集合,第三个参数表示写的集合,第四个参数表示错的集合,第五个参数表示轮询的时间,NULL表示无限时间,成功返回后,没有数据的将被置为0

        if (FD_ISSET(listenfd, &rset)) { //若listenfd还存在,说明存在请求连接的用户

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            FD_SET(connfd, &rfds);

            if (connfd > max_fd) max_fd = connfd;

            if (--nready == 0) continue; //如果为true,说明其他accept后的用户没有数据过来,直接执行continue

        }

        int i = 0;
        for (i = listenfd+1;i <= max_fd;i ++) {

            if (FD_ISSET(i, &rset)) { // 

                n = recv(i, buff, MAXLNE, 0);
                if (n > 0) {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(i, buff, n, 0);
                } else if (n == 0) {

                    FD_CLR(i, &rfds);
                    //printf("disconnect\n");
                    close(i);
                    
                }
                if (--nready == 0) break;
            }

        }
        

    }
    
#endif
 
    close(listenfd);
    return 0;
}

select总结:

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

1、单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.上限可以监视C1000K个fd

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

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

11.poll

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout)

pollfd结构体定义如下:

struct pollfd
{
 int fd; /* 文件描述符 */
 short events; /* 等待的事件 */
 short revents; /* 实际发生了的事件 */
} ;

1、第一个参数:用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。下表列出指定events标志以及测试revents标志的一些常值:

常量 说明 是否能作为 events 的输入 是否能作为revents的返回结果

POLLIN 普通或者优先级带数据可读 能 能

POLLRDNORM 普通数据可读 能 能

POLLRDBAND 优先级带数据可读 能 能

POLLPRI 高优先级数据可读 能 能

POLLOUT 普通数据可读 能 能

POLLWRNORM 普通数据可写 能 能

POLLWRBAND 优先级数据可写 能 能

POLLERR 发生错误 能 能

POLLHUP 发生挂起 能

POLLNVAL描述字不是一个打开的文件 能

2.第二个参数:nfds指定数组中监听的元素个数,比所有文件描述符最大值大1;

3.第三个参数:timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

#elif 0


    struct pollfd fds[POLL_SIZE] = {0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;

    int max_fd = listenfd;
    int i = 0;
    for (i = 1;i < POLL_SIZE;i ++) {
        fds[i].fd = -1;
    }

    while (1) {

        int nready = poll(fds, max_fd+1, -1);

    
        if (fds[0].revents & POLLIN) {

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }

            printf("accept \n");
            fds[connfd].fd = connfd;
            fds[connfd].events = POLLIN;

            if (connfd > max_fd) max_fd = connfd;

            if (--nready == 0) continue;
        }

        //int i = 0;
        for (i = listenfd+1;i <= max_fd;i ++)  {

            if (fds[i].revents & POLLIN) {
                
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0) {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(i, buff, n, 0);
                } else if (n == 0) { //

                    fds[i].fd = -1;

                    close(i);
                    
                }
                if (--nready == 0) break;

            }

        }

    }

12.epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不会复用文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll 那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

优点:

  • 支持一个进程打开大数目的socket描述符
  • IO效率不随FD数目增加而线性下降
  • 内核微调

epoll工作模式

epoll有两种工作方式:LT和ET

  • LT(level-triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
  • ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

epoll的接口

epoll的接口有三个函数:

1. int epoll_create(int size);

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

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

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

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

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

epoll的框架

头文件#include <sys/epoll.h> ,首先,通过epoll_create(int maxfds)来创建一个epoll的句柄。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

然后,在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event* events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后, events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

接下来,epoll_wait范围之后应该是一个循环,遍历所有的事件。

#else

    //poll/select --> 
    // epoll_create 
    // epoll_ctl(ADD, DEL, MOD)
    // epoll_wait

    int epfd = epoll_create(1); //int size

    struct epoll_event events[POLL_SIZE] = {0};
    struct epoll_event ev;

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    while (1) {

        int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
        if (nready == -1) {
            continue;
        }

        int i = 0;
        for (i = 0;i < nready;i ++) {

            int clientfd =  events[i].data.fd;
            if (clientfd == listenfd) {

                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                    return 0;
                }

                printf("accept\n");
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

            } else if (events[i].events & EPOLLIN) {

                printf("recv\n");
                n = recv(clientfd, buff, MAXLNE, 0);
                if (n > 0) {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);

                    send(clientfd, buff, n, 0);
                } else if (n == 0) { //


                    ev.events = EPOLLIN;
                    ev.data.fd = clientfd;

                    epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);

                    close(clientfd);
                    
                }

            }

        }

    }
struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) { 
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    pthread_t threadid;
    pthread_create(&threadid, NULL, client_routine, (void *)&connfd);

参考博客:网络io与select,poll,epoll - 放弃吧 - 博客园

狮的主页会不断更新C/C++ Linux系统以及程序员面试面经还有后端开发,服务器开发等等知识体系内容;点个关注+喜欢敬请更新

猜你喜欢

转载自blog.csdn.net/m0_58687318/article/details/126604351