【网络】(十一)更高效的epoll

第十篇文章中将select函数换做了poll,解决了客户端最大并发数量限制的问题,但通过压力测试会发现连接建立的速度是比较慢的,不管时select还是poll,都会存在这个问题。

select会受fd_set集合大小的限制,该大小不易更改,需要重新编译内核;同时也受系统所能打开的最大文件描述符的限制,该限制易更改。
poll只受系统所能打开的最大文件描述符的限制,前面也专门提到了该如何更改这个限制;该值的极限值受物理内存大小的限制,查看/proc/sys/fs/file-max文件可查看当前系统所能打开的最大文件描述符,4G内存的电脑上这一数值差不多有378675。该两者在使用时,都需要内核遍历所有的文件描述符,直到找到所有发生事件的文件描述符,并通知应用程序,所以当文件描述符非常多时,该两者的效率就会明显下降,这在上一篇中用压力测试客户端可看出来。

1、select、poll和epoll的区别

相比于select与poll,epoll最大的好处在于它不会随着监听文件描述符数目的增长而降低效率。
内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多
epoll的实现是基于回调的,如果fd有期望的事件发生,就通过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关;
内核/用户空间内存拷贝问题,如何让内核把fd消息通知给用户空间呢?在这个问题上,select/poll采用内存拷贝方法,而epoll采用共享内存的方式;
epoll不仅会告诉应用程序有I/O事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息,应用程序就能直接定位到事件,而不必遍历整个fd集合

2、改进poll服务端
将上一篇文章中的服务端程序用epoll重新实现,提高效率!

handle.h/handle.c 与上一篇文章相同
客户端程序暂时不做更改,client.c/connclient.c 与上一篇文章相同

服务端源码

因为要使用容器,所以采用C++改写,编译命令

g++ -Wall -g server.cpp handle.c -o server

server.cpp

#include 
#include 
#include 


#include "handle.h"
#include 
#include 
#include 

typedef std::vector<struct epoll_event> Eventlist;



#define FD_MAXSIZE 2048 //最大客户端数量


//为了防止SIGPIPE信号产生而终止了进程,所以捕获此信号
void handle_sigpipe(void)
{
    struct sigaction act;
    act.sa_handler = SIG_IGN;       //忽略SIGPIPE信号
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    if(sigaction(SIGPIPE, &act, NULL) == -1)
        handle_error("sigaction");
}

/* * activate_noblock -- 设置IO为非阻塞模式 * 参数 fd:套接字 */
void activate_nonblock(int fd)
{
    int iret = 0;
    int flags = fcntl(fd,F_GETFL);
    if(flags == -1)
        handle_error("fcntl");
    flags |= O_NONBLOCK;
    iret = fcntl(fd,F_SETFL,flags);
    if(iret == -1)
        handle_error("fcntl");
}

int main(void)
{
    init(FD_MAXSIZE);
    handle_sigpipe();       //捕获SIGPIPE信号

    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");


    //使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
    int on = 1;
    if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        close(sk_fd);
        handle_error("setsockopt");
    }


    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("bind");
    }

    //被动套接字
    if(listen(sk_fd, SOMAXCONN) < 0)     //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
    {
        close(sk_fd);
        handle_error("listen");
    }

    std::vector<int> clients;   //客户端连接套接字集合
    int epfd;           //epoll实例描述符

    //创建一个EPOLL实例,EPOLL_CLOEXEC指定当进程被替换时,自动关闭epfd
    epfd = epoll_create1(EPOLL_CLOEXEC);

    struct epoll_event event;
    event.data.fd = sk_fd;      //监听套接字
    event.events = EPOLLIN | EPOLLET;   //监听的事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, sk_fd, &event);  //监听套接字添加事件管理

    Eventlist events(16);   //记录哪些IO产生了事件
    struct sockaddr_in peeraddr;
    socklen_t peerlen;
    int conn_sk;
    int nready;
    int count = 0;      //客户端数目
    while(true)
    {
        //检测哪些IO发生了事件
        nready = epoll_wait(epfd, &*events.begin(), static_cast<int>(events.size()), -1);
        if(nready == 0)
        {
            printf("epoll_wait time out!\n");
            continue;
        }
        else if(nready == -1)
        {
            if(errno == EINTR)
                continue;
            handle_error("epoll_wait");
        }
        if(nready == static_cast<int>(events.size()))   //events集合空间不够用了
            events.resize(events.size()*2);

        //遍历事件
        for(int i = 0; i < nready; i++)
        {
            if(events[i].data.fd == sk_fd)  //监听套接字发生了事件
            {
                peerlen = sizeof(struct sockaddr_in);
                conn_sk = accept(sk_fd, (struct sockaddr*)&peeraddr, &peerlen);
                if(conn_sk < 0)
                {
                    if(errno == EINTR)
                        continue;
                    handle_error("accept");
                }

                clients.push_back(conn_sk);     //已连接套接字添加到客户端套接字集合中

                activate_nonblock(conn_sk);     //设置套接字为非阻塞模式

                event.data.fd = conn_sk;
                event.events = EPOLLIN | EPOLLET;   //监听的事件
                epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sk, &event);  //添加事件管理

                printf("Count = %d: Connect ip = %s\tport = %d\n",count++,inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
            }
            else if(events[i].events & EPOLLIN)
            {
                conn_sk = events[i].data.fd;
                if(conn_sk < 0)
                    continue;
                char recvbuf[1024] = {0};
                int iret = recvline(conn_sk,recvbuf,sizeof(recvbuf));
                if(iret == -1)
                    handle_error("recvline");
                if(iret == 0)
                {
                    printf("Client was closed!\n");
                    event = events[i];
                    //从监听集合中删除它
                    epoll_ctl(epfd, EPOLL_CTL_DEL, conn_sk,&event);
                    //删除该客户端的已连接套接字
                    clients.erase(std::remove(clients.begin(), clients.end(),conn_sk), clients.end());
                    close(conn_sk);
                }
                fputs(recvbuf, stdout);
                writen(conn_sk, recvbuf, strlen(recvbuf));
            }
        }
    }

    for(unsigned int i = 0; i < clients.size(); i++)
    {
        close(clients[i]);
    }
    close(sk_fd);
    return 0;
}

3、压力测试

编译上一篇中的压力测试客户端:

gcc -Wall -g -std=gnu99 connclient.c -o client

通过连接服务端,可明显发现速度快多了!

你可能感兴趣的:(Linux,编程)