epoll代码示例——handy库自带epoll.cc分析

前言

C++网络库有很多。handy是一个C++11风格的网络库,对深入学习C++有很大帮助。

代码分析

下面是来自handy/raw_examples下的epoll.cc文件。是水平触发的一个示例。该http服务器无论接收到什么样的请求,都返回一个静态资源123456。编译:c++ -o epoll epoll.cc,运行: sudo ./epoll。源代码中sendRes的if (con.writeEnabled)这句似乎有些问题,导致发送超大资源时出现问题。我已经做了修改,使之能够正确发送超大文件。

/*
 * 编译:c++ -o epoll epoll.cc
 * 运行: ./epoll
 * 测试:curl -v localhost
 */


/* 
    运行效果
    使用sudo 运行epoll程序。该程序在本机0.0.0.0的80端口监听,作为一个http服务器运行
    每当有连接访问时,返回静态资源httpRes
    LT是默认模式

 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;


bool output_log = true;
// 一个宏,用来打印错误并退出
#define exit_if(r, ...) if(r) {printf(__VA_ARGS__); printf("%s:%d error no: %d error msg %s\n", __FILE__, __LINE__, errno, strerror(errno)); exit(1);}
// 这个函数用于将指定的fd设置为非阻塞状态
void setNonBlock(int fd) {
    // 首先我们取出原来文件描述符的flags
    int flags = fcntl(fd, F_GETFL, 0);
    exit_if(flags<0, "fcntl failed");
    // 然后加上O_NONBLOCK,再设置回去
    int r = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    exit_if(r<0, "fcntl failed");
}
// 对epoll_ctl二次包装,将events 和 fd放入ev中。
// 并且设置events 设置为可读可写时触发
void updateEvents(int efd, int fd, int events, int op) {
    struct epoll_event ev = {0};
    ev.events = events;
    ev.data.fd = fd;
    printf("%s fd %d events read %d write %d\n",
           op==EPOLL_CTL_MOD?"mod":"add", fd, ev.events & EPOLLIN, ev.events & EPOLLOUT);
    int r = epoll_ctl(efd, op, fd, &ev);
    exit_if(r, "epoll_ctl failed");
}
// 尝试在fd上做accept操作。如果成功,将其加入到epoll fd的监听列表中。epoll的events设置为当数据有写入的时候触发。
void handleAccept(int efd, int fd) {
    struct sockaddr_in raddr;
    socklen_t rsz = sizeof(raddr);
    int cfd = accept(fd,(struct sockaddr *)&raddr,&rsz);
    exit_if(cfd<0, "accept failed");
    sockaddr_in peer, local;
    socklen_t alen = sizeof(peer);
    int r = getpeername(cfd, (sockaddr*)&peer, &alen);
    exit_if(r<0, "getpeername failed");
    printf("accept a connection from %s\n", inet_ntoa(raddr.sin_addr));
    setNonBlock(cfd);
    updateEvents(efd, cfd, EPOLLIN, EPOLL_CTL_ADD);
}
// 表示一个连接。成员有连接已读取的数据、已写入的数据
// string用来保存二进制内容没有问题吗,如果遇到\0会如何?
// 没有问题https://www.zhihu.com/question/33104941
struct Con {
    string readed;
    size_t written;
    bool writeEnabled;
    Con(): written(0), writeEnabled(false) {}
};
// 用来映射fd与con的数据结构
map cons;

string httpRes;
// 发送资源
void sendRes(int efd, int fd) {
    // 首先取得连接信息
    Con& con = cons[fd];
    // 没有接受到数据就要求写入
    // 说明其可能上一次继续发送的数据发送完了
    // 其对应的文件描述符在cons中已经删除
    // 然后触发了epoll信号
    // 此时关闭其上一次发送的标志
    // 然后关闭其缓冲区发送触发epoll的标志
    // 只保留其有数据可读时触发
    // 为什么不在写入完数据时就将这步做了呢?
    // if (!con.readed.length()) {
    //     if (con.writeEnabled) {
    //         updateEvents(efd, fd, EPOLLIN, EPOLL_CTL_MOD);
    //         con.writeEnabled = false;
    //     }
    //     return;
    // }
    // 计算还需要写入的数据长度
    size_t left = httpRes.length() - con.written;
    int wd = 0;
    // 连续写入数据,直到内核缓冲区无法写入数据为止
    while((wd=::write(fd, httpRes.data()+con.written, left))>0) {
        con.written += wd;
        left -= wd;
        if(output_log) printf("write %d bytes left: %lu\n", wd, left);
    };
    // 如果没有数据可以写入,则删除这个连接。但是不断开连接,即将连接信息置空
    if (left == 0) {
//        close(fd); // 测试中使用了keepalive,因此不关闭连接。连接会在read事件中关闭
        if (con.writeEnabled) {
            updateEvents(efd, fd, EPOLLIN, EPOLL_CTL_MOD);
            con.writeEnabled = false;
        }
        cons.erase(fd);
        return;
    }
    // 如果内核缓冲区满了,没办法写入了
    if (wd < 0 &&  (errno == EAGAIN || errno == EWOULDBLOCK)) {
        // 将其标记上可继续写
        if (!con.writeEnabled) {
            // 等待其可继续写,或可读
            // 避免重复进行系统调用,使用con.writeEnabled标记位
            printf("update it to EPOLLIN|EPOLLOUT\n");
            updateEvents(efd, fd, EPOLLIN|EPOLLOUT, EPOLL_CTL_MOD);
            con.writeEnabled = true;
        }
        return;
    }
    // 如果是其他情况,比如在没有写完数据时直接返回0,或者是返回了其他错误
    // 则说明出错了
    if (wd<=0) {
        printf("write error for %d: %d %s\n", fd, errno, strerror(errno));
        close(fd);
        cons.erase(fd);
    }
}
// 当loop once处理读取数据时,调用该函数
void handleRead(int efd, int fd) {
    char buf[4096];
    int n = 0;
    // 每次读取4k字节,循环读出当前内核中已存在的数据(有可能分包导致信息不完整)
    while ((n=::read(fd, buf, sizeof buf)) > 0) {
        if(output_log) printf("read %d bytes\n", n);
        // 这里通过一个map来获取之前fd对应的连接信息。
        // 当fd对应的下标不存在的时候,则会调用con的默认构造函数Con(): written(0), writeEnabled(false) {}
        string& readed = cons[fd].readed;
        // 调用string类的append方法将数据加入到连接信息中
        // 注意为了保证二进制安全需要传入参数n
        readed.append(buf, n);
        std::cout  << "now info is" << std::endl << "---" <<  readed << endl << "---" <<  std::endl;
        // 判断一个http请求发送完毕。
        // 不判断http请求的内容,一律发送静态资源
        if (readed.length()>4) {
            if (readed.substr(readed.length()-2, 2) == "\n\n" || readed.substr(readed.length()-4, 4) == "\r\n\r\n") {
                //当读取到一个完整的http请求,测试发送响应
                // TCP连接建立起来之后,客户端就开始传输首部,然后以\r\n\r\n来标志首部的结束和实体的开始(当然是请求里包含实体才会有实体的开始),
                // 接下来就是实体的传输,当实体传输完之后,客户端就开始接收数据,服务器就知道,这次请求就已经结束了,
                // 那么实体就是\r\n\r\n到停止接收的那么一段数据。对应的,客户端接收响应的时候也是这样。
                // 没有实体,则\r\n\r\n就是http的结束
                // 开始写入数据。注意有可能使缓冲区写满,若写满了则在之后继续写入
                sendRes(efd, fd);
            }
        }
    }
    // read无法读取的话,就会返回-1。此时errno(errno是属于线程的,是线程安全的)是EAGAIN代表没读完。EWOULDBLOCK和EAGAIN是一样的。
    // 那就返回,然后等待下次再读取
    if (n<0 && (errno == EAGAIN || errno == EWOULDBLOCK)){
        printf("nothing to read from %d, return. \n", fd);
        return;
    }
    //实际应用中,n<0应当检查各类错误,如EINTR
    if (n < 0) {
        printf("read %d error: %d %s\n", fd, errno, strerror(errno));
    }
    // 执行到这里,n是0,表示对端关闭连接。这时我们也关闭连接
    printf("%d close the connection\n", fd);
    close(fd);
    cons.erase(fd);
}
// 当loop once缓冲区可写的时候,简单的写入我们准备好的静态资源
void handleWrite(int efd, int fd) {
    sendRes(efd, fd);
}
// 对一个epoll句柄进行循环中的一次操作
// 其中l是LISTEN的fd
void loop_once(int efd, int lfd, int waitms) {
    // 最多让内核拷贝20个事件出来
    const int kMaxEvents = 20;
    struct epoll_event activeEvs[100];
    int n = epoll_wait(efd, activeEvs, kMaxEvents, waitms);
    // n是返回了多少个事件
    if(output_log) printf("epoll_wait return %d\n", n);
    for (int i = 0; i < n; i ++) {
        int fd = activeEvs[i].data.fd;
        int events = activeEvs[i].events;
        // EPOLLIN 事件或者是 EPOLLERR事件。EPOLLERR也代表管道写入结束。
        // 参见: http://man7.org/linux/man-pages/man2/epoll_ctl.2.html
        if (events & (EPOLLIN | EPOLLERR)) {
            // EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
            // 当对方关闭连接,则是EPOLLERR事件
            if (fd == lfd) {
                printf("this is accept\n");
                handleAccept(efd, fd); 
            } else {
                printf("this can read\n");
                handleRead(efd, fd);
            }
        } else if (events & EPOLLOUT) {
            // 这里处理文件描述符如果可以写入的事件
            // EPOLLOUT事件只有在连接时触发一次,表示可写
            // 之后表示缓冲区的数据已经送出,可以继续写入
            // 详见https://www.zhihu.com/question/22840801
            if(output_log) printf("handling epollout\n");
            handleWrite(efd, fd);
        } else {
            exit_if(1, "unknown event");
        }
    }
}

int main(int argc, const char* argv[]) {
    if (argc > 1) { output_log = false; }
    /* 
小知识
signal(参数1,参数2);
参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号时系统定义的宏。
参数2:我们处理的方式(是系统默认还是忽略还是捕获)。SIG_IGN: 如果func参数被设置为SIG_IGN,该信号将被忽略。
     */
    ::signal(SIGPIPE, SIG_IGN);
    // 设置http返回的内容
    httpRes = "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 19048576\r\n\r\n123456";
    // 将剩下的内容填充成0。最后content的长度是大约1024*1024
    for(int i=0;i<19048570;i++) {
        httpRes+='\0';
    }
    // 设置端口为80端口
    short port = 80;
    // 创建一个epoll句柄
    int epollfd = epoll_create(1);
    exit_if(epollfd < 0, "epoll_create failed");
    // 创建一个socket套接字
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    exit_if(listenfd < 0, "socket failed");
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    // 先绑定socket到端口上
    int r = ::bind(listenfd,(struct sockaddr *)&addr, sizeof(struct sockaddr));
    // 这一步如果没有超级用户的权限,就会报错。linux对于非root权限用户不能使用1024以下的端口
    exit_if(r, "bind to 0.0.0.0:%d failed %d %s", port, errno, strerror(errno));
    /* 
    #include
int listen(int sockfd, int backlog)
返回:0──成功, -1──失败
参数sockfd
被listen函数作用的套接字,sockfd之前由socket函数返回。在被socket函数返回的套接字fd之时,它是一个主动连接的套接字,
也就是此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,
也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。
参数backlog
这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。
因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。
如果这个情况出现了,服务器进程希望内核如何处理呢?
内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,
所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。
内核用于跟踪这些完成连接但是用户代码还没有通过accept调用的队列长度这里设置为了20。在队列长度小于20时,内核会立即完成连接的建立。
但是如果队列长度大于20,在用户代码调用accept之前该连接都不会建立,对方则处于阻塞状态。
     */
    r = listen(listenfd, 20);
    exit_if(r, "listen failed %d %s", errno, strerror(errno));
    printf("fd %d listening at %d\n", listenfd, port);
    // 接下来设置文件描述符为阻塞。
    // 为什么要设置为非阻塞?https://www.zhihu.com/question/23614342
    setNonBlock(listenfd);
    // 将其设置为可读取时触发,添加到epoll文件描述符池中
    updateEvents(epollfd, listenfd, EPOLLIN, EPOLL_CTL_ADD);
    for (;;) { //实际应用应当注册信号处理函数,退出时清理资源
        loop_once(epollfd, listenfd, 10000);
    }
    return 0;
}


