C++網(wǎng)絡(luò)編程之IO多路復(fù)用(三)

概述

在前兩篇文章中,我們介紹了如何使用select和poll進(jìn)行IO多路復(fù)用。select通常有一個固定的文件描述符數(shù)量上限(通常是1024),poll雖然沒有嚴(yán)格的文件描述符數(shù)量限制,但在實際使用中也可能受到系統(tǒng)資源的限制。相比之下,epoll支持非常大的文件描述符數(shù)量(理論上可以達(dá)到系統(tǒng)文件描述符的最大值),因此更適合高并發(fā)場景。在本篇中,我們將重點介紹epoll。

epoll

epoll是Linux特有的IO多路復(fù)用接口,專為大規(guī)模并發(fā)場景設(shè)計。epoll相比于select和poll有更高的性能,因為它可以處理大量的文件描述符,并且在獲取事件時不需要遍歷整個文件描述符集合。epoll的內(nèi)部實現(xiàn)中使用了紅黑樹和就緒鏈表兩種數(shù)據(jù)結(jié)構(gòu),以便高效地管理和查詢事件。
epoll的核心思想是:創(chuàng)建一個事件表,這個事件表會將所有需要監(jiān)控的文件描述符和它們對應(yīng)的事件關(guān)聯(lián)起來。當(dāng)某個文件描述符上的事件發(fā)生時,內(nèi)核會將該事件添加到就緒列表中。用戶程序只需要從就緒列表中讀取事件即可,而不需要像select或poll那樣每次都需要遍歷整個文件描述符集來查找哪些事件已經(jīng)就緒。
與epoll相關(guān)的系統(tǒng)API主要有三個:epoll_create、epoll_ctl、epoll_wait,下面分別進(jìn)行介紹。
1、epoll_create函數(shù)用于創(chuàng)建一個epoll實例,并返回一個新的文件描述符。在較新的Linux版本中,推薦使用epoll_create1函數(shù)。epoll_create1是epoll_create的改進(jìn)版,允許指定標(biāo)志位。

int epoll_create(int size);
int epoll_create1(int flags);

size:這個參數(shù)在現(xiàn)代內(nèi)核中已經(jīng)不再使用,但在調(diào)用時仍需要傳遞一個大于零的值。它是歷史遺留下來的,用于向后兼容。
flags:可以是0或EPOLL_CLOEXEC。EPOLL_CLOEXEC標(biāo)志表示執(zhí)行exec函數(shù)族時,關(guān)閉此文件描述符。
返回值:成功時返回新的文件描述符(非負(fù)整數(shù)),失敗時返回-1,并設(shè)置errno。
2、epoll_ctl函數(shù)用于向epoll實例添加、修改或刪除感興趣的文件描述符。

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

epfd:由epoll_create或epoll_create1創(chuàng)建的epoll文件描述符。
op:操作類型,可以是以下之一:
(1)EPOLL_CTL_ADD:將文件描述符fd添加到epoll實例中。
(2)EPOLL_CTL_MOD:修改文件描述符fd在epoll實例中的事件。
(3)EPOLL_CTL_DEL:從epoll實例中移除文件描述符fd。
fd:要操作的文件描述符。
event:指向epoll_event結(jié)構(gòu)體的指針,定義了監(jiān)聽的事件類型和關(guān)聯(lián)的數(shù)據(jù)。
返回值:成功時返回0,失敗時返回-1,并設(shè)置errno。
3、epoll_wait函數(shù)用于等待并獲取就緒的IO事件。

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

epfd:由epoll_create或epoll_create1創(chuàng)建的epoll文件描述符。
events:指向epoll_event數(shù)組的指針,用來存放發(fā)生的事件。
maxevents:events數(shù)組的最大長度。
timeout:等待時間,單位為毫秒。-1表示無限等待,0表示立即返回,不阻塞。
返回值:成功時返回發(fā)生的事件數(shù)量,失敗時返回-1,并設(shè)置errno。

邊緣觸發(fā)模式

