多路IO轉接服務器也叫做多任務IO服務器。該類服務器實現的主旨思想是,不再由應用程序自己監視客戶端連接,取而代之由內核替應用程序監視文件。
IO 多路轉接方式比較:
常見的 IO 多路轉接方式有:select、poll、epoll,他們的區別為:
- select 可以跨平臺,Linux、Mac、Windows 都支持,而 poll 和 epoll 只能在 Linux 上使用
- epoll 底層為紅黑樹,select 和 poll 底層為線性表,epoll 的效率較高
- select 連接的設備上限為 1024,poll 和 epoll 沒上限,取決于當前操作系統的配置
IO 多路轉接本質:
- 在服務器端有兩類文件描述符,分別對應一個讀緩沖區和寫緩沖區
- 用于監聽的文件描述符對應的讀緩沖區主要用來存儲客戶端的連接請求,當調用 accept 時會檢測這個讀緩沖區是否有連接請求
- 用于通信的文件描述符對應的讀緩沖區用于存儲客戶端發送來的數據,服務器調用 read 方法能夠將數據讀取出來,寫緩沖區用于服務器通過 write 寫入的數據
- 當只有一個線程時,accpet、read、write 只要有一個阻塞,就不能繼續運行了
- IO 多路轉接實際上就是將本該由用戶進行的文件描述符讀/寫緩沖區的檢測交給了內核,內核可以同時檢測若干個文件描述符以及它們的讀/寫緩沖區,檢測讀緩沖區是否有數據/檢測寫緩沖區是否有剩余的空間,當條件滿足時,內核會告知用戶相關信息(可操作的文件描述符),此時 accpet、read、write 就不會阻塞了,若內核通知多個文件描述符,在用戶空間處理時是按順序處理的
select
-
select能監聽的文件描述符個數受限于FD_SETSIZE,一般為1024,單純改變進程打開的文件描述符個數并不能改變select監聽文件個數
-
解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select采用的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
// 返回值就緒描述符的數目
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 監控的文件描述符集里最大文件描述符加1,因為此參數會告訴內核檢測前多少個文件描述符的狀態
readfds: 監控有讀數據到達文件描述符集合,傳入傳出參數
writefds: 監控寫數據到達文件描述符集合,傳入傳出參數
exceptfds: 監控異常發生達文件描述符集合,如帶外數據到達異常,傳入傳出參數
timeout: 定時阻塞監控時間,3種情況
1.NULL,永遠等下去
2.設置timeval,等待固定時間
3.設置timeval里時間均為0,檢查描述字后立即返回,輪詢
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //測試文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
示例圖如下:
這個圖的意思是,應用層先把要監聽的文件描述符做標記1,之后再將其拷貝一份,將拷貝的這一份文件描述符在拷貝到內核中,讓內核監聽這些做標記的文件描述符,如果被監聽的文件沒有變化,那么內核中的文件描述的標記就會被抹消,然后在將改變的文件描述符集合復制到應用層,讓其對改變的文件描述符進行讀取,
例如應用層準備監聽4567這四個文件描述符,復制到內核去監聽,內核發現只有5號發生了改變,所以告知應用層去5號文件描述符讀取數據,如果是lfd即4發生了變化,就說明有新的連接產生了
server
流程圖示:
#include <stdio.h>
#include <sys/select.h> // select多路復用API
#include <sys/types.h> // 基本系統數據類型
#include <unistd.h> // POSIX API(read/write/close等)
#include "wrap.h" // 自定義錯誤處理函數封裝
#include <sys/time.h>#define PORT 8888 // 服務器監聽端口int main(int argc, char *argv[])
{// 創建TCP套接字并綁定端口int lfd = tcp4bind(PORT, NULL);// 設置監聽隊列長度為128Listen(lfd, 128);int maxfd = lfd; // 初始化最大文件描述符(當前只有監聽套接字)fd_set oldset, rset; // 定義兩個fd_set:// oldset:永久記錄所有需監控的fd// rset:每次select調用傳入的臨時集合FD_ZERO(&oldset); // 清空文件描述符集合FD_ZERO(&rset);FD_SET(lfd, &oldset); // 將監聽套接字加入監控集合while (1){rset = oldset; // 復制永久集合到臨時集合(select會修改傳入的集合)// 核心:阻塞監聽所有文件描述符的可讀事件int n = select(maxfd + 1, &rset, NULL, NULL, NULL);// 錯誤處理if (n < 0){perror("select error");break;}else if (n == 0){ // 無事件發生(超時)continue;}// 處理監聽套接字事件(新連接到達)查看lfd監聽描述符是否在就緒的rset集合中,在表示有新連接if (FD_ISSET(lfd, &rset)){ // 檢查監聽套接字是否就緒struct sockaddr_in cliaddr;socklen_t len = sizeof(cliaddr);char ip[16] = "";// 接受新連接int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);printf("new client ip=%s port=%d\n",inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),ntohs(cliaddr.sin_port));// 將新連接加入永久監控集合FD_SET(cfd, &oldset);// 更新最大文件描述符if (cfd > maxfd)maxfd = cfd;// 若已無其他事件,跳過后續處理if (--n == 0)continue;}// 處理已連接套接字的數據事件//在 Unix/Linux 中,文件描述符按從小到大的順序分配。lfd 是服務器啟動時最早創建的套接字,其值通常為3(0-2 被標準輸入/輸出/錯誤占用),后續 cfd 依次遞增(4, 5, ...)//因此 lfd + 1 自然指向第一個客戶端連接套接字。for (int i = lfd + 1; i <= maxfd; i++){if (FD_ISSET(i, &rset)){ // 檢查當前fd是否就緒char buf[1500] = "";int ret = Read(i, buf, sizeof(buf)); // 讀取數據// 錯誤處理if (ret < 0){perror("read error");close(i);FD_CLR(i, &oldset); // 從監控集合移除}// 客戶端關閉連接else if (ret == 0){struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("client%d close\n",remote_port);close(i);FD_CLR(i, &oldset);}// 正常數據處理else{struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("客戶端%d:%s\n", remote_port,buf);Write(i, buf, ret); // 回顯數據}}}}return 0;
}
幾個問題?
1.select(maxfd + 1, &rset, NULL, NULL, NULL);為什么要maxfd + 1?
fd_set rset;
int maxfd = 5; ?// 當前最大FD為5
FD_ZERO(&rset);
FD_SET(3, &rset); ?// 監控FD=3
FD_SET(5, &rset); ?// 監控FD=5// 內核會檢查0~5的FD(共6個),但僅FD=3和5實際被監控
select(5 + 1, &rset, NULL, NULL, NULL);
若誤傳?maxfd=5
(未+1),內核可能漏檢FD=5,導致數據就緒卻未被觸發
2.int n = select(maxfd + 1, &rset, NULL, NULL, NULL);這里是怎樣遍歷文件描述符集合的?是從0開始遍歷rset里的文件描述符嗎?
- 在?
select
?函數中,內核遍歷文件描述符集合(rset
)的方式是通過線性掃描位圖,從文件描述符 ?0? 開始,依次檢查每個比特位是否被置位(即是否為1),直到達到?maxfd + 1
?指定的范圍。從0到?maxfd
,無論文件描述符是否打開或活躍。這種設計簡單但效率低,是?select
?被?epoll
?取代的主要原因之一
3.select(maxfd + 1, &rset, NULL, NULL, NULL);里的rset作用是什么
- 在?
select
?函數中,rset
?是一個?fd_set
?類型的位圖集合,其核心作用是標識需要監控的可讀文件描述符(FD)集合,并在函數返回時標記哪些FD已就緒可讀。
4.for (int i = lfd + 1; i <= maxfd; i++) 為什么從 lfd + 1開始遍歷??
- ?
lfd
?通常是較小的值:
在 Unix/Linux 中,文件描述符按從小到大的順序分配。lfd
?是服務器啟動時最早創建的套接字,其值通常為3(0-2 被標準輸入/輸出/錯誤占用),后續?cfd
?依次遞增(4, 5, ...)。
因此?lfd + 1
?自然指向第一個客戶端連接套接字。
client
客戶端使用:
nc 127.0.0.1 8888
模擬客戶端鏈接服務器
結果顯示如下:
但這樣有個問題,無論連接服務器的客戶端是否活躍,遍歷時都會遍歷這些連接的客戶端,所以這就會引發一個問題(大量并發,少了活躍):
假設現在 4-1023個文件描述符需要監聽,但是5-1000這些文件描述符關閉了,遍歷時還是要從4-1023進行遍歷,實際只需要遍歷4、1001-1023即可。
假設現在 4-1023個文件描述符需要監聽,但是只有 5,1002 發來消息,遍歷時還是要從4-1023進行遍歷,實際只需要遍歷5、1002即可。
select進階優化版
之前的代碼,如果最大fd是1023,每次確定有事件發生的fd時,就要掃描3-1023的所有文件描述符,這看起來很蠢。于是定義一個數組,把要監聽的活躍的文件描述符存下來,每次掃描這個數組就行了。看起來科學得多。
server
//進階版select,通過數組防止遍歷1024個描述符
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 8888int main(int argc, char *argv[])
{int i, j, n, maxi;int nready, client[FD_SETSIZE]; /* 自定義數組client, 防止遍歷1024個文件描述符 FD_SETSIZE默認為1024 */int maxfd, listenfd, connfd, sockfd;char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;fd_set rset, allset; /* rset 讀事件文件描述符集合 allset用來暫存 */listenfd = Socket(AF_INET, SOCK_STREAM, 0);//端口復用int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family= AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port= htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);maxfd = listenfd; /* 起初 listenfd 即為最大文件描述符 */maxi = -1; /* 將來用作client[]的下標, 初始值指向0個元素之前下標位置 */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1; /* 用-1初始化client[] */FD_ZERO(&allset);FD_SET(listenfd, &allset); /* 構造select監控文件描述符集 */while (1) { rset = allset; /* 每次循環時都從新設置select監控信號集 */nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfdif (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) { /* 說明有新的客戶端鏈接請求 */clie_addr_len = sizeof(clie_addr);connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不會阻塞 */printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) { /* 找client[]中沒有使用的位置 */client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */break;}if (i == FD_SETSIZE) { /* 達到select能監控的文件個數上限 1024 */fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset); /* 向監控文件描述符集合allset添加新的文件描述符connfd */if (connfd > maxfd)maxfd = connfd; /* select第一個參數需要 */if (i > maxi)maxi = i; /* 保證maxi存的總是client[]最后一個元素下標 */if (--nready == 0)continue;} for (i = 0; i <= maxi; i++) { /* 檢測哪個clients 有數據就緒 */if ((sockfd = client[i]) < 0)continue;//數組內的文件描述符如果被釋放有可能變成-1if (FD_ISSET(sockfd, &rset)) {if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 當client關閉鏈接時,服務器端也關閉對應鏈接 */Close(sockfd);FD_CLR(sockfd, &allset); /* 解除select對此文件描述符的監控 */client[i] = -1;} else if (n > 0) {for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);Write(STDOUT_FILENO, buf, n);}if (--nready == 0)break; /* 跳出for, 但還在while中 */}}}Close(listenfd);return 0;
}
若?
lfd = 3
,maxfd = 100
,但僅 5 個活躍連接,基礎版仍需循環 97 次(4~100),而進階版僅需循環 5 次(數組中的活躍 FD)。
局限性
雖然使用 select 這種 IO 多路轉接技術可以降低系統開銷,提高程序效率,但是它也有局限性:
待檢測集合(第 2、3、4 個參數)需要頻繁的在用戶區和內核區之間進行數據的拷貝,效率低
內核對于 select 傳遞進來的待檢測集合的檢測方式是線性的
檢測效率與集合內待檢測的文件描述符有關:如果集合內待檢測的文件描述符很多,檢測效率會比較低;如果集合內待檢測的文件描述符相對較少,檢測效率會比較高
使用 select 能夠檢測的最大文件描述符個數有上限,默認是 1024,這是在內核中被寫死了的
poll
poll 的機制與 select 類似,與 select 在本質上沒有多大差別,使用方法也類似,下面的是對于二者的對比:
- 內核對應文件描述符的檢測也是以線性的方式進行輪詢,根據描述符的狀態進行處理
- poll 和 select 檢測的文件描述符集合會在檢測過程中頻繁的進行用戶區和內核區的拷貝,它的開銷隨著文件描述符數量的增加而線性增大,從而效率也會越來越低。
- select 檢測的文件描述符個數上限是 1024,poll 沒有最大文件描述符數量的限制
- select 可以跨平臺使用,poll 只能在 Linux 平臺使用
- select 通過 fd_set(位圖集合)來記錄文件描述符,poll 使用一個整型數來記錄
poll 函數
#include <poll.h>
// 每個委托poll檢測的fd都對應這樣一個結構體
struct pollfd {
? ? int ? fd; ? ? ? ? /* 委托內核檢測的文件描述符 */
? ? short events; ? ? /* 委托內核檢測文件描述符的什么事件 */
? ? short revents; ? ?/* 文件描述符實際發生的事件 -> 傳出 */ ?// 不需要進行初始化
};struct pollfd myfd[100]; ? // 可能需要檢測若干個文件描述符,要存儲在數組中
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
?
參數含義:
fds: 這是一個 struct pollfd 類型的數組, 里邊存儲了待檢測的文件描述符的信息,這個數組中有三個成員:fd:委托內核檢測的文件描述符
events:委托內核檢測的 fd 事件(輸入、輸出、錯誤),每一個事件有多個取值
revents:這是一個傳出參數,數據由內核寫入,存儲內核檢測之后的結果(不需要初始化,根據 events 委托內核檢測的時間傳出結果)
nfds: 這是第一個參數數組中最后一個有效元素的下標 + 1(也可以指定參數 1 數組的元素總個數)
timeout: 指定 poll 函數的阻塞時長
-1:一直阻塞,直到檢測的集合中有就緒的文件描述符(有事件產生)解除阻塞
0:不阻塞,不管檢測集合中有沒有已就緒的文件描述符,函數馬上返回
大于 0:阻塞指定的毫秒(ms)數之后,解除阻塞
返回值:成功返回集合中已就緒的文件描述符的總個數,失敗返回-1
server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h" // 自定義的包裹函數頭文件(如Socket、Bind等)#define MAXLINE 80 // 緩沖區大小
#define SERV_PORT 6666 // 服務器端口
#define OPEN_MAX 1024 // 最大文件描述符數量int main(int argc, char *argv[])
{int i, j, maxi, listenfd, connfd, sockfd;int nready; // poll返回的就緒文件描述符數量ssize_t n; // 讀取的字節數char buf[MAXLINE], str[INET_ADDRSTRLEN]; // 緩沖區和IP地址字符串socklen_t clilen; // 客戶端地址長度struct pollfd client[OPEN_MAX]; // poll監控的文件描述符數組struct sockaddr_in cliaddr, servaddr; // 客戶端和服務器地址結構/* 1. 創建監聽套接字 */listenfd = Socket(AF_INET, SOCK_STREAM, 0); // IPv4 TCP套接字/* 2. 綁定服務器地址 */bzero(&servaddr, sizeof(servaddr)); // 清空結構體servaddr.sin_family = AF_INET; // IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有本地IPservaddr.sin_port = htons(SERV_PORT); // 設置端口Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* 3. 開始監聽 */Listen(listenfd, 20); // 監聽隊列最大長度為20/* 4. 初始化poll監控數組 */client[0].fd = listenfd; // 第一個元素是監聽套接字client[0].events = POLLRDNORM; // 監聽普通讀事件(新連接)for (i = 1; i < OPEN_MAX; i++)client[i].fd = -1; // 其余元素初始化為-1(表示空閑)maxi = 0; // 當前client數組中有效元素的最大下標/* 5. 主循環:處理poll事件 */for (;;){// 阻塞等待事件發生,監控maxi+1個描述符(從0到maxi),無限等待nready = poll(client, maxi + 1, -1);/* 5.1 處理監聽套接字(新連接) */if (client[0].revents & POLLRDNORM) // 監聽套接字可讀(有新連接){clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); // 打印客戶端IP和端口/* 將新連接加入client數組 */for (i = 1; i < OPEN_MAX; i++){if (client[i].fd < 0){ // 找到第一個空閑位置client[i].fd = connfd; // 存儲新連接的描述符break;}}if (i == OPEN_MAX) // 超過最大連接數限制perr_exit("too many clients");client[i].events = POLLRDNORM; // 對新連接監控讀事件if (i > maxi)maxi = i; // 更新最大有效下標if (--nready <= 0) // 如果沒有更多就緒事件,繼續pollcontinue;}/* 5.2 處理已連接套接字的數據 */for (i = 1; i <= maxi; i++) // 遍歷所有可能的連接{if ((sockfd = client[i].fd) < 0) // 跳過無效描述符continue;/* 檢查讀事件或錯誤事件 */if (client[i].revents & (POLLRDNORM | POLLERR)){n = Read(sockfd, buf, MAXLINE); // 讀取數據if (n < 0){// 讀取錯誤if (errno == ECONNRESET) // 客戶端發送RST重置連接{printf("client[%d] aborted connection\n", i);Close(sockfd);client[i].fd = -1; // 重置為未使用}else{perr_exit("read error"); // 其他錯誤直接退出}}else if (n == 0) // 客戶端關閉連接{printf("client[%d] closed connection\n", i);Close(sockfd);client[i].fd = -1;}else // 正常讀取數據{for (j = 0; j < n; j++) // 轉為大寫buf[j] = toupper(buf[j]);Writen(sockfd, buf, n); // 回寫給客戶端}if (--nready <= 0) // 沒有更多就緒事件,跳出循環break;}}}return 0;
}
client
客戶端使用:
nc 127.0.0.1 6666
模擬客戶端鏈接服務器
epoll
epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率,因為它會復用文件描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
目前epell是linux大規模并發網絡程序中的熱門首選模型。
epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。
可以使用cat命令查看一個進程可以打開的socket描述符上限。
cat /proc/sys/fs/file-max
如有需要,可以通過修改配置文件的方式修改該上限值。
sudo vi /etc/security/limits.conf
在文件尾部寫入以下配置,soft軟限制,hard硬限制。如下圖所示。
* soft nofile 65536
* hard nofile 100000
基礎API
1.創建一個epoll句柄,參數size用來告訴內核監聽的文件描述符的個數,跟內存大小有關。
#include <sys/epoll.h>
int epoll_create(int size) size:監聽數目
2.控制某個epoll監控的文件描述符上的事件:注冊、修改、刪除。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: 為epoll_creat的句柄
op: 表示動作,用3個宏來表示:
EPOLL_CTL_ADD (注冊新的fd到epfd),
EPOLL_CTL_MOD (修改已經注冊的fd的監聽事件),
EPOLL_CTL_DEL (從epfd刪除一個fd);
event: 告訴內核需要監聽的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
EPOLLIN : 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)
EPOLLOUT: 表示對應的文件描述符可以寫
EPOLLPRI: 表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來)
EPOLLERR: 表示對應的文件描述符發生錯誤
EPOLLHUP: 表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)而言的
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
3.等待所監控文件描述符上有事件的產生,類似于select()調用。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events: 用來存內核得到事件的集合,
maxevents: 告之內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size,
timeout: 是超時時間
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就緒,時間到時返回0,出錯返回-1
server
處理流程:
1.創建 epoll 實例對象 epoll_create
2.將用于監聽的套接字添加到 epoll 實例中 epoll_ctl
3.檢測添加到 epoll 實例中的文件描述符是否已就緒,并將這些已就緒的文件描述符進行處理 epoll_wait
- 如果是監聽的文件描述符,和新客戶端建立連接,將得到的文件描述符添加到 epoll 實例中
- 如果是通信的文件描述符,和對應的客戶端通信,如果連接已斷開,將該文件描述符從 epoll 實例中刪除
?
#include <stdio.h>
#include <fcntl.h>
#include "wrap.h"
#include <sys/epoll.h>
int main(int argc, char *argv[])
{//創建套接字 綁定int lfd = tcp4bind(8000,NULL);//監聽Listen(lfd,128);//創建樹int epfd = epoll_create(1);//將lfd上樹struct epoll_event ev,evs[1024];ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);//while監聽while(1){int nready = epoll_wait(epfd,evs,1024,-1);//監聽printf("epoll wait _________________\n");if(nready <0){perror("");break;}else if( nready == 0){continue;}else//有文件描述符變化{ int i = 0;for( i=0;i<nready;i++){//判斷lfd變化,并且是讀事件變化if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN){struct sockaddr_in cliaddr;char ip[16]="";socklen_t len = sizeof(cliaddr);int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);//提取新的連接printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));//設置cfd為非阻塞int flags = fcntl(cfd,F_GETFL);//獲取的cfd的標志位flags |= O_NONBLOCK;fcntl(cfd,F_SETFL,flags);//將cfd上樹ev.data.fd =cfd;ev.events =EPOLLIN | EPOLLET;//設置為邊沿觸發epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else if( evs[i].events & EPOLLIN)//cfd變化 ,而且是讀事件變化{while(1){char buf[4]="";//如果讀一個緩沖區,緩沖區沒有數據,如果是帶阻塞,就阻塞等待,如果//是非阻塞,返回值等于-1,并且會將errno 值設置為EAGAINint n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出錯,cfd下樹{//如果緩沖區讀干凈了,這個時候應該跳出while(1)循環,繼續監聽if(errno == EAGAIN){break;}//普通錯誤perror("");close(evs[i].data.fd);//將cfd關閉epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客戶端關閉 ,{printf("client close\n");close(evs[i].data.fd);//將cfd關閉epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下樹break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}}}}}return 0;
}
client
客戶端使用:
nc 127.0.0.1 8000
模擬客戶端鏈接服務器