多路I/O轉接服務器(select、poll、epoll)

多路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

  1. select能監聽的文件描述符個數受限于FD_SETSIZE,一般為1024,單純改變進程打開的文件描述符個數并不能改變select監聽文件個數

  2. 解決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 = 3maxfd = 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

模擬客戶端鏈接服務器

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87312.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87312.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87312.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

最新臨時文件快傳系統源碼 輕量化 帶后臺

簡介&#xff1a; 最新臨時文件快傳系統源碼 輕量化 帶后臺 首發 輕松上傳文件并生成提取碼分享給他人&#xff0c;無需注冊&#xff0c;方便快捷。 圖片&#xff1a;

MyBatis多數據源動態連接工具類實現

這個DatabaseService工具類提供了動態創建MyBatis SqlSession的能力&#xff0c;可以靈活地連接到不同的數據庫&#xff0c;非常適合需要動態切換數據源的場景。 package com.cmes.immp.device.utils;import lombok.SneakyThrows; import org.apache.commons.dbcp2.BasicDataS…

用亮數據 MCP 驅動 Trae 智能體:打造高效亞馬遜商品采集與分析助手

本文適合希望快速構建數據驅動型智能體的開發者、數據工程師及 AI 產品設計者閱讀 并非廣告&#xff0c;希望本文可以幫助有需求的同學&#xff0c;祝大家天天開心 在數字時代&#xff0c;數據是決策與洞察趨勢的關鍵。但移動互聯網數據獲取不易&#xff0c;傳統爬蟲技術面對復…

如何降低AIGC生成內容的重復率?五種免費降AI率的方法 (25年更新)

隨著AI生成內容&#xff08;AIGC&#xff09;的普及&#xff0c;越來越多的學術寫作依賴AI工具來生成論文和文章。然而&#xff0c;AI生成內容的查重率常常偏高&#xff0c;導致很多論文無法通過學術查重系統。為了解決這一問題&#xff0c;以下是五種有效的免費降AIGC率的方法…

小米YU7使用UWB技術,厘米級定位精準迎賓,安全防破解無感控車

當您雙手抱著快遞走向愛車時&#xff0c;車門自動解鎖&#xff1b;當您站在前備箱前稍作停留&#xff0c;箱蓋優雅升起——這不是科幻電影&#xff0c;而是小米YU7搭載UWB技術帶來的真實體驗。在2025年5月的小米15周年戰略新品發布會上&#xff0c;雷軍揭曉了這項革命性技術&am…

WPF學習(動畫)

文章目錄 一、圖像變換 RenderTransform1、常見變換類型2、RenderTransform 的核心作用3、RenderTransform 的使用方式4、與 LayoutTransform 的對比5、在動畫中的應用 二、 滾動的橢圓三、Storyboard放置位置1. **元素的 Resources 集合**2. **控件模板&#xff08;ControlTem…

Crossbar結構的排隊策略

目錄 一、概述 二、排隊策略 三、輸入排隊結構(IQ) 3.1 結構特點 3.2 改進方案 四、輸出排隊結構&#xff08;OQ&#xff09; 五、輸入輸出聯合排隊結構(CIOQ) 六、輸入交叉節點聯合排隊結構(CICQ) 一、概述 Crossbar是一種全連接的交換結構&#xff0c;由 MN 個交叉…

狀態模式 - Flutter中的狀態變身術,讓對象隨“狀態“自由切換行為!

訂單狀態流轉/播放器控制/游戲角色行為…一個模式搞定所有狀態驅動型邏輯&#xff01; 經典場景&#xff1a;訂單狀態管理 假設你在開發一個外賣App&#xff0c;訂單有以下狀態&#xff1a; 等待接單已接單配送中已完成已取消 每個狀態下&#xff1a; 顯示的UI不同可執行的…

數據庫9:數據庫字符編碼調整與校隊(排序)規則

一.常用字符編碼 1.ASCII編碼 用一個字節表示一個字符 2.ANSI編碼 每個國家為了顯示本國的語言而對ASCII碼進行了拓展 用兩個字節表示一個漢字&#xff0c;中國的ANSI編碼是GB2312編碼&#xff08;簡體&#xff09;&#xff0c;日本的ANSI編碼是JIS編碼&#xff0c;臺灣的A…

人臉活體識別4:Android實現人臉眨眼 張嘴 點頭 搖頭識別(可實時檢測)