epoll支持邊緣觸發(fā)(Edge-Triggered,即ET)模式,select和poll不支持,它們僅支持水平觸發(fā)模式(Level-Triggered,即LT)。邊緣觸發(fā)模式是一種高效的IO事件通知機(jī)制,它與水平觸發(fā)模式有所不同。在邊緣觸發(fā)模式下,epoll只會在文件描述符的狀態(tài)發(fā)生變化時通知應(yīng)用程序一次。這意味著,如果應(yīng)用程序沒有處理完所有數(shù)據(jù),內(nèi)核將不會再次通知該事件,直到新的數(shù)據(jù)到達(dá)或狀態(tài)再次發(fā)生變化。
epoll的邊緣觸發(fā)模式具有以下幾個特點。
1、僅在狀態(tài)變化時通知。當(dāng)文件描述符從不可讀變?yōu)榭勺x,或者從不可寫變?yōu)榭蓪憰r,epoll會通知應(yīng)用程序。如果應(yīng)用程序沒有完全讀取或?qū)懭胨袛?shù)據(jù),那么剩余的數(shù)據(jù)將不會再次觸發(fā)事件,直到有新的數(shù)據(jù)到來或狀態(tài)再次變化。
2、非阻塞IO。在邊緣觸發(fā)模式下,必須使用非阻塞IO。這是因為當(dāng)read或write調(diào)用返回EAGAIN或EWOULDBLOCK時,表示當(dāng)前沒有更多數(shù)據(jù)可以讀取或?qū)懭?,但并不意味著文件描述符已?jīng)不可讀或不可寫。應(yīng)用程序需要繼續(xù)嘗試讀取或?qū)懭?,直到所有?shù)據(jù)處理完畢。
3、更高的性能。由于邊緣觸發(fā)模式減少了內(nèi)核和用戶空間之間的上下文切換次數(shù),因此通常比水平觸發(fā)模式更高效,特別是在高并發(fā)場景中。
4、更復(fù)雜的編程模型。使用邊緣觸發(fā)模式需要更加小心地處理IO操作,以確保不會遺漏任何數(shù)據(jù)。

實戰(zhàn)代碼

在下面的示例代碼中,我們使用epoll函數(shù)實現(xiàn)了TCP服務(wù)器的IO多路復(fù)用。
首先,我們創(chuàng)建一個監(jiān)聽套接字listen_sock,將其綁定到指定的端口8888,并開始監(jiān)聽連接請求。
然后,我們使用epoll_create1創(chuàng)建一個epoll實例,并檢查是否成功。接著,將監(jiān)聽套接字listen_sock添加到 epoll實例中,并設(shè)置為邊緣觸發(fā)模式(EPOLLET)和可讀事件(EPOLLIN)。
在主循環(huán)中,我們使用epoll_wait函數(shù)等待IO事件的發(fā)生。epoll_wait會阻塞,直到有事件發(fā)生或超時,返回值nfds是就緒事件的數(shù)量。遍歷events數(shù)組中的每一個就緒事件,檢查每個事件的文件描述符fd是否為監(jiān)聽套接字listen_sock。
如果事件的文件描述符是監(jiān)聽套接字listen_sock,則表示有新的連接請求。調(diào)用accept接受新連接,并創(chuàng)建一個新的客戶端套接字conn_sock。將新連接的套接字添加到epoll實例中,并設(shè)置為邊緣觸發(fā)模式(EPOLLET)和可讀事件(EPOLLIN)。
如果事件的文件描述符不是監(jiān)聽套接字listen_sock,則表示該文件描述符上有數(shù)據(jù)可讀。調(diào)用read函數(shù)讀取數(shù)據(jù),并回顯給客戶端。如果讀取到0字節(jié),表示客戶端已斷開連接。如果讀取失敗,也認(rèn)為客戶端斷開連接。在這兩種情況下,關(guān)閉套接字,并從epoll實例中移除該套接字。
最后,當(dāng)程序退出時,關(guān)閉所有打開的套接字和epoll實例。

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>

using namespace std;

#define MAX_EVENTS                  10

int main()
{
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1)
    {
        cout << "Create socket failed" << endl;
        return 1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
    {
        cout << "Bind failed" << endl;
        close(listen_sock);
        return 1;
    }

    if (listen(listen_sock, 5) == -1)
    {
        cout << "Listen failed" << endl;
        close(listen_sock);
        return 1;
    }

    // 創(chuàng)建epoll實例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1)
    {
        cout << "Create epoll failed" << endl;
        close(listen_sock);
        return 1;
    }

    // 添加監(jiān)聽Socket
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listen_sock;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev);

    while (true)
    {
        // 等待并獲取就緒的IO事件
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1)
        {
            cout << "Wait epoll failed" << endl;
            break;
        }

        for (int i = 0; i < nfds; ++i)
        {
            if (events[i].data.fd == listen_sock)
            {
                // 新連接
                struct sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr, 
                    &client_len);
                if (conn_sock != -1)
                {
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = conn_sock;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_sock, &ev);
                }
                else
                {
                    cout << "New connection" << endl;
                }
            }
            else
            {
                // 處理數(shù)據(jù)
                char buf[1024];
                ssize_t nread = read(events[i].data.fd, buf, sizeof(buf));
                if (nread > 0)
                {
                    // 接收并回顯數(shù)據(jù)
                    cout << "Received data: " << string(buf, nread) << endl;
                    write(events[i].data.fd, buf, nread);
                }
                else if (nread == 0)
                {
                    // 客戶端關(guān)閉連接
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
                    close(events[i].data.fd);
                }
                else
                {
                    // 發(fā)生錯誤
                    cout << "Read error" << endl;
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
                    close(events[i].data.fd);
                }
            }
        }
    }

    close(listen_sock);
    close(epoll_fd);
    return 0;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容