运行效果

image.png
sudo ./epoll
fd 4 listening at 80
add fd 4 events read 1 write 0
epoll_wait return 1
this is accept
accept a connection from 127.0.0.1
add fd 5 events read 1 write 0
epoll_wait return 1
this can read
read 412 bytes
now info is
---GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,und;q=0.8,zh-TW;q=0.7,en;q=0.6,pl;q=0.5


---
write 4081834 bytes left: 14966851
update it to EPOLLIN|EPOLLOUT
mod fd 5 events read 1 write 4
nothing to read from 5, return.
epoll_wait return 1
handling epollout
write 2226422 bytes left: 12740429
epoll_wait return 1
handling epollout
write 2095456 bytes left: 10644973
epoll_wait return 1
handling epollout
write 1964490 bytes left: 8680483
epoll_wait return 1
handling epollout
write 1506109 bytes left: 7174374
epoll_wait return 1
handling epollout
write 1833524 bytes left: 5340850
epoll_wait return 1
handling epollout
write 1637075 bytes left: 3703775
write 130966 bytes left: 3572809
epoll_wait return 1
handling epollout
write 1571592 bytes left: 2001217
epoll_wait return 1
handling epollout
write 1440626 bytes left: 560591
epoll_wait return 1
handling epollout
write 560591 bytes left: 0
mod fd 5 events read 1 write 0
epoll_wait return 1
this can read
read 375 bytes
now info is
---GET /favicon.ico HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,und;q=0.8,zh-TW;q=0.7,en;q=0.6,pl;q=0.5