人臉活體識別4&#xff1a;Android實現人臉眨眼 張嘴 點頭 搖頭識別(可實時檢測) 目錄 人臉活體識別4&#xff1a;Android實現人臉眨眼 張嘴 點頭 搖頭識別(可實時檢測) 1. 前言 2.人臉活體識別方法 &#xff08;1&#xff09;基于人臉動作的檢測?? &#xff08;2&…

DAY1-Linux操作系統1

文章參考【黑馬程序員Python教程_600集Python從入門到精通教程&#xff08;懂中文就能學會&#xff09;】 https://www.bilibili.com/video/BV1ex411x7Em/?p40&share_sourcecopy_web&vd_source263bbee2ddeb835c3ab6d9d3c80e0f7c 一.常用命令簡單介紹 使用軟件 虛擬機…

第十二節:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入門 - 兩種權限控制方式(附前后端代碼)

Vben5 系列文章目錄 ?? 基礎篇 ? 第一節:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入門 ? 第二節:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入門 - Python Flask 后端開發詳解(附源碼) ? 第三節:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入…

華為云Flexus+DeepSeek征文 | 華為云 ModelArts Studio 賦能 AI 法務:合同審查與法律文件生成系統

一、引言 在法律行業數字化轉型的浪潮中&#xff0c;AI 技術正重塑法律服務的流程與效率。本文介紹如何利用華為云 ModelArts Studio 構建一套完整的 AI 法務系統&#xff0c;實現合同審查、法律文件生成、法律咨詢與風險識別的智能化解決方案。 二、系統架構設計 &#xff0…

SQL的底層邏輯解析

SQL的底層邏輯涉及數據庫管理系統(DBMS)如何解析、優化和執行SQL查詢&#xff0c;主要包括以下幾個層面&#xff1a; ?查詢處理流程? 解析器(Parser)&#xff1a;將SQL語句轉換為語法樹查詢優化器(Optimizer)&#xff1a;基于統計信息和成本模型生成最優執行計劃執行引擎(Exe…

深入剖析AI大模型:PyTorch 技術詳解

今天說一說PyTorch。作為一名python程序員&#xff0c;可能對它了解起來還是很快的。在人工智能浪潮席卷全球的當下&#xff0c;深度學習作為其核心技術&#xff0c;被廣泛應用于圖像識別、自然語言處理、語音識別等多個領域。而在深度學習的開發框架中&#xff0c;PyTorch 憑借…

物聯網架構:定義、解釋和實例

物聯網&#xff08;IoT&#xff09;架構是一個復雜且多維度的概念&#xff0c;構成了物聯網系統的核心框架。它是勾勒物聯網設備、應用程序和技術如何相互交互以實現預期功能的藍圖。物聯網架構并非 “一刀切” 的模型&#xff0c;而是會根據相關物聯網系統的具體需求而有所不同…

拿到一臺新服務器,怎么跑AI項目

公司新采購一臺AI服務器&#xff0c;花大本錢裝了個A6000顯卡&#xff0c;今天來記錄下新服務的使用步驟。 1、查看系統。 這臺服務器預裝了Ubuntu20.04系統。 lsb_release -a 查看下cpu、內存情況 top 看著還行。 再看下硬盤空間 df -h 空間不算小&#xff0c;2T。 2、…

IO--進程實操

1.創建一個進程扇 #include <051head.h> int main(int argc, const char *argv[]) {pid_t pid;for(int i0;i<4;i){pidfork();if(pid-1) //父進程{ERRLOG("fork error..\n");} else if(pid0) //這是子進程{ …

模型預測控制(MPC)概覽

模型預測控制&#xff08;Model Predictive Control, MPC&#xff09; 一、理論基礎與發展脈絡 1. 歷史起源 20世紀70年代起源于工業過程控制&#xff08;如化工領域的動態矩陣控制DMC、模型算法控制MAC&#xff09;&#xff0c;由Richalet、Mehra等學者提出&#xff0c;核心…

Python初體驗:從入門到實踐

Python無疑是開啟編程世界大門的絕佳鑰匙。今天,就讓我們一起踏上Python的學習之旅。 #01 編寫第一個Python程序 環境搭建好之后,上節已經編寫了第一個Python程序。現在就好比,我們已經準備好了廚房和食材,要開始做第一道菜了。啟動Jupyter后,在Jupyter中新建一個文件,…