在正式閱讀本篇博客之前,建議大家先按順序把下面這兩篇博客看一下,否則直接來看這篇博客的話估計很難搞懂
多路IO復(fù)用技術(shù)①——select詳解&如何使用select模型在本地主機(jī)實現(xiàn)簡易的一對多服務(wù)器多路IO復(fù)用技術(shù)②——poll詳解&如何使用poll模型在本地主機(jī)實現(xiàn)簡易的一對多服務(wù)器
在了解以上兩篇博客講解的內(nèi)容后,我們正式開始本篇博客的相關(guān)內(nèi)容講解,第三種多路套接字監(jiān)聽技術(shù)——epoll模型
目錄
EPOLL模型優(yōu)化的第一部分
1.對拷貝開銷的優(yōu)化
2.對掛載開銷的優(yōu)化
3.對查找開銷的優(yōu)化
EPOLL模型的相關(guān)接口
相關(guān)結(jié)構(gòu)體 struct epoll_event
相關(guān)函數(shù)
EPOLL模型優(yōu)化的第二部分
4.監(jiān)聽方面的優(yōu)化
EPOLL模型的優(yōu)缺點
使用epoll模型實現(xiàn)簡易一對多服務(wù)器的程序?qū)崿F(xiàn)
程序構(gòu)成 結(jié)果圖示
EPOLL的兩種監(jiān)聽模式
epoll模型是最優(yōu)秀的套接字監(jiān)聽技術(shù),他幾乎解決了select和poll模型中的所有缺點,并在其優(yōu)點上進(jìn)行了進(jìn)一步的優(yōu)化,接下來,我們就來了解一下epoll模型的進(jìn)行多路套接字監(jiān)聽的原理吧
EPOLL模型優(yōu)化的第一部分
我們知道,select和poll模型實現(xiàn)的監(jiān)聽原理其實都是按照以下這些步驟:
系統(tǒng)會將這個用戶層的監(jiān)聽集合拷貝到內(nèi)核層
系統(tǒng)會將內(nèi)核層的該監(jiān)聽集合中監(jiān)聽的套接字放入IO設(shè)備等待隊列,由IO設(shè)備等待隊列來進(jìn)行一次又一次的遍歷來判斷那些套接字中有數(shù)據(jù)需要處理
當(dāng)IO設(shè)備等待隊列發(fā)現(xiàn)有套接字處于就緒狀態(tài)時,會傳出就緒集合到內(nèi)核層
系統(tǒng)通過select模型將該就緒集合由內(nèi)核層拷貝到用戶層,供用戶使用
想要了解下面的內(nèi)容,這里需要為大家簡單講解一種數(shù)據(jù)結(jié)構(gòu)——紅黑樹 ,并希望大家能夠記住紅黑樹的優(yōu)點
紅黑樹的優(yōu)勢就在于非常適合用來存儲有序的數(shù)據(jù),并且增刪查改的時間復(fù)雜度很低,只有O(log2n)
1.對拷貝開銷的優(yōu)化
在之前的博客中,我們講過,隨著select/poll模型的持續(xù)使用,會產(chǎn)生大量的拷貝開銷和掛載開銷
這些拷貝開銷大致由兩部分組成:
系統(tǒng)從用戶層到內(nèi)核層,內(nèi)核層到用戶層翻來覆去的層級轉(zhuǎn)換
一旦監(jiān)聽集合更新,就要重新拷貝整個就緒集合,只要服務(wù)器不停,就會面臨無止盡的拷貝
該如何減少第一部分的拷貝開銷呢?
其實很簡單粗暴,epoll模型的開發(fā)人員直接將監(jiān)聽集合定義在內(nèi)核層。所以epoll模型的監(jiān)聽集合都是建立在內(nèi)核層的
PS:這種監(jiān)聽集合實際上是紅黑樹,每一個葉子節(jié)點對應(yīng)一個監(jiān)聽套接字
至于如何減少第二部分的拷貝開銷,就和紅黑樹樹這種數(shù)據(jù)結(jié)構(gòu)息息相關(guān)了。
我們知道,select和poll模型都是采用數(shù)組作為作為監(jiān)聽集合,數(shù)組這種結(jié)構(gòu)非常不便于查找其中哪些數(shù)據(jù)產(chǎn)生了變化,就導(dǎo)致當(dāng)有新的套接字要放入監(jiān)聽集合中時,系統(tǒng)會將整個新的監(jiān)聽集合全部拷貝到內(nèi)核層。
而紅黑樹就完美的避開了這一點,每當(dāng)有新的套接字要放入監(jiān)聽樹中時,我們只需要為這棵監(jiān)聽紅黑樹添加一個葉子節(jié)點就夠了,而不是重建這棵樹,這就導(dǎo)致了這方面的開銷大大減少了
2.對掛載開銷的優(yōu)化
在對拷貝開銷的優(yōu)化方面,epoll模型的開發(fā)人員依舊采用了紅黑樹這種數(shù)據(jù)結(jié)構(gòu),我們來為大家簡單的介紹一下原因:
我們知道,在select與poll模型中,數(shù)組這種結(jié)構(gòu)非常不便于查找其中哪些數(shù)據(jù)產(chǎn)生了變化,所以每當(dāng)有新的套接字放入監(jiān)聽集合中時,系統(tǒng)會將監(jiān)聽的套接字全部重新掛載到IO設(shè)備等待隊列,這就導(dǎo)致掛載開銷方面的開銷非常大。
而紅黑樹則完美的解決了這個問題,由于紅黑樹的特性,每當(dāng)有對應(yīng)新套接字的葉子節(jié)點被放入監(jiān)聽樹中時,系統(tǒng)能夠很輕松的就能找到對應(yīng)新套接字的葉子節(jié)點是哪一個,然后只需要將對應(yīng)的新套接字放入IO設(shè)備等待隊列就可以了,在掛載開銷方面的消耗大大減少
3.對查找開銷的優(yōu)化
我們知道,雖然數(shù)組的下標(biāo)是有序的,方便我們進(jìn)行遍歷,但是數(shù)組中的內(nèi)容并不一定是有序的,這就導(dǎo)致我們無法根據(jù)我們目標(biāo)內(nèi)容來直接找到其在數(shù)組中的哪個位置,而是需要去通過循環(huán)一次次的查找,但紅黑樹就完美地解決了這個問題
我們知道,當(dāng)我們調(diào)用accept函數(shù)讓服務(wù)器與多個客戶端建立TCP鏈接時,其返回的int類型的套接字文件描述符,其實都是有序遞增的,建立一個鏈接返回值就+1。由于紅黑樹在有序數(shù)據(jù)存儲方面的優(yōu)勢,使得服務(wù)器在對于監(jiān)聽就緒和取消監(jiān)聽方面的效率非常之高。
其實epoll模型不止在這些方面進(jìn)行了優(yōu)化,其他部分需要大家了解了EPOLL模型的相關(guān)接口后,才能為大家進(jìn)行講解,所以別著急,我們先來了解下EPOLL模型的相關(guān)接口吧
EPOLL模型的相關(guān)接口
以下接口的頭文件都是 #include <sys/epoll.h>
相關(guān)結(jié)構(gòu)體 struct epoll_event
struct epoll_event { uint32_t events; // epoll 事件類型,包括可讀,可寫等 epoll_data_t data; // 用戶數(shù)據(jù),可以是一個指針或文件描述符等 callback(); //這個函數(shù)與用戶沒有關(guān)系,我們是看不見的,只有當(dāng)發(fā)現(xiàn)對應(yīng)套接字就緒時才會調(diào)用該函數(shù)進(jìn)行回調(diào)操作 }; typedef union epoll_data { void *ptr; //指向任何類型的用戶數(shù)據(jù) int fd; //套接字文件描述符 uint32_t u32; //32位的無符號整數(shù) uint64_t u64; //64位的無符號整數(shù) } epoll_data_t;
其中,events字段表示要監(jiān)聽的事件類型,可以是以下值之一:
EPOLLIN:表示對應(yīng)的文件描述符上有數(shù)據(jù)可讀
EPOLLOUT:表示對應(yīng)的文件描述符上可以寫入數(shù)據(jù)
EPOLLRDHUP:表示對端已經(jīng)關(guān)閉連接,或者關(guān)閉了寫操作端的寫入
EPOLLPRI:表示有緊急數(shù)據(jù)可讀
EPOLLERR:表示發(fā)生錯誤
EPOLLHUP:表示文件描述符被掛起
EPOLLET:表示將epoll設(shè)置為邊緣觸發(fā)模式
EPOLLONESHOT:表示將事件設(shè)置為一次性事件
這個回調(diào)的原理是什么呢?其實很簡單
其實負(fù)責(zé)監(jiān)聽這些套接字事件的設(shè)備就是網(wǎng)卡,所以最先知道套接字就緒的就是網(wǎng)卡設(shè)備,這時候開發(fā)人員就想了,那我能不能實現(xiàn)一個功能,創(chuàng)建一個結(jié)構(gòu)體隊列,直接將該隊列與網(wǎng)卡進(jìn)行綁定,形成一種回調(diào)關(guān)系。這樣的話,當(dāng)網(wǎng)卡監(jiān)聽到某套接字處于就緒狀態(tài)時,就通過異步通知的方式來告訴服務(wù)器是哪個套接字就緒了,不需要再一次次的遍歷來判斷哪個套接字就緒
所以epoll模型監(jiān)聽套接字的過程就如下所示:
建立結(jié)構(gòu)體(ep_item)隊列,隊列中的每一個結(jié)構(gòu)體和一個套接字對應(yīng),將該隊列與網(wǎng)卡進(jìn)行綁定
建立一個就緒鏈表,存放對應(yīng)就緒套接字的結(jié)構(gòu)體epoll_events
監(jiān)聽到某一套接字就緒時,調(diào)用結(jié)構(gòu)體epoll_events中的callback函數(shù),進(jìn)行拆包,將結(jié)構(gòu)體ep_item中的結(jié)構(gòu)體epoll_events拆分出來,并彈出到就緒鏈表中(雙向鏈表)
將這個就緒鏈表中的內(nèi)容拷貝到用戶在用戶層定義的結(jié)構(gòu)體(epoll_event)數(shù)組,以便用戶使用
不理解的話可以看下面的這個圖
相關(guān)函數(shù)
先來介紹一下一會會用到的參數(shù):
int epfd; // 返回的監(jiān)聽紅黑樹文件描述符
int epoll_max; //監(jiān)聽套接字的最大數(shù)量
int option; //需要執(zhí)行的操作——添加、修改、刪除
int sockfd; //需要添加,修改,刪除的socket文件描述符
struct epoll_event * node; //掛在紅黑樹上的節(jié)點
int array_size; //存放對應(yīng)就緒套接字的結(jié)構(gòu)體的數(shù)組大小
struct epoll_event ready_array[array_size]; //存放對應(yīng)就緒套接字的結(jié)構(gòu)體的數(shù)組
int ready_max; //允許同時監(jiān)聽的最大套接字?jǐn)?shù)量
int timeout; //工作模式
PS:一般情況下,epoll_max = ready_max = array_size
我們來分別介紹一下option和timeout參數(shù)的有效值:
option參數(shù)的有效值為:
EPOLL_CTL_ADD:添加與目標(biāo)套接字對應(yīng)的結(jié)構(gòu)體(epoll_event)到epoll監(jiān)聽樹上,開始監(jiān)聽該套接字
EPOLL_CTL_MOD:修改目標(biāo)套接字的監(jiān)聽事件
EPOLL_CTL_DEL:將與目標(biāo)套接字對應(yīng)的結(jié)構(gòu)體(epoll_event)從epoll監(jiān)聽樹上摘除,也就是不再監(jiān)聽該套接字
timeout參數(shù)的有效值:
timeout = 0:表示非阻塞監(jiān)聽套接字相關(guān)事件
timeout = -1:表示阻塞監(jiān)聽套接字相關(guān)事件
timeout > 0:表示阻塞監(jiān)聽時間為timout(單位:毫秒),如果這段時間內(nèi)沒有監(jiān)聽到套接字相關(guān)事件,后續(xù)變?yōu)榉亲枞O(jiān)聽
函數(shù) | 功能 | 返回值 |
int epoll_create(epoll_max); | 建立監(jiān)聽紅黑樹 | 1.成功,返回值為epoll監(jiān)聽樹對應(yīng)的文件描述符 2.出錯,返回 -1,并設(shè)置錯誤碼 |
int epoll_ctl(epfd , option , sockfd , node); | 添加/刪除節(jié)點或修改監(jiān)聽事件 | 1.成功,返回0 2.出錯,返回 -1,并設(shè)置錯誤碼 |
int epoll_wait(epfd , ready_array , ready_max , timeout); | 監(jiān)聽套接字相關(guān)事件 | 1.成功,返回值為就緒的套接字?jǐn)?shù)目 2.如果在請求的超時毫秒內(nèi)沒有套接字準(zhǔn)備就緒,返回0 3.出錯,返回 -1,并設(shè)置錯誤碼 |
epoll_create()的錯誤碼如下所示:
EINVAL:epoll_max大小不為正。
EMFILE:遇到了每個用戶對/ proc / sys / fs / epoll / max_user_instances施加的epoll實例數(shù)量的限制。
ENFILE:超過系統(tǒng)設(shè)定的進(jìn)程最大創(chuàng)建的文件描述符數(shù)量。
ENOMEM:沒有足夠的內(nèi)存來創(chuàng)建內(nèi)核對象。
epoll_ctl()的錯誤碼如下所示:
EBADF:epfd或fd不是有效的文件描述符。
EEXIST:option為EPOLL_CTL_ADD,并且提供的文件描述符fd已在該epoll實例中注冊。
EINVAL:epfd不是epoll文件描述符,或者fd與epfd相同,或者此接口不支持請求的操作option。
ENOENT:option是EPOLL_CTL_MOD或EPOLL_CTL_DEL,并且fd未在該epoll實例中注冊。
ENOMEM:沒有足夠的內(nèi)存來處理請求的操作控制操作。
ENOSPC:嘗試在主機(jī)上注冊(EPOLL_CTL_ADD)新文件描述符時遇到了/ proc / sys / fs / epoll / max_user_watches施加的限制。
EPERM:目標(biāo)文件fd不支持epoll。
epoll_wait()的錯誤碼如下所示:
EBADF:epfd不是有效的文件描述符。
EFAULT:具有寫許可權(quán)不能訪問事件指向的存儲區(qū)。
EINTR:在任何請求的事件發(fā)生或超時到期之前,信號處理程序中斷了該調(diào)用;參見signal(7)。
EINVAL:epfd不是epoll文件描述符,或者epoll_max小于或等于零。
EPOLL模型優(yōu)化的第二部分
4.監(jiān)聽方面的優(yōu)化
由于epoll模型的自定義結(jié)構(gòu)體隊列、異步通知和回調(diào)函數(shù)的使用,導(dǎo)致用戶不再需要遍歷去查找就緒的套接字是哪些,直接對自己在用戶層自定義的結(jié)構(gòu)體數(shù)組進(jìn)行遍歷處理就可以了,大大減少了時間片方面的消耗,使得服務(wù)器的處理能力實現(xiàn)質(zhì)的飛躍
基本了解了epoll在哪些方面做出了優(yōu)化后,我們就可以來聊一聊epoll模型的優(yōu)缺點了
EPOLL模型的優(yōu)缺點
優(yōu)點:
不存在重復(fù)的拷貝開銷與掛載開銷
自定義實現(xiàn)監(jiān)聽隊列,采用異步回調(diào)方式,無需輪詢,隊列體積更小
不止返回就緒的套接字?jǐn)?shù)量,還返回就緒的套接字是哪些
監(jiān)聽的事件種類豐富
可以為不同的套接字設(shè)置不同的監(jiān)聽事件,不像select模型只能批量設(shè)置監(jiān)聽事件
可以監(jiān)聽的socket數(shù)量不受1024的硬限制
缺點:
僅linux系統(tǒng)支持
紅黑樹的缺點
使用epoll模型實現(xiàn)簡易一對多服務(wù)器的程序?qū)崿F(xiàn)
程序構(gòu)成
該服務(wù)器與客戶端由以下幾個程序共同組成:
func_2th_parcel.h:定義二次包裹的函數(shù)名
func_2th_parcel.c:對網(wǎng)絡(luò)初始化相關(guān)的函數(shù)進(jìn)行二次包裹
epoll_server.c:使用poll模型的服務(wù)器程序
client.c:客戶端程序
/************************************************************************* > File Name: func_2th_parcel.h > Author: Nan > Mail: **@qq.com > Created Time: 2023年10月18日 星期三 18時32分22秒 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <signal.h> #include <pthread.h> #include <fcntl.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/mman.h> #include <time.h> #include <ctype.h> #include <sys/select.h> #include <sys/socket.h> #include <poll.h> #include <sys/epoll.h> //socket函數(shù)的二次包裹 int SOCKET(int domain , int type , int protocol); //bind函數(shù)的二次包裹 int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen); //listen函數(shù)的二次包裹 int LISTEN(int sockfd , int backlog); //send函數(shù)的二次包裹 ssize_t SEND(int sockfd , const void* buf , size_t len , int flags); //recv函數(shù)的二次包裹 ssize_t RECV(int sockfd , void* buf , size_t len , int flags); //connect函數(shù)的二次包裹 int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen); //accept函數(shù)的二次包裹 int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen); //網(wǎng)絡(luò)初始化函數(shù) int SOCKET_NET_CREATE(const char* ip , int port); //服務(wù)端與客戶端建立連接并返回客戶端套接字文件描述符 int SERVER_ACCEPTING(int server_fd);
/************************************************************************* > File Name: func_2th_parcel.c > Author: Nan > Mail: **@qq.com > Created Time: 2023年10月18日 星期三 18時32分42秒 ************************************************************************/ #include <func_2th_parcel.h> int SOCKET(int domain , int type , int protocol){ int return_value; if((return_value = socket(domain , type , protocol)) == -1){ perror("socket call failed!\n"); return return_value; } return return_value; } int BIND(int sockfd , const struct sockaddr* addr , socklen_t addrlen){ int return_value; if((return_value = bind(sockfd , addr , addrlen)) == -1){ perror("bind call failed!\n"); return return_value; } return return_value; } int LISTEN(int sockfd , int backlog){ int return_value; if((return_value = listen(sockfd , backlog)) == -1){ perror("listen call failed!\n"); return return_value; } return return_value; } ssize_t SEND(int sockfd , const void* buf , size_t len , int flags){ ssize_t return_value; if((return_value = send(sockfd , buf , len , flags)) == -1){ perror("send call failed!\n"); return return_value; } return return_value; } ssize_t RECV(int sockfd , void* buf , size_t len , int flags){ ssize_t return_value; if((return_value = recv(sockfd , buf , len , flags)) == -1){ perror("recv call failed!\n"); return return_value; } return return_value; } int CONNECT(int sockfd , const struct sockaddr* addr , socklen_t addrlen){ int return_value; if((return_value = connect(sockfd , addr , addrlen)) == -1){ perror("connect call failed!\n"); return return_value; } return return_value; } int ACCEPT(int sockfd , struct sockaddr* addr , socklen_t addrlen){ int return_value; if((return_value = accept(sockfd , addr , &addrlen)) == -1){ perror("accept call failed!\n"); return return_value; } return return_value; } int SOCKET_NET_CREATE(const char* ip , int port){ int sockfd; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET , ip , &addr.sin_addr.s_addr); sockfd = SOCKET(AF_INET , SOCK_STREAM , 0); BIND(sockfd , (struct sockaddr*)&addr , sizeof(addr)); LISTEN(sockfd , 128); return sockfd; } int SERVER_ACCEPTING(int server_fd) { int client_sockfd; struct sockaddr_in client_addr; char client_ip[16]; char buffer[1500]; bzero(buffer , sizeof(buffer)); bzero(&client_addr , sizeof(client_addr)); socklen_t addrlen = sizeof(client_addr); client_sockfd = ACCEPT(server_fd , (struct sockaddr*)&client_addr , addrlen); bzero(client_ip , 16); //將客戶端的IP地址轉(zhuǎn)成CPU可以識別的序列并存儲到client_ip數(shù)組中 inet_ntop(AF_INET , &client_addr.sin_addr.s_addr , client_ip , 16); sprintf(buffer , "Hi , %s welcome tcp test server service..\n" , client_ip); printf("client %s , %d , connection success , client sockfd is %d\n" , client_ip , ntohs(client_addr.sin_port) , client_sockfd); SEND(client_sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL); return client_sockfd; }
/************************************************************************* > File Name: epoll_server.c > Author: Nan > Mail: **@qq.com > Created Time: 2023年10月25日 星期三 18時53分30秒 ************************************************************************/ #include <func_2th_parcel.h> #define MAX_EPOLL 100000 int main(void) { int server_sockfd;//服務(wù)器套接字文件描述符 int epfd;//監(jiān)聽樹文件描述符 struct epoll_event ready_array[MAX_EPOLL];//由于存放epoll返回的就緒結(jié)構(gòu)體 struct epoll_event node;//向監(jiān)聽樹中放入的節(jié)點 int ready_num;//處于就緒狀態(tài)的套接字的數(shù)量 int client_sockfd;//客戶端套接字文件描述符 char rw_buffer[1500];//讀寫緩沖區(qū) int flag; int recv_len = 0;//接受到的數(shù)據(jù)的長度 int i; bzero(rw_buffer , sizeof(rw_buffer));//清空讀寫緩沖區(qū) server_sockfd = SOCKET_NET_CREATE("192.168.79.128" , 6060);//進(jìn)行網(wǎng)絡(luò)初始化 //初始化epoll結(jié)構(gòu)體 node.data.fd = server_sockfd; node.events = EPOLLIN;//監(jiān)聽讀事件 //初始化監(jiān)聽樹,并做錯誤處理 if((epfd = epoll_create(MAX_EPOLL)) == -1) { perror("epoll_create call failed\n"); exit(-1); } //向監(jiān)聽樹中添加服務(wù)器套接字結(jié)構(gòu)體,并做錯誤處理 if((epoll_ctl(epfd , EPOLL_CTL_ADD , server_sockfd , &node)) == -1) { perror("epoll_ctl call failed\n"); exit(-1); } printf("epoll_server wait TCP connect\n"); while(1) { //獲取處于就緒狀態(tài)的套接字?jǐn)?shù)量 if((ready_num = epoll_wait(epfd , ready_array , MAX_EPOLL , -1)) == -1) { perror("epoll_wait call failed\n"); exit(0); } i = 0; while(ready_num) { //辨別就緒,如果是服務(wù)端套接字就緒 if(ready_array[i].data.fd == server_sockfd) { client_sockfd = SERVER_ACCEPTING(ready_array[i].data.fd);//與客戶端建立TCP鏈接 node.data.fd = client_sockfd; if(epoll_ctl(epfd , EPOLL_CTL_ADD , client_sockfd , &node) == -1) { perror("epoll_ctl failed\n"); exit(-1); } } //如果是客戶端套接字就緒 else { recv_len = RECV(ready_array[i].data.fd , rw_buffer , sizeof(rw_buffer) , 0); flag = 0; //如果recv_len = 0,就說明與客戶端套接字對應(yīng)的客戶端退出了,將對應(yīng)客戶端套接字移出套接字存儲數(shù)組與監(jiān)聽集合 if(recv_len == 0) { perror("某一客戶端與本服務(wù)器斷開鏈接\n"); printf("客戶端%d 與本服務(wù)器斷開鏈接,關(guān)閉其對應(yīng)的套接字并停止監(jiān)聽\n" , ready_array[i].data.fd); //關(guān)閉該套接字 close(ready_array[i].data.fd); //將其對應(yīng)的節(jié)點從監(jiān)聽樹上摘下來 epoll_ctl(epfd , EPOLL_CTL_DEL , ready_array[i].data.fd , NULL); //注意,這個兩個步驟不能搞反 } //進(jìn)行業(yè)務(wù)處理:小寫字母轉(zhuǎn)大寫字母 printf("服務(wù)器已接收到客戶端%d 發(fā)來的信息 : %s,現(xiàn)在對其進(jìn)行處理\n" , ready_array[i].data.fd , rw_buffer); while(recv_len > flag) { rw_buffer[flag] = toupper(rw_buffer[flag]); flag++; } printf("服務(wù)器已對客戶端%d 發(fā)來的信息完成處理,處理后的數(shù)據(jù)為 : %s\n" , ready_array[i].data.fd , rw_buffer); SEND(ready_array[i].data.fd , rw_buffer , recv_len , MSG_NOSIGNAL); bzero(rw_buffer , sizeof(rw_buffer)); recv_len = 0; } ready_num--; i++; } } close(server_sockfd); printf("server shutdown\n"); return 0; }
/************************************************************************* > File Name: client.c > Author: Nan > Mail: **@qq.com > Created Time: 2023年10月19日 星期四 18時29分12秒 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <signal.h> #include <pthread.h> #include <fcntl.h> #include <sys/socket.h> #include <netdb.h> #include <arpa/inet.h> #include <time.h> //服務(wù)器實現(xiàn)大小寫轉(zhuǎn)換業(yè)務(wù) int main() { //1.定義網(wǎng)絡(luò)信息結(jié)構(gòu)體與讀寫緩沖區(qū)并初始化 struct sockaddr_in dest_addr; char buffer[1500]; bzero(&dest_addr , sizeof(dest_addr)); bzero(buffer , sizeof(buffer)); dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(6060); //字符串ip轉(zhuǎn)大端序列 inet_pton(AF_INET , "192.168.79.128" , &dest_addr.sin_addr.s_addr); int sockfd = socket(AF_INET , SOCK_STREAM , 0); int i; //2.判斷連接是否成功 if((connect(sockfd , (struct sockaddr*) &dest_addr , sizeof(dest_addr))) == -1) { perror("connect failed!\n"); exit(0); } recv(sockfd , buffer , sizeof(buffer) , 0); printf("%s" , buffer); bzero(buffer , sizeof(buffer)); //3.循環(huán)讀取終端輸入的數(shù)據(jù) while( (fgets(buffer , sizeof(buffer) , stdin) ) != NULL) { i = strlen(buffer); buffer[i-1] = '\0'; //向服務(wù)端發(fā)送消息 send(sockfd , buffer , strlen(buffer) , MSG_NOSIGNAL); //接收服務(wù)端發(fā)來的消息 recv(sockfd , buffer , sizeof(buffer) , 0); //打印服務(wù)端發(fā)來的信息 printf("response : %s\n" , buffer); //清空讀寫緩沖區(qū),以便下一次放入數(shù)據(jù) bzero(buffer , sizeof(buffer)); } //4.關(guān)閉套接字,斷開連接 close(sockfd); return 0; }
結(jié)果圖示
EPOLL的兩種監(jiān)聽模式
1.EPOLLLT,水平觸發(fā)模式,也被稱之為負(fù)責(zé)模式,如果不進(jìn)行特殊設(shè)置,默認(rèn)情況下對套接字都是以此模式監(jiān)聽。
在此模式下,當(dāng)epoll監(jiān)聽到某一套接字就緒時,就會先發(fā)送處理通知給上層。之后每隔一段時間就來查看該套接字緩沖區(qū)中的數(shù)據(jù)有沒有被處理完。
如果沒有處理完,就會繼續(xù)發(fā)送處理通知給上層,一天不處理完就發(fā)一天,一周處理不完就發(fā)一周,直到緩沖區(qū)中的數(shù)據(jù)被全部處理完畢。這就是所說的負(fù)責(zé)模式
PS:在此模式下,如果緩沖區(qū)中的數(shù)據(jù)沒有被處理完,epoll_wait()無法開啟下一輪監(jiān)聽。意思就是如果數(shù)據(jù)沒處理完,就去調(diào)用epoll_wait()函數(shù)的話,會立即返回,返回值為未處理的就緒事件數(shù)量
優(yōu)點:能夠保證套接字緩沖區(qū)中的所有數(shù)據(jù)被處理完畢
缺點:開銷比較大
適用場景:該模式適合處理有效期較短、或需要被緊急處理的數(shù)據(jù)
2.EPOLLET,邊緣觸發(fā)模式,也被稱之為不負(fù)責(zé)模式,使用該模式需要進(jìn)行手動設(shè)置
在此模式下,當(dāng)epoll監(jiān)聽到某一套接字就緒時,指揮發(fā)送一次處理通知給上層。之后無論該套接字緩沖區(qū)中的數(shù)據(jù)有沒有被處理完,都與epoll無關(guān),epoll可以立即進(jìn)入新一輪的監(jiān)聽
優(yōu)點:開銷比較小
缺點:存在隱患,不能夠保證套接字緩沖區(qū)中的所有數(shù)據(jù)被處理(完畢),用戶要自行保證數(shù)據(jù)讀取完畢
PS:邊緣觸發(fā)模式一般結(jié)合非阻塞讀取套接字緩沖區(qū)的數(shù)據(jù)
想要設(shè)置邊緣觸發(fā)模式也很簡單,按照下面的步驟來就可以了
int sockfd;//套接字文件描述符 struct epoll_event node; node.data.fd = sockfd; node.events = EPOLLIN|EPOLLET;//監(jiān)聽讀事件,監(jiān)聽模式切換為邊緣觸發(fā)模式
以上就是本篇博客的全部內(nèi)容了,大家有什么地方?jīng)]有看懂的話,可以在評論區(qū)留言給我,咱要力所能及的話就幫大家解答解答
今天的學(xué)習(xí)記錄到此結(jié)束啦,咱們下篇文章見,ByeBye!文章來源:http://www.zghlxwxcb.cn/news/detail-744045.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-744045.html
到了這里,關(guān)于Linux多路IO復(fù)用技術(shù)——epoll詳解與一對多服務(wù)器實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!