---
write 10477280 bytes left: 8571405
update it to EPOLLIN|EPOLLOUT
mod fd 5 events read 1 write 4
nothing to read from 5, return.
epoll_wait return 1
handling epollout
write 1440626 bytes left: 7130779
epoll_wait return 1
handling epollout
write 1768041 bytes left: 5362738
epoll_wait return 1
handling epollout
write 1571592 bytes left: 3791146
epoll_wait return 1
handling epollout
write 1637075 bytes left: 2154071
epoll_wait return 1
handling epollout
write 1702558 bytes left: 451513
epoll_wait return 1
handling epollout
write 451513 bytes left: 0
mod fd 5 events read 1 write 0
epoll_wait return 0
epoll_wait return 0
epoll_wait return 0

这里我发送的资源改大了,改成如下的数值:

    httpRes = "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 19048576\r\n\r\n123456";
    // 将剩下的内容填充成0。最后content的长度是大约1024*1024
    for(int i=0;i<19048570;i++) {
        httpRes+='\0';
    }

可以看到分了多次传输。最终终端页面上显示123456,后面全都是\0,不会显示。
可以看到浏览器请求了两次,一次请求根目录,一次请求页面的图标favicon.ico

你可能感兴趣的:(epoll代码示例——handy库自带epoll.cc分析)