第 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的流程:
創(chuàng)建套接字:首先,創(chuàng)建一個(gè)套接字(一般是服務(wù)器套接字),用于監(jiān)聽連接請(qǐng)求或數(shù)據(jù)傳輸。
創(chuàng)建 epoll 實(shí)例:通過調(diào)用
epoll_create()
函數(shù)創(chuàng)建一個(gè) epoll 實(shí)例,用于管理文件描述符的事件。注冊(cè)事件:將要監(jiān)聽的套接字(文件描述符)注冊(cè)到 epoll 實(shí)例中,通過調(diào)用
epoll_ctl()
函數(shù),并設(shè)置感興趣的事件類型(如讀事件、寫事件等)以及相關(guān)的數(shù)據(jù)(如文件描述符本身)。進(jìn)入事件循環(huán):進(jìn)入一個(gè)循環(huán),在循環(huán)中調(diào)用
epoll_wait()
函數(shù)來等待事件的發(fā)生。epoll_wait()
會(huì)阻塞程序直到有事件發(fā)生或超時(shí)。處理事件:一旦有事件發(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ù)。移除事件:如果需要,可以在事件處理后將文件描述符從 epoll 實(shí)例中移除,通過調(diào)用
epoll_ctl()
函數(shù),通常是在客戶端關(guān)閉連接時(shí)執(zhí)行。關(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é)果:
????????從結(jié)果可以看出,每當(dāng)收到客戶端數(shù)據(jù)時(shí),都會(huì)注冊(cè)該事件,并因此調(diào)用 epoll_wait 函數(shù)。
????????下面的代碼是修改后的邊緣觸發(fā)方式的代碼,僅僅是把上面的某一行代碼改為:
event.events = EPOLLIN | EPOLLET;
? ? ? ? ?運(yùn)行結(jié)果:
????????從上面的例子看出,接收到客戶端的消息時(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é)果:
? ? ? ? ?從上述結(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)。如圖:
?
????????
運(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)和原因。文章來源:http://www.zghlxwxcb.cn/news/detail-650774.html
?????????分離接收數(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)!