国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

《TCP IP網(wǎng)絡(luò)編程》第十七章

這篇具有很好參考價(jià)值的文章主要介紹了《TCP IP網(wǎng)絡(luò)編程》第十七章。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

第 17 章 優(yōu)于 select 的 epoll

17.1 epoll 理解及應(yīng)用

????????select 復(fù)用方法由來已久,因此,利用該技術(shù)后,無論如何優(yōu)化程序性能也無法同時(shí)介入上百個(gè)客戶端。這種 select 方式并不適合以 web 服務(wù)器端開發(fā)為主流的現(xiàn)代開發(fā)環(huán)境,所以需要學(xué)習(xí) Linux 環(huán)境下的 epoll。

基于 select 的 I/O 復(fù)用技術(shù)速度慢的原因:

????????第 12 章實(shí)現(xiàn)了基于 select 的 I/O 復(fù)用技術(shù)服務(wù)端,其中有不合理的設(shè)計(jì)如下:

  • 調(diào)用 select 函數(shù)后常見的針對(duì)所有文件描述符的循環(huán)語句。
  • 每次調(diào)用 select 函數(shù)時(shí)都需要向該函數(shù)傳遞監(jiān)視對(duì)象信息。

????????調(diào)用 select 函數(shù)后,并不是把發(fā)生變化的文件描述符單獨(dú)集中在一起,而是通過作為監(jiān)視對(duì)象的 fd_set 變量的變化,找出發(fā)生變化的文件描述符。因此無法避免針對(duì)所有監(jiān)視對(duì)象的循環(huán)語句。而且,作為監(jiān)視對(duì)象的 fd_set 會(huì)發(fā)生變化,所以調(diào)用 select 函數(shù)前應(yīng)該復(fù)制并保存原有信息,并在每次調(diào)用 select 函數(shù)時(shí)傳遞新的監(jiān)視對(duì)象信息。

????????select 性能上最大的弱點(diǎn)是:每次傳遞監(jiān)視對(duì)象信息。準(zhǔn)確的說,select 是監(jiān)視套接字變化的函數(shù)。而套接字是操作系統(tǒng)管理的,所以 select 函數(shù)要借助操作系統(tǒng)才能完成功能。select 函數(shù)的這一缺點(diǎn)可以通過如下方式彌補(bǔ):

????????僅向操作系統(tǒng)傳遞一次監(jiān)視對(duì)象,監(jiān)視范圍或內(nèi)容發(fā)生變化時(shí)只通知發(fā)生變化的事項(xiàng)。

????????這樣就無需每次調(diào)用 select 函數(shù)時(shí)都向操作系統(tǒng)傳遞監(jiān)視對(duì)象信息,但是前提操作系統(tǒng)支持這種處理方式。Linux 的支持方式是 epoll ,Windows 的支持方式是 IOCP。

select 也有優(yōu)點(diǎn):

????????select 的兼容性比較高,這樣就可以支持很多的操作系統(tǒng),不受平臺(tái)的限制,滿足以下兩個(gè)條件使可以使用 select 函數(shù):

  • 服務(wù)器接入者少
  • 程序應(yīng)該具有兼容性

實(shí)現(xiàn) epoll 時(shí)必要的函數(shù)和結(jié)構(gòu)體:

????????能夠克服 select 函數(shù)缺點(diǎn)的 epoll 函數(shù)具有以下優(yōu)點(diǎn),這些優(yōu)點(diǎn)正好與之前的 select 函數(shù)缺點(diǎn)相反。

  • 無需編寫以監(jiān)視狀態(tài)變化為目的的針對(duì)所有文件描述符的循環(huán)語句
  • 調(diào)用對(duì)應(yīng)于 select 函數(shù)的 epoll_wait 函數(shù)時(shí)無需每次傳遞監(jiān)視對(duì)象信息。

????????下面是 epoll 函數(shù)的功能:

  • epoll_create:創(chuàng)建保存 epoll 文件描述符的空間
  • epoll_ctl:向空間注冊(cè)并注銷文件描述符
  • epoll_wait:與 select 函數(shù)類似,等待文件描述符發(fā)生變化

????????select 函數(shù)中為了保存監(jiān)視對(duì)象的文件描述符,直接聲明了 fd_set 變量,但 epoll 方式下的操作系統(tǒng)負(fù)責(zé)保存監(jiān)視對(duì)象文件描述符,因此需要向操作系統(tǒng)請(qǐng)求創(chuàng)建保存文件描述符的空間,此時(shí)用的函數(shù)就是 epoll_create

????????此外,為了添加和刪除監(jiān)視對(duì)象文件描述符,select 方式中需要 FD_SET、FD_CLR 函數(shù)。但在 epoll 方式中,通過 epoll_ctl 函數(shù)請(qǐng)求操作系統(tǒng)完成。

????????最后,select 方式下調(diào)用 select 函數(shù)等待文件描述符的變化,而 epoll方式則調(diào)用 epoll_wait 函數(shù)。

