一步一步来epoll

一切都从最基本开始。

 

网络编程中客户端连服务端的经典代码:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

struct sockaddr_in svraddr;

svraddr.sin_family              = AF_INET;

svraddr.sin_port                 = htons(iPort);

svraddr.sin_addr.s_addr             = inet_addr(Ip);

 

connect(sockfd, (struct sockaddr*)&svraddr, sizeof(svraddr);

char req[1024]="test";

sendto(acceptfd, req, strlen(req)+1, 0, (struct sockaddr*)&svraddr, sizeof(svraddr));

网络编程中服务端接收客户端的经典代码:

int listenfd = socket(AF_INET, SOCK_STREAM, 0);

 

struct sockaddr_in svraddr;

svraddr.sin_family              = AF_INET;

svraddr.sin_port                 = htons(iPort);

svraddr.sin_addr.s_addr             = INADDR_ANY;

 

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

listen(listenfd, SOMAXCONN);

 

struct sockaddr_in cliaddr;

socklen_t clilen = 0;

 

while(1)

{                

         int acceptfd = accept(listenfd, (struct sockaddr*)&cliaddr, & clilen);

 

         char rec[1024];

         memset(sRec, 0, 1024);

         int byte_read = recv(acceptfd, rec, 1024, 0);

         if (byte_read > 0)

         {

                   sendto(acceptfd, rec, byte_read, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));

         }

        

         close(acceptfd);

}      

在单进程单请求的处理中,这几个函数完美的完成了一次客户端请求服务端发包的过程。Server端就是一个处理机,当来一个请求时,server捕获到这个请求,然后处理。

当服务端accept时,会阻塞当前进程,直到客户端发送请求过来。

 

由于服务端是处理单请求,当多个请求同时到来时,后面的请求会被阻塞住。比如:

服务端S先执行,阻塞于accept

客户端c1连接S,但先不发送数据,则S会阻塞在recv这个io上面

此时,客户端c2连接S,即使发送数据过去,S仍然无法响应

c1发送数据给S后,S处理完毕,会acceptc2,处理c2的请求

 

 

这种模式的缺点太明显了,无法做到处理多个fd。那么有没有这么一种设计,当io请求到来的时候,告诉服务端有请求去处理呢?

这就是i/o多路复用了 

 

 

Select下的多路复用,仍然会阻塞,但是不会像Accept那样阻塞在i/o上面,而是会阻塞到select这个系统调用上面。

如此这般,当有请求(新客户端连接请求,已经连接的套接口发送数据请求等)到来时,select会捕捉到这些请求。

Select的基本流程是这样的:

阻塞于包含一堆fd的集合之上,这些fds可能是套接口,文件等

Fd中有可写,可读,或异常,或超时,select返回。

进入逻辑处理阶段

Select继续阻塞,循环处理。

 

经过改造之后的server端代码

char line[MAXLINE];

socklen_t clilen;

//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

struct epoll_event ev,events[20];

//生成用于处理acceptepoll专用的文件描述符

epfd=epoll_create(256);

struct sockaddr_in clientaddr;

struct sockaddr_in serveraddr;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

//socket设置为非阻塞方式

//setnonblocking(listenfd);

//设置与要处理的事件相关的文件描述符

ev.data.fd=listenfd;

//设置要处理的事件类型

ev.events=EPOLLIN|EPOLLET;

//ev.events=EPOLLIN;

//注册epoll事件

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

bzero(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_addr.s_addr        = inet_addr(Ip);

serveraddr.sin_port=htons(SERV_PORT);

bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

listen(listenfd, LISTENQ);

maxi = 0;

for ( ; ; ) {

         //等待epoll事件的发生

         nfds=epoll_wait(epfd,events,20,500);

         //处理所发生的所有事件    

         for(i=0;i<nfds;++i)

         {

                   if(events[i].data.fd==listenfd)

                   {

                            connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                            //setnonblocking(connfd);

                            //设置用于读操作的文件描述符

                            ev.data.fd=connfd;

                            //设置用于注测的读操作事件

                            ev.events=EPOLLIN|EPOLLET;

                            //ev.events=EPOLLIN;

                            //注册ev

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

                   }

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

                   {

                            if ( (sockfd = events[i].data.fd) < 0)

                                     continue;

                            if ( (n = read(sockfd, line, MAXLINE)) < 0) {

                                     if (errno == ECONNRESET) {

                                               close(sockfd);

                                               events[i].data.fd = -1;

                                     }

                            } else if (n == 0) {

                                     close(sockfd);

                                     events[i].data.fd = -1;

                            }

                            line[n] = '/0';

                            cout << "read " << line << endl;

                            //设置用于写操作的文件描述符

                            ev.data.fd=sockfd;

                            //设置用于注测的写操作事件

                            ev.events=EPOLLOUT|EPOLLET;

                            //修改sockfd上要处理的事件为EPOLLOUT

                            epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

                   }

                   else if(events[i].events&EPOLLOUT)

                   {  

                            sockfd = events[i].data.fd;

                            write(sockfd, line, n);

                            //设置用于读操作的文件描述符

                            ev.data.fd=sockfd;

                            //设置用于注测的读操作事件

                            ev.events=EPOLLIN|EPOLLET;

                            //修改sockfd上要处理的事件为EPOLIN

                            epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

                   }

         }

}

 

 

        

经过select改造后的server可以同时接受10个请求而不会阻塞,效率提升了很多。

Select返回可能会小于0,这就是异常,其中包括被信号打断,此时error= EINTR,我们需要特殊处理这种情况。

Select超时情况下会返回为0.

正常情况下select会返回准备就绪的fd。如果是listen的套接字返回,则程序需要去accept请求,如果是accept的套接字返回,则程序需要去读或者写该套接字。

 

Select有个缺点,就是他仅能知道集合中有某个fd触发,却无法知道是哪一个,只能挨个去找。

这样就诞生了epollEpoll会记录下某个fd的信息,这样当fd触发的时候,epoll机制可以很快的找到相应的fd,进行处理。

 

Epoll技术是在linux2.6新加的,在头文件sys/epoll.h里面定义了其中的全部数据结构和操作。

主要使用三个接口:

int epoll_create(int size);

创建一个epoll句柄

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

将某个fd操作(op)到该句柄上

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

等待该epoll句柄。

 

Epoll设计下server代码就变成了下面这个样子了。

int listenfd = socket(AF_INET, SOCK_STREAM, 0);

int epollfd = epoll_create(20);

struct epoll_event ev, events[20];

//setnonblocking(listenfd);

ev.events = EPOLLIN | EPOLLET;

ev.data.fd = listenfd;

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

 

struct sockaddr_in svraddr;

svraddr.sin_family = AF_INET;

svraddr.sin_port = htons(atoi(argv[1]));

svraddr.sin_addr.s_addr = INADDR_ANY;

bind(listenfd, (struct sockaddr *)&svraddr, sizeof(svraddr));

listen(listenfd, SOMAXCONN);

int clientfd[10];

memset(clientfd, -1, sizeof(clientfd));

struct sockaddr_in cliaddr;

socklen_t clilen;

char recv[1024];

while(1)

{

         int nfds = epoll_wait(epollfd, events, 20, 500);

         for(int n = 0; n < nfds; ++n)

         {

                   if(events[n].data.fd == listenfd)

                   {

                            int iclifd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);

                            for(int i=0; i<10; i++)

                            {

                                     if (clientfd[i] == -1)

                                     {

                                               clientfd[i] = iclifd;

                                               epoll_ctl(epollfd, EPOLL_CTL_ADD, iclifd, &ev);

                                               break;

                                     }

                            }

                   }

                   for(int i=0; i<=10; i++)

                   {

                            if(clientfd[i]>=0 && events[n].data.fd==clientfd[i])

                            {

                                     int n = read(clientfd[i], recv, 1024);

                                     if (n<=0)

                                     {

                                               close(clientfd[i]);

                                               epoll_ctl(epollfd, EPOLL_CTL_DEL, iclifd, &ev);

                                               clientfd[i] = -1;

                                     }

                                     else

                                     {

                                               write(clientfd[i], recv, n);

                                     }

                            }

                   }

         }

}

 

Epoll的重要数据结构:

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 */

}; 

结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据。

例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_datafd字段以便后面的读写操作在这个文件描述符上进行。这个可以通过epoll_ctrlEPOLL_CTL_ADD 选项来实现,然后waitepoll句柄,当触发特定的事件时进行特殊的处理。

epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件。可能的取值为:

EPOLLIN :表示对应的文件描述符可以读;

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET:表示对应的文件描述符有事件发生;

 

ETLT模式的区别:

LT(水平触发)模式是缺省的工作模式,同时支持blocknoblock方式,这种模式下面编程会容易一些,因为只要没有处理完毕,内核不不断的通知。LT模式下的epollpoll/select差不多,只是理论速度会快一点而已。

 

ET(边沿触发)模式是epoll的高速模式,只支持noblock方式。这种情况下,描述符由未就绪到就绪的变化内核会通知,直到程序做了某些操作使得该描述符不再为就绪状态。

比如从一个描述符读取数据,一次没有读完,LT模式下面内核会通知继续去读,但在ET模式下需要程序自己去控制,直到读完位置。一次读取的内容如果与自己想读取的数量一致,这个时候缓冲区很有可能有未读的数据存在。

同样,往fd写数据的时候,可能发送端的速度高于接收端的速度,这样就很容易把缓冲区填满(返回EAGAIN错误),这个时候就需要等一下子,慢慢再传才可以了。

 

ET模式下面客户端关闭某个fd的时候,fd状态改变,内核会告诉服务端该fd有数据可读,但这个时候的读显然已经违背了服务端的本意,时间上也差远了。

 

 

多路复用的目的,就是为了提高服务端处理高并发请求的能力,采用懒人办法,就是多进程处理,父进程负责接收信号,然后fork子进程处理逻辑,这样在逻辑梳理上就清晰多了。

每种方法都有自己的利弊,就看我们更看重哪一点了。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值