前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

作者头像
Aichen
发布2018-05-18 12:04:38
1.8K0
发布2018-05-18 12:04:38
举报
文章被收录于专栏:白驹过隙白驹过隙

上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度。 IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作。

为什么要IO复用? 1.网络编程中非常多函数是阻塞的,如connect,利用IO复用可以以非阻塞形式执行代码。 2.之前提到listen维护两个队列,完成握手的队列可能有多个就绪的描述符,IO复用可以批处理描述符。 3.有时候可能要同时处理TCP和UDP,同时监听多个端口,同时处理读写和连接等。

为什么epoll效率要比select高? 1.在连接数量较大的场景,select遍历需要每个描述符,epoll由内核维护事件表,只需要处理有响应的描述符。 2.select本身处理文件描述符受到限制,默认1024。 3.效率并不是绝对的,当连接率高,断开和连接频繁时,select不一定比epoll差。所以要根据具体场合使用。

epoll的两种模式,电平触发和边沿触发。 1.电平触发效率较边沿触发低,电平触发模式下,当epoll_wait返回的事件没有全部相应处理完毕,内核缓冲区还存在数据时,会反复通知,直到处理完成。epoll默认使用这种模式。 2.边沿触发效率较高,内核缓冲区事件只通知一次。

一个epoll实现demo

代码语言:javascript
复制
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

int main(int argc, char* argv[])
{
    int listen_fd, connfd_fd, socket_fd, epfd, nfds;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;

    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    struct epoll_event ev,events[20];
    //生成用于处理accept的epoll专用的文件描述符
    epfd=epoll_create(5);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    //设置与要处理的事件相关的文件描述符
    ev.data.fd = listen_fd;
    //设置要处理的事件类型
    ev.events=EPOLLIN|EPOLLET;
    //注册epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev);
    
    memset(&serveraddr, 0, sizeof(serveraddr));  
    serveraddr.sin_family = AF_INET;  
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERV_PORT);
    
    if (bind(listen_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
    {  
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
        exit(0);  
    }  
    
    if (listen(listen_fd, LISTENQ) == -1)
    {  
        exit(0);  
    }   
    
    for ( ; ; ) 
    {
        //等待epoll事件的发生
        nfds = epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件
        for (int i = 0; i < nfds; ++i)
        {
            if (events[i].data.fd == listen_fd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。

            {
                connfd_fd = accept(listen_fd,(sockaddr *)&clientaddr, &clilen);
                if (connfd_fd < 0){
                    perror("connfd_fd < 0");
                    exit(1);
                }
                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //设置用于读操作的文件描述符
                ev.data.fd = connfd_fd;
                //设置用于注测的读操作事件
                ev.events = EPOLLIN|EPOLLET;
                //注册ev
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd_fd,&ev);
            }
            else if (events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
            {            
                memset(&line,'\0', sizeof(line));  
                if ( (socket_fd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(socket_fd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(socket_fd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(socket_fd);
                    events[i].data.fd = -1;
                }                
                cout << line << endl;
                //设置用于写操作的文件描述符
                ev.data.fd = socket_fd;
                //设置用于注测的写操作事件
                ev.events = EPOLLOUT|EPOLLET;
                //修改socket_fd上要处理的事件为EPOLLOUT
                //epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
            }
            else if (events[i].events&EPOLLOUT) // 如果有数据发送
            {
                socket_fd = events[i].data.fd;
                write(socket_fd, line, n);
                //设置用于读操作的文件描述符
                ev.data.fd = socket_fd;
                //设置用于注测的读操作事件
                ev.events = EPOLLIN|EPOLLET;
                //修改socket_fd上要处理的事件为EPOLIN
                epoll_ctl(epfd,EPOLL_CTL_MOD,socket_fd,&ev);
            }
        }
    }
    return 0;
}

执行效果如下:

QQ图片20160512223244
QQ图片20160512223244

第一次学epoll时,容易错误的认为epoll也可以实现并发,其实正确的话是epoll可以实现高性能并发服务器,epoll只是提供了IO复用,在IO“并发”,真正的并发只能通过线程进程实现。 那为什么可以同时连接两个客户端呢?实际上这两个客户端都是在一个进程上运行的,前面提到过各个描述符之间是相互不影响的,所以是一个进程轮循在处理多个描述符。

Reactor模式: Reactor模式实现非常简单,使用同步IO模型,即业务线程处理数据需要主动等待或询问,主要特点是利用epoll监听listen描述符是否有相应,及时将客户连接信息放于一个队列,epoll和队列都是在主进程/线程中,由子进程/线程来接管各个描述符,对描述符进行下一步操作,包括connect和数据读写。主程读写就绪事件。 大致流程图如下:

image
image

Preactor模式: Preactor模式完全将IO处理和业务分离,使用异步IO模型,即内核完成数据处理后主动通知给应用处理,主进程/线程不仅要完成listen任务,还需要完成内核数据缓冲区的映射,直接将数据buff传递给业务线程,业务线程只需要处理业务逻辑即可。 大致流程如下:

image
image
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016-05-12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档