????????select 方式中通過 fd_set 變量查看監(jiān)視對(duì)象的狀態(tài)變化,而 epoll 方式通過如下結(jié)構(gòu)體 epoll_event 將發(fā)生變化的文件描述符單獨(dú)集中在一起。下面為其結(jié)構(gòu)體:

struct epoll_event
{
    __uint32_t events;
    epoll_data_t data;
};
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

????????聲明足夠大的 epoll_event 結(jié)構(gòu)體數(shù)組候,傳遞給 epoll_wait 函數(shù)時(shí),發(fā)生變化的文件描述符信息將被填入數(shù)組。因此,無需像 select 函數(shù)那樣針對(duì)所有文件描述符進(jìn)行循環(huán)。

epoll_create:

????????下面是 epoll_create 函數(shù)的原型:

#include <sys/epoll.h>
int epoll_create(int size);
/*
成功時(shí)返回 epoll 的文件描述符,失敗時(shí)返回 -1
size:epoll 實(shí)例的大小
*/

????????調(diào)用 epoll_create 函數(shù)時(shí)創(chuàng)建的文件描述符保存空間稱為「epoll 例程」,但有些情況下名稱不同,需要稍加注意。通過參數(shù) size 傳遞的值決定 epoll 例程的大小,但該值只是向操作系統(tǒng)提出的建議。換言之,size 并不用來決定 epoll 的大小,而僅供操作系統(tǒng)參考

(Linux 2.6.8 之后的內(nèi)核將完全忽略傳入 epoll_create 函數(shù)的 size 函數(shù),因此內(nèi)核會(huì)根據(jù)情況調(diào)整 epoll 例程大小。)

????????epoll_create 函數(shù)創(chuàng)建的資源與套接字相同,也由操作系統(tǒng)管理。因此,該函數(shù)和創(chuàng)建套接字的情況相同,也會(huì)返回文件描述符,也就是說返回的文件描述符主要用于區(qū)分 epoll 例程。需要終止時(shí),與其他文件描述符相同,也要調(diào)用 close 函數(shù)。

epoll_ctl:

????????生成例程后,應(yīng)在其內(nèi)部注冊(cè)監(jiān)視對(duì)象文件描述符,此時(shí)使用 epoll_ctl 函數(shù)。

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
成功時(shí)返回 0 ,失敗時(shí)返回 -1
epfd:用于注冊(cè)監(jiān)視對(duì)象的 epoll 例程的文件描述符
op:用于指定監(jiān)視對(duì)象的添加、刪除或更改等操作
fd:需要注冊(cè)的監(jiān)視對(duì)象文件描述符
event:監(jiān)視對(duì)象的事件類型
*/

????????與其他 epoll 函數(shù)相比,該函數(shù)看起來有些復(fù)雜,但通過調(diào)用語句就很容易理解,假設(shè)按照如下形式調(diào)用 epoll_ctl 函數(shù):

epoll_ctl(A,EPOLL_CTL_ADD,B,C);

????????第二個(gè)參數(shù) EPOLL_CTL_ADD 意味著「添加」,上述語句有如下意義:

epoll 例程 A 中注冊(cè)文件描述符 B ,主要目的是為了監(jiān)視參數(shù) C 中的事件。

????????再介紹一個(gè)調(diào)用語句:

epoll_ctl(A,EPOLL_CTL_DEL,B,NULL);

????????上述語句中第二個(gè)參數(shù)意味著「刪除」,有以下含義:

從 epoll 例程 A 中刪除文件描述符 B。

????????從上述示例中可以看出,從監(jiān)視對(duì)象中刪除時(shí),不需要監(jiān)視類型,因此向第四個(gè)參數(shù)可以傳遞為 NULL。

????????下面是第二個(gè)參數(shù)的類型及其含義:

  • EPOLL_CTL_ADD:將文件描述符注冊(cè)到 epoll 例程
  • EPOLL_CTL_DEL:從 epoll 例程中刪除文件描述符
  • EPOLL_CTL_MOD:更改注冊(cè)的文件描述符的關(guān)注事件發(fā)生情況

????????epoll_event 結(jié)構(gòu)體用于保存事件的文件描述符結(jié)合。但也可以在 epoll 例程中注冊(cè)文件描述符時(shí),用于注冊(cè)關(guān)注的事件。該函數(shù)中 epoll_event 結(jié)構(gòu)體的定義并不顯眼,因此通過調(diào)用語句說明該結(jié)構(gòu)體在 epoll_ctl 函數(shù)中的應(yīng)用。

struct epoll_event event;
...
event.events=EPOLLIN;//發(fā)生需要讀取數(shù)據(jù)的情況時(shí)
event.data.fd=sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
...

????????上述代碼將sockfd 注冊(cè)到 epoll 例程 epfd 中,并在需要讀取數(shù)據(jù)的情況下產(chǎn)生相應(yīng)事件。接下來給出 epoll_event 的成員 events 中可以保存的常量及所指的事件類型。

  • EPOLLIN:需要讀取數(shù)據(jù)的情況
  • EPOLLOUT:輸出緩沖為空,可以立即發(fā)送數(shù)據(jù)的情況
  • EPOLLPRI:收到 OOB 數(shù)據(jù)的情況
  • EPOLLRDHUP:斷開連接或半關(guān)閉的情況,這在邊緣觸發(fā)方式下非常有用
  • EPOLLERR:發(fā)生錯(cuò)誤的情況
  • EPOLLET:以邊緣觸發(fā)的方式得到事件通知
  • EPOLLONESHOT:發(fā)生一次事件后,相應(yīng)文件描述符不再收到事件通知。因此需要向 epoll_ctl 函數(shù)的第二個(gè)參數(shù)傳遞 EPOLL_CTL_MOD ,再次設(shè)置事件。

????????可通過位或運(yùn)算同時(shí)傳遞多個(gè)上述參數(shù)。

epoll_wait:

????????下面是函數(shù)原型:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
成功時(shí)返回發(fā)生事件的文件描述符個(gè)數(shù),失敗時(shí)返回 -1
epfd : 表示事件發(fā)生監(jiān)視范圍的 epoll 例程的文件描述符
events : 保存發(fā)生事件的文件描述符集合的結(jié)構(gòu)體地址值
maxevents : 第二個(gè)參數(shù)中可以保存的最大事件數(shù)
timeout : 以 1/1000 秒為單位的等待時(shí)間,傳遞 -1 時(shí),一直等待直到發(fā)生事件
*/

????????該函數(shù)調(diào)用方式如下。需要注意的是,第二個(gè)參數(shù)所指緩沖需要?jiǎng)討B(tài)分配。

int event_cnt;
struct epoll_event *ep_events;
...
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
...
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
...

????????調(diào)用函數(shù)后,返回發(fā)生事件的文件描述符個(gè)數(shù),同時(shí)在第二個(gè)參數(shù)指向的緩沖中保存發(fā)生事件的文件描述符集合。因此,無需像 select 一樣插入針對(duì)所有文件描述符的循環(huán)。

基于 epoll 的回聲服務(wù)器端:

????????下面是帶詳細(xì)注釋的回聲服務(wù)器端的代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }
    // 創(chuàng)建服務(wù)器套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    // 綁定套接字到指定地址和端口
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    // 監(jiān)聽套接字
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    // 創(chuàng)建 epoll 實(shí)例
    epfd = epoll_create(EPOLL_SIZE); //可以忽略這個(gè)參數(shù),填入的參數(shù)為操作系統(tǒng)參考
    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    // 將服務(wù)器套接字加入 epoll 監(jiān)聽
    event.events = EPOLLIN; //需要讀取數(shù)據(jù)的情況
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是監(jiān)聽 enevt 中的事件

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //獲取改變了的文件描述符,返回?cái)?shù)量
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        for (i = 0; i < event_cnt; i++)
        {
            // 如果是服務(wù)器套接字的事件
            if (ep_events[i].data.fd == serv_sock) //客戶端請(qǐng)求連接時(shí)
            {
                adr_sz = sizeof(clnt_adr);
                // 接受客戶端連接
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock; //把客戶端套接字添加進(jìn)去
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client : %d \n", clnt_sock);
            }
            else //是客戶端套接字時(shí)
            {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //從epoll中刪除套接字
                    close(ep_events[i].data.fd);
                    printf("closed client : %d \n", ep_events[i].data.fd);
                }
                else // 讀取數(shù)據(jù)并回傳給客戶端
                {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }
    // 關(guān)閉套接字和 epoll 實(shí)例
    close(serv_sock);
    close(epfd);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

? ? ? ? 實(shí)驗(yàn)略。運(yùn)行結(jié)果和以前 select 實(shí)現(xiàn)的和 fork 實(shí)現(xiàn)的結(jié)果一樣,都可以支持多客戶端同時(shí)運(yùn)行。但是運(yùn)用?epoll 效率高于 select。

? ? ? ? 下面總結(jié)一下epoll的流程:

  1. 創(chuàng)建套接字:首先,創(chuàng)建一個(gè)套接字(一般是服務(wù)器套接字),用于監(jiān)聽連接請(qǐng)求或數(shù)據(jù)傳輸。

  2. 創(chuàng)建 epoll 實(shí)例:通過調(diào)用 epoll_create() 函數(shù)創(chuàng)建一個(gè) epoll 實(shí)例,用于管理文件描述符的事件。

  3. 注冊(cè)事件:將要監(jiān)聽的套接字(文件描述符)注冊(cè)到 epoll 實(shí)例中,通過調(diào)用 epoll_ctl() 函數(shù),并設(shè)置感興趣的事件類型(如讀事件、寫事件等)以及相關(guān)的數(shù)據(jù)(如文件描述符本身)。

  4. 進(jìn)入事件循環(huán):進(jìn)入一個(gè)循環(huán),在循環(huán)中調(diào)用 epoll_wait() 函數(shù)來等待事件的發(fā)生。epoll_wait() 會(huì)阻塞程序直到有事件發(fā)生或超時(shí)。

  5. 處理事件:一旦有事件發(fā)生,epoll_wait() 函數(shù)會(huì)返回發(fā)生事件的文件描述符集合。在事件循環(huán)內(nèi),遍歷這些事件,根據(jù)文件描述符的類型進(jìn)行不同的處理:

    • 如果是服務(wù)器套接字上的事件,表示有新的連接請(qǐng)求,使用 accept() 函數(shù)接受連接,然后將新的客戶端套接字注冊(cè)到 epoll 實(shí)例中。
    • 如果是客戶端套接字上的事件,表示有數(shù)據(jù)傳輸事件發(fā)生,可以使用 read() 函數(shù)讀取數(shù)據(jù),并根據(jù)需要進(jìn)行處理,然后使用 write() 函數(shù)回傳數(shù)據(jù)。
  6. 移除事件:如果需要,可以在事件處理后將文件描述符從 epoll 實(shí)例中移除,通過調(diào)用 epoll_ctl() 函數(shù),通常是在客戶端關(guān)閉連接時(shí)執(zhí)行。

  7. 關(guān)閉套接字和 epoll 實(shí)例:在程序結(jié)束時(shí),確保關(guān)閉所有的套接字和釋放分配的內(nèi)存,包括關(guān)閉 epoll 實(shí)例。

????????select 和 epoll 的區(qū)別:?

  • 每次調(diào)用 select 函數(shù)都會(huì)向操作系統(tǒng)傳遞監(jiān)視對(duì)象信息,浪費(fèi)大量時(shí)間。
  • epoll 僅向操作系統(tǒng)傳遞一次監(jiān)視對(duì)象,監(jiān)視范圍或內(nèi)容發(fā)生變化時(shí)只通知發(fā)生變化的事項(xiàng)。

17.2 條件觸發(fā)和邊緣觸發(fā)

????????學(xué)習(xí) epoll 時(shí)要了解條件觸發(fā)(Level Trigger)和邊緣觸發(fā)(Edge Trigger)。

條件觸發(fā)和邊緣觸發(fā)的區(qū)別在于發(fā)生事件的時(shí)間點(diǎn):

  • 條件觸發(fā)的特性條件觸發(fā)方式中,只要輸入緩沖有數(shù)據(jù)就會(huì)一直通知該事件

????????例如,服務(wù)器端輸入緩沖收到 50 字節(jié)數(shù)據(jù)時(shí),服務(wù)器端操作系統(tǒng)將通知該事件(注冊(cè)到發(fā)生變化的文件描述符)。但是服務(wù)器端讀取 20 字節(jié)后還剩下 30 字節(jié)的情況下,仍會(huì)注冊(cè)事件。也就是說,條件觸發(fā)方式中,只要輸入緩沖中還剩有數(shù)據(jù),就將以事件方式再次注冊(cè)。

  • 邊緣觸發(fā)特性邊緣觸發(fā)中輸入緩沖收到數(shù)據(jù)時(shí)僅注冊(cè) 1 次該事件。即使輸入緩沖中還留有數(shù)據(jù),也不會(huì)再進(jìn)行注冊(cè)。

掌握條件觸發(fā)的事件特性:

????????epoll 默認(rèn)以條件觸發(fā)的方式工作,因此可以通過示例驗(yàn)證條件觸發(fā)的特性。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 2
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    epfd = epoll_create(EPOLL_SIZE); //可以忽略這個(gè)參數(shù),填入的參數(shù)為操作系統(tǒng)參考
    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    event.events = EPOLLIN; //需要讀取數(shù)據(jù)的情況
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是監(jiān)聽 enevt 中的事件

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //獲取改變了的文件描述符,返回?cái)?shù)量
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait");
        for (i = 0; i < event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock) //客戶端請(qǐng)求連接時(shí)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock; //把客戶端套接字添加進(jìn)去
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client : %d \n", clnt_sock);
            }
            else //是客戶端套接字時(shí)
            {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //從epoll中刪除套接字
                    close(ep_events[i].data.fd);
                    printf("closed client : %d \n", ep_events[i].data.fd);
                }
                else
                {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

????????上面的代碼把調(diào)用 read 函數(shù)時(shí)使用的緩沖大小縮小到了 4 個(gè)字節(jié),插入了驗(yàn)證 epoll_wait 調(diào)用次數(shù)的驗(yàn)證函數(shù)。減少緩沖大小是為了阻止服務(wù)器端一次性讀取接收的數(shù)據(jù)。換言之,調(diào)用 read 函數(shù)后,輸入緩沖中仍有數(shù)據(jù)要讀取,而且會(huì)因此注冊(cè)新的事件并從 epoll_wait 函數(shù)返回時(shí)將循環(huán)輸出「return epoll_wait」字符串。

運(yùn)行結(jié)果:

《TCP IP網(wǎng)絡(luò)編程》第十七章,《TCPIP網(wǎng)絡(luò)編程》,網(wǎng)絡(luò),服務(wù)器,運(yùn)維,網(wǎng)絡(luò)編程

????????從結(jié)果可以看出,每當(dāng)收到客戶端數(shù)據(jù)時(shí),都會(huì)注冊(cè)該事件,并因此調(diào)用 epoll_wait 函數(shù)。

????????下面的代碼是修改后的邊緣觸發(fā)方式的代碼,僅僅是把上面的某一行代碼改為:

 event.events = EPOLLIN | EPOLLET;

? ? ? ? ?運(yùn)行結(jié)果:

《TCP IP網(wǎng)絡(luò)編程》第十七章,《TCPIP網(wǎng)絡(luò)編程》,網(wǎng)絡(luò),服務(wù)器,運(yùn)維,網(wǎng)絡(luò)編程

????????從上面的例子看出,接收到客戶端的消息時(shí),只輸出一次「return epoll_wait」字符串,這證明僅注冊(cè)了一次事件。?

????????select 模型是以條件觸發(fā)的方式工作的。

邊緣觸發(fā)的服務(wù)器端必知的兩點(diǎn):

  • 通過 errno 變量驗(yàn)證錯(cuò)誤原因
  • 為了完成非阻塞(Non-blocking)I/O ,更改了套接字特性。

????????Linux 套接字相關(guān)函數(shù)一般通過 -1 通知發(fā)生了錯(cuò)誤。雖然知道發(fā)生了錯(cuò)誤,但僅憑這些內(nèi)容無法得知產(chǎn)生錯(cuò)誤的原因。因此,為了在發(fā)生錯(cuò)誤的時(shí)候提額外的信息,Linux 聲明了如下全局變量:

int errno;

????????為了訪問該變量,需要引入?error.h?頭文件,因此此頭文件有上述變量的 extern 聲明。另外,每種函數(shù)發(fā)生錯(cuò)誤時(shí),保存在 errno 變量中的值都不同。

????????read 函數(shù)發(fā)現(xiàn)輸入緩沖中沒有數(shù)據(jù)可讀時(shí)返回 -1,同時(shí)在 errno 中保存 EAGAIN 常量。

????????下面是 Linux 中提供的改變和更改文件屬性的辦法:

#include <fcntl.h>
int fcntl(int fields, int cmd, ...);
/*
成功時(shí)返回 cmd 參數(shù)相關(guān)值,失敗時(shí)返回 -1
filedes : 屬性更改目標(biāo)的文件描述符
cmd : 表示函數(shù)調(diào)用目的
*/

????????從上述聲明可以看出 fcntl 有可變參數(shù)的形式。如果向第二個(gè)參數(shù)傳遞 F_GETFL ,可以獲得第一個(gè)參數(shù)所指的文件描述符屬性(int 型)。反之,如果傳遞 F_SETFL ,可以更改文件描述符屬性。若希望將文件(套接字)改為非阻塞模式,需要如下 2 條語句。

int flag = fcntl(fd,F_GETFL,0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);

????????通過第一條語句,獲取之前設(shè)置的屬性信息,通過第二條語句在此基礎(chǔ)上添加非阻塞 O_NONBLOCK 標(biāo)志。調(diào)用 read/write 函數(shù)時(shí),無論是否存在數(shù)據(jù),都會(huì)形成非阻塞文件(套接字)。fcntl 函數(shù)的適用范圍很廣。

實(shí)現(xiàn)邊緣觸發(fā)回聲服務(wù)器端:

????????通過 errno 確認(rèn)錯(cuò)誤的原因是:邊緣觸發(fā)方式中,接收數(shù)據(jù)僅注冊(cè)一次該事件。

????????因?yàn)檫@種特點(diǎn),一旦發(fā)生輸入相關(guān)事件時(shí),就應(yīng)該讀取輸入緩沖中的全部數(shù)據(jù)。因此需要驗(yàn)證輸入緩沖是否為空。

????????read 函數(shù)返回 -1,變量 errno 中的值變成 EAGAIN 時(shí),說明沒有數(shù)據(jù)可讀。

????????既然如此,為什么要將套接字變成非阻塞模式?邊緣觸發(fā)條件下,以阻塞方式工作的 read & write 函數(shù)有可能引起服務(wù)端的長(zhǎng)時(shí)間停頓。因此,邊緣觸發(fā)方式中一定要采用非阻塞 read & write 函數(shù)。

????????下面是以邊緣觸發(fā)方式工作的回聲服務(wù)端代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

#define BUF_SIZE 4 //緩沖區(qū)設(shè)置為 4 字節(jié)
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    epfd = epoll_create(EPOLL_SIZE); //可以忽略這個(gè)參數(shù),填入的參數(shù)為操作系統(tǒng)參考
    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    setnonblockingmode(serv_sock);
    event.events = EPOLLIN; //需要讀取數(shù)據(jù)的情況
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是監(jiān)聽 enevt 中的事件

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //獲取改變了的文件描述符,返回?cái)?shù)量
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait");
        for (i = 0; i < event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock) //客戶端請(qǐng)求連接時(shí)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                setnonblockingmode(clnt_sock);    //將 accept 創(chuàng)建的套接字改為非阻塞模式
                event.events = EPOLLIN | EPOLLET; //改成邊緣觸發(fā)
                event.data.fd = clnt_sock;        //把客戶端套接字添加進(jìn)去
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client : %d \n", clnt_sock);
            }
            else //是客戶端套接字時(shí)
            {
                while (1)
                {
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0)
                    {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //從epoll中刪除套接字
                        close(ep_events[i].data.fd);
                        printf("closed client : %d \n", ep_events[i].data.fd);
                        break;
                    }
                    else if (str_len < 0)
                    {
                        if (errno == EAGAIN) //read 返回-1 且 errno 值為 EAGAIN ,意味讀取了輸入緩沖的全部數(shù)據(jù)
                            break;
                    }
                    else
                    {
                        write(ep_events[i].data.fd, buf, str_len);
                    }
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
void setnonblockingmode(int fd)
{
    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

????????運(yùn)行結(jié)果:《TCP IP網(wǎng)絡(luò)編程》第十七章,《TCPIP網(wǎng)絡(luò)編程》,網(wǎng)絡(luò),服務(wù)器,運(yùn)維,網(wǎng)絡(luò)編程

? ? ? ? ?從上述結(jié)果可以看出:客戶端從請(qǐng)求連接到斷開連接一共發(fā)送5次數(shù)據(jù),服務(wù)器端也相應(yīng)產(chǎn)生5個(gè)事件。

條件觸發(fā)和邊緣觸發(fā)孰優(yōu)孰劣:

????????邊緣觸發(fā)方式可以做到:分離接收數(shù)據(jù)和處理數(shù)據(jù)的時(shí)間點(diǎn)。如圖:

?

????????《TCP IP網(wǎng)絡(luò)編程》第十七章,《TCPIP網(wǎng)絡(luò)編程》,網(wǎng)絡(luò),服務(wù)器,運(yùn)維,網(wǎng)絡(luò)編程

運(yùn)行流程如下:

  • 服務(wù)器端分別從 A B C 接收數(shù)據(jù)
  • 服務(wù)器端按照 A B C 的順序重新組合接收到的數(shù)據(jù)
  • 組合的數(shù)據(jù)將發(fā)送給任意主機(jī)。

為了完成這個(gè)過程,如果可以按照如下流程運(yùn)行,服務(wù)端的實(shí)現(xiàn)并不難:

  • 客戶端按照 A B C 的順序連接服務(wù)器,并且按照次序向服務(wù)器發(fā)送數(shù)據(jù)
  • 需要接收數(shù)據(jù)的客戶端應(yīng)在客戶端 A B C 之前連接到服務(wù)器端并等待

但是實(shí)際情況中可能是下面這樣:

  • 客戶端 C 和 B 正在向服務(wù)器發(fā)送數(shù)據(jù),但是 A 并沒有連接到服務(wù)器
  • 客戶端 A B C 亂序發(fā)送數(shù)據(jù)
  • 服務(wù)端已經(jīng)接收到數(shù)據(jù),但是要接收數(shù)據(jù)的目標(biāo)客戶端并沒有連接到服務(wù)器端。

????????因此,即使輸入緩沖收到數(shù)據(jù),服務(wù)器端也能決定讀取和處理這些數(shù)據(jù)的時(shí)間點(diǎn),這樣就給服務(wù)器端的實(shí)現(xiàn)帶來很大靈活性。

習(xí)題:

1、利用 select 函數(shù)實(shí)現(xiàn)服務(wù)器端時(shí),代碼層面存在的兩個(gè)缺點(diǎn)是?

????????使用 select 需要編寫復(fù)雜的邏輯來處理不同的文件描述符狀態(tài),包括讀、寫、異常等情況。隨著連接數(shù)量增加,代碼的可讀性和維護(hù)難度也會(huì)增加,容易引入邏輯錯(cuò)誤。

????????select 是一種阻塞式的調(diào)用,會(huì)輪詢監(jiān)聽多個(gè)文件描述符,但在大量連接的情況下,輪詢耗費(fèi)大量 CPU 時(shí)間。此外,select 的時(shí)間復(fù)雜度是 O(n),其中 n 是文件描述符的數(shù)量,這意味著隨著連接數(shù)增加,性能可能下降。

????????當(dāng)連接數(shù)非常大時(shí),select 可能無法很好地?cái)U(kuò)展,因?yàn)樗谝粋€(gè)單線程內(nèi)處理所有連接,限制了并發(fā)處理能力。對(duì)于大規(guī)模連接的情況,更適合采用基于事件驅(qū)動(dòng)的庫(kù)或框架,如 epoll(Linux 下的高性能 I/O 多路復(fù)用機(jī)制)。

2、無論是 select 方式還是 epoll 方式,都需要將監(jiān)視對(duì)象文件描述符信息通過函數(shù)調(diào)用傳遞給操作系統(tǒng)。請(qǐng)解釋傳遞該信息的原因。

????????文件描述符是由操作系統(tǒng)管理的,所以必須要借助操作系統(tǒng)才能完成。

3、select 方式和 epoll 方式的最大差異在于監(jiān)視對(duì)象文件描述符傳遞給操作系統(tǒng)的方式。請(qǐng)說明具體差異,并解釋為何存在這種差異。

????????select 函數(shù)每次調(diào)用都要傳遞所有的監(jiān)視對(duì)象信息,而 epoll 函數(shù)僅向操作系統(tǒng)傳遞 1 次監(jiān)視對(duì)象,監(jiān)視范圍或內(nèi)容發(fā)生變化時(shí)只通知發(fā)生變化的事項(xiàng)。select 采用這種方法是為了保持兼容性。

4、雖然 epoll 是 select 的改進(jìn)方案,但 select 也有自己的優(yōu)點(diǎn)。在何種情況下使用 select 更加合理。

????????①服務(wù)器端接入者少②程序應(yīng)具有兼容性。

5、epoll 是以條件觸發(fā)和邊緣觸發(fā)方式工作。二者有何差別?從輸入緩沖的角度說明這兩種方式通知事件的時(shí)間點(diǎn)差異。

????????在條件觸發(fā)中,只要輸入緩沖有數(shù)據(jù),就會(huì)一直通知該事件。邊緣觸發(fā)中輸入緩沖收到數(shù)據(jù)時(shí)僅注冊(cè) 1 次該事件,即使輸入緩沖中還留有數(shù)據(jù),也不會(huì)再進(jìn)行注冊(cè)。

6、采用邊緣觸發(fā)時(shí)可以分離數(shù)據(jù)的接收和處理時(shí)間點(diǎn)。請(qǐng)說明其優(yōu)點(diǎn)和原因。

?????????分離接收數(shù)據(jù)和處理數(shù)據(jù)的時(shí)間點(diǎn),給服務(wù)端的實(shí)現(xiàn)帶來很大靈活性。文章來源地址http://www.zghlxwxcb.cn/news/detail-650774.html

到了這里,關(guān)于《TCP IP網(wǎng)絡(luò)編程》第十七章的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 《TCP IP網(wǎng)絡(luò)編程》第十三章

    《TCP IP網(wǎng)絡(luò)編程》第十三章

    Linux 中的 send recv: ?????????send 函數(shù)定義: ????????recv 函數(shù)的定義: ????????send 和 recv 函數(shù)的最后一個(gè)參數(shù)是收發(fā)數(shù)據(jù)的可選項(xiàng),該選項(xiàng)可以用位或(bit OR)運(yùn)算符(| 運(yùn)算符)同時(shí)傳遞多個(gè)信息。send recv 函數(shù)的可選項(xiàng)意義: MSG_OOB:發(fā)送緊急消息?: ????

    2024年02月15日
    瀏覽(23)
  • 《TCP IP網(wǎng)絡(luò)編程》第十八章

    《TCP IP網(wǎng)絡(luò)編程》第十八章

    線程背景: ????????第 10 章介紹了多進(jìn)程服務(wù)端的實(shí)現(xiàn)方法。多進(jìn)程模型與 select 和 epoll 相比的確有自身的優(yōu)點(diǎn),但同時(shí)也有問題。如前所述, 創(chuàng)建(復(fù)制)進(jìn)程的工作本身會(huì)給操作系統(tǒng)帶來相當(dāng)沉重的負(fù)擔(dān)。而且,每個(gè)進(jìn)程都具有獨(dú)立的內(nèi)存空間,所以進(jìn)程間通信的實(shí)

    2024年02月12日
    瀏覽(27)
  • 《TCP IP網(wǎng)絡(luò)編程》第十六章

    《TCP IP網(wǎng)絡(luò)編程》第十六章

    ? ????????「分離 I/O 流」是一種常用表達(dá)。有 I/O 工具可區(qū)分二者,無論采用哪種方法,都可以認(rèn)為是分離了 I/O 流。 2次 I/O 流分離: 第一種是第 10 章的「TCP I/O 過程」分離。通 過調(diào)用 fork 函數(shù)復(fù)制出一個(gè)文件描述符,以區(qū)分輸入和輸出中使用的文件描述符。雖然文件描

    2024年02月13日
    瀏覽(25)
  • TCP/IP網(wǎng)絡(luò)編程 第十九章:Windows平臺(tái)下線程的使用

    要想掌握Windows平臺(tái)下的線程,應(yīng)首先理解“內(nèi)核對(duì)象”(Kernel Objects)的概念。如果僅介紹Windows平臺(tái)下的線程使用技巧,則可以省略相對(duì)陌生的內(nèi)核對(duì)象相關(guān)內(nèi)容。但這并不能使各位深入理解Windows平臺(tái)下的線程。 內(nèi)核對(duì)象的定義 操作系統(tǒng)創(chuàng)建的資源有很多種,如進(jìn)程、線程

    2024年02月16日
    瀏覽(30)
  • TCP/IP網(wǎng)絡(luò)編程 第十六章:關(guān)于IO流分離的其他內(nèi)容

    TCP/IP網(wǎng)絡(luò)編程 第十六章:關(guān)于IO流分離的其他內(nèi)容

    兩次I/O流分離 我們之前通過2種方法分離過IO流,第一種是第十章的“TCPI/O過程(Routine)分離”。這種方法通過調(diào)用fork函數(shù)復(fù)制出1個(gè)文件描述符,以區(qū)分輸入和輸出中使用的文件描述符。雖然文件描述符本身不會(huì)根據(jù)輸入和輸出進(jìn)行區(qū)分,但我們分開了2個(gè)文件描述符的用途

    2024年02月16日
    瀏覽(29)
  • TCP/IP網(wǎng)絡(luò)編程 第十五章:套接字和標(biāo)準(zhǔn)I/O

    TCP/IP網(wǎng)絡(luò)編程 第十五章:套接字和標(biāo)準(zhǔn)I/O

    標(biāo)準(zhǔn)I/O函數(shù)的兩個(gè)優(yōu)點(diǎn) 將標(biāo)準(zhǔn)I/O函數(shù)用于數(shù)據(jù)通信并非難事。但僅掌握函數(shù)使用方法并沒有太大意義,至少應(yīng)該 了解這些函數(shù)具有的優(yōu)點(diǎn)。下面列出的是標(biāo)準(zhǔn)I/O函數(shù)的兩大優(yōu)點(diǎn): □標(biāo)準(zhǔn)I/O函數(shù)具有良好的移植性(Portability) □標(biāo)準(zhǔn)I/O函數(shù)可以利用緩沖提高性能。 關(guān)于移植性無需

    2024年02月16日
    瀏覽(96)
  • 突破編程_C++_網(wǎng)絡(luò)編程(TCPIP 四層模型(概述))

    TCP/IP 協(xié)議,全稱為 Transmission Control Protocol/Internet Protocol,中文名為傳輸控制協(xié)議/因特網(wǎng)互聯(lián)協(xié)議,又名網(wǎng)絡(luò)通訊協(xié)議。這是 Internet 最基本的協(xié)議,也是 Internet 國(guó)際互聯(lián)網(wǎng)絡(luò)的基礎(chǔ)。它主要由網(wǎng)絡(luò)層的 IP 協(xié)議和傳輸層的 TCP 協(xié)議組成,定義了電子設(shè)備如何連入因特網(wǎng),以及數(shù)

    2024年04月08日
    瀏覽(38)
  • 突破編程_C++_網(wǎng)絡(luò)編程(TCPIP 四層模型(傳輸層))

    在 TCP/IP 四層模型中,傳輸層位于網(wǎng)絡(luò)層之上和應(yīng)用層之下,負(fù)責(zé)在源主機(jī)和目標(biāo)主機(jī)之間提供端到端的可靠數(shù)據(jù)傳輸服務(wù)。傳輸層的主要功能與作用體現(xiàn)在以下幾個(gè)方面: 分段與重組:由于網(wǎng)絡(luò)層的數(shù)據(jù)包大小有限制(如 IP 數(shù)據(jù)包的最大長(zhǎng)度為 65535 字節(jié)),而應(yīng)用層的數(shù)

    2024年04月09日
    瀏覽(32)
  • 《TCP IP網(wǎng)絡(luò)編程》

    《TCP IP網(wǎng)絡(luò)編程》

    ? ? ? ? 2023.6.28 正式開始學(xué)習(xí)網(wǎng)絡(luò)編程。 每一章每一節(jié)的筆記都會(huì)記錄在博客中以便復(fù)習(xí)。 ? ? ? ? 網(wǎng)絡(luò)編程又叫套接字編程。所謂網(wǎng)絡(luò)編程,就是編寫程序使兩臺(tái)連網(wǎng)的計(jì)算機(jī)相互交換數(shù)據(jù)。 為什么叫套接字編程? 我們平常將插頭插入插座上就能從電網(wǎng)中獲取電力,同

    2024年02月11日
    瀏覽(24)
  • 【Java基礎(chǔ)教程】(四十七)網(wǎng)絡(luò)編程篇:網(wǎng)絡(luò)通訊概念,TCP、UDP協(xié)議,Socket與ServerSocket類使用實(shí)踐與應(yīng)用場(chǎng)景~

    【Java基礎(chǔ)教程】(四十七)網(wǎng)絡(luò)編程篇:網(wǎng)絡(luò)通訊概念,TCP、UDP協(xié)議,Socket與ServerSocket類使用實(shí)踐與應(yīng)用場(chǎng)景~

    了解多線程與網(wǎng)絡(luò)編程的操作關(guān)系; 了解網(wǎng)絡(luò)程序開發(fā)的主要模式; 了解 TCP 程序的基本實(shí)現(xiàn); 在Java中,網(wǎng)絡(luò)編程的核心意義是實(shí)現(xiàn)不同電腦主機(jī)之間的數(shù)據(jù)交互。Java采用了一種簡(jiǎn)化的概念,將這個(gè)過程進(jìn)一步抽象為JVM(Java虛擬機(jī))進(jìn)程之間的通信??梢栽谕慌_(tái)電腦上

    2024年02月15日
    瀏覽(98)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包