1. 檔案描述符型別
REG :檔案
DIR:目錄
CHR :字元
BLK:塊裝置
UNIX:unix域套接字
FIFO :先進先出佇列
IPv4:網際協議 (IP) 套接字
其中, 標準輸入STDIN(0)和STDOUT輸出(1), STDERR錯誤(2)為指定的值
2. IO複用模型
(1). select (在指定的一段時間內,輪詢監聽使用者需要的檔案描述符(使用者新增到fd_set中的),當監聽到的檔案描述符傳來可讀、可寫或異常事件發生時就會返回)
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds: 指定被監聽檔案描述符的總數,因為檔案描述符通常從0開始計數,因此nfds通常為readfd,writefd,exceptfd這三個描述符集中的最大描述符編號加1;
readfd,writefd,exceptfd: 分別指向可讀、可寫、異常事件對應的檔案描述符集合。應用程式呼叫select時,透過這三個引數傳入需要監聽的檔案描述符,輪詢等待有事件產生
timeout: 超時間設定; NULL表示一直阻塞直到某個檔案描述符就緒; 非NULL表示超時時間後立即返回;
返回值: 大於0, 描述符有資料返回; 等於0超時; 小於0, select錯誤;
理解select模型的關鍵在於理解fd_set,為說明方便,取fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd。
(1)執行fd_set set;FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變為0001,0011
(4)執行select(6,&set,0,0,0)阻塞等待---讀取
(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。
(2). epoll
#include <sys/epoll.h>
int epoll_create(int size);
函式功能: 建立epoll專用檔案描述符的緩衝區。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函式功能: 新增、刪除、修改 監聽檔案描述符。
函式引數:
int epfd epoll專用的檔案描述符
int op 操作命令。EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL
int fd 要操作檔案描述符
struct epoll_event *event 存放監聽檔案描述符資訊的結構體
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函式功能: 等待事件發生。
函式引數:
int epfd epoll專用的檔案描述符
struct epoll_event *events : 存放產生事件的檔案描述結構體。
int maxevents :最大監聽的數量.
int timeout :等待事件ms單位. <0 >0 ==0
返回值: 產生事件的數量。
typedef union epoll_data {
void *ptr;
int fd; //檔案描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events EPOLLIN 輸入事件 */
epoll_data_t data; /* User data variable */
};
(3). select和epoll區別:
select epoll
效能: 隨著連線數增加, 急劇下降。處理成千上萬併發連線數時, 連線數增加時, 效能基本上沒有下降, 大量併發連線時,
效能很差 效能很好
連線數: 連線數量有限制,最大連線數不超過1024 連線數無限制
內在處理機制: 線性輪詢 回撥callback
開發複雜性 低 中
簡單理解:
住校時,你的朋友來找你:
select版宿管阿姨,帶著你的朋友挨個房間找,直到找到你
epoll版阿姨,會先記下每位同學的房間號, 你的朋友來時,只需告訴你的朋友你住在哪個房間,無需親自帶著你朋友滿大樓逐個房間找人
如果來了10000個人,都要找自己住這棟樓的同學時,select版和epoll版宿管大媽,誰效率高?同理,高併發伺服器中,輪詢I/O是最耗時操作之一,epoll效能更高也是很明顯。
select的呼叫複雜度O(n)。如一個保姆照看一群孩子,如果把孩子是否需要尿尿比作網路I/O事件,select就像保姆挨個詢問每個孩子:你要尿尿嗎?若孩子回答是,保姆則把孩子拎出來放到另外一個地方。當所有孩子詢問完之後,保姆領著這些要尿尿的孩子去上廁所(處理網路I/O事件)
epoll機制下,保姆無需挨個詢問孩子是否要尿尿,而是每個孩子若自己需要尿尿,主動站到事先約定好的地方,而保姆職責就是檢視事先約定好的地方是否有孩子。若有小孩,則領著孩子去上廁所(網路事件處理)。因此,epoll的這種機制,能夠高效的處理成千上萬的併發連線,而且效能不會隨著連線數增加而下降。
Epoll 使用
EpollServer.cpp
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> #include <sys/epoll.h> #include <iostream> #include <string> #include <list> using namespace std; typedef struct _ClientFd { int m_fd = -1; // 連線控制代碼 string name = string(); // 名稱 } ClientFd; // socket控制代碼 int sockfd; //訊息結構體, 目前協定的訊息接受傳送的格式 struct MSG_DATA { char type; //訊息型別. 0表示有聊天的訊息資料 1表示好友上線 2表示好友下線 // char name[50]; //好友名稱 // unsigned char buff[100]; //傳送的聊天資料訊息 string name; //好友名稱 string buff; //傳送的聊天資料訊息 int number; //線上人數的數量 }; #define MAX_EPOLL_FD 100 struct epoll_event events[MAX_EPOLL_FD]; struct epoll_event event; int epfd; int nfd; struct MSG_DATA msg_data; // 存放客戶端控制代碼列表 std::list<ClientFd*> clientFdList; // 新增描述符 void List_addFd(int fd) { ClientFd *pFd = new ClientFd(); pFd->m_fd = fd; clientFdList.emplace_back(pFd); } // 獲取成員名稱 void List_getName(struct MSG_DATA *msg_data,int client_fd) { auto iter = clientFdList.begin(); while( iter != clientFdList.end() ) { if ( (*iter)->m_fd == client_fd ) { msg_data->name = (*iter)->name; break; } iter++; } } // 刪除描述符 void List_DelFd(int clientFd) { auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { iter = clientFdList.erase(iter); break; } ++iter; } } int List_GetCnt() { return clientFdList.size(); } // 儲存檔案 void List_SaveName(struct MSG_DATA *msg_data, int clientFd) { auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { msg_data->name = (*iter)->name; break; } ++iter; } } // 伺服器轉發訊息 void Server_SendMsgData(struct MSG_DATA *msg_data,int clientFd) { printf("%d | %s\n", __LINE__, __FUNCTION__); auto iter = clientFdList.begin(); while ( iter != clientFdList.end() ) { if ( (*iter)->m_fd == clientFd ) { printf(""); write((*iter)->m_fd, msg_data, sizeof(struct MSG_DATA)); break; } ++iter; } } /*訊號工作函式*/ void signal_work_func(int sig) { (void)sig; close(sockfd); exit(0); //結束程序 } // 函式入口 int main(int argc,char **argv) { if(argc!=2) { printf("./app <埠號>\n"); return 0; } signal(SIGPIPE,SIG_IGN); //忽略 SIGPIPE 訊號--防止伺服器異常退出 signal(SIGINT,signal_work_func); /*1. 建立socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); /*2. 繫結埠號與IP地址*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[1])); // 埠號0~65535 addr.sin_addr.s_addr=INADDR_ANY; //inet_addr("0.0.0.0"); //IP地址 if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0) { perror("伺服器:埠號繫結失敗.\n"); return -1; } /*3. 設定監聽的數量*/ listen(sockfd,20); /*4. 等待客戶端連線*/ int client_fd; struct sockaddr_in client_addr; socklen_t addrlen; int i; int cnt; /*5. 建立epoll相關的介面*/ epfd=epoll_create(MAX_EPOLL_FD); event.events=EPOLLIN; //監聽的事件 event.data.fd=sockfd; //監聽的套接字 epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event); while(1) { //等待事件發生, 有資料 nfd=epoll_wait(epfd,events,MAX_EPOLL_FD,-1); for(i=0;i<nfd;i++) { if(events[i].data.fd==sockfd) //表示有新的客戶端連線上伺服器 { client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&addrlen); printf("連線的客戶端IP地址:%s\n",inet_ntoa(client_addr.sin_addr)); printf("連線的客戶端埠號:%d\n",ntohs(client_addr.sin_port)); //儲存已經連線上來的客戶端 // List_AddNode(list_head,client_fd); // 新增新連線客戶端的控制代碼 List_addFd(client_fd); //將新連線的客戶端套接字新增到epoll函式監聽佇列裡 event.data.fd=client_fd; //監聽的套接字 epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event); } else //表示客戶端給伺服器傳送了訊息-----實現訊息的轉發 { //讀取客戶端傳送的訊息, 儲存在msg_data中 cnt = read(events[i].data.fd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //表示當前客戶端斷開了連線 { //獲取名稱 // List_GetName(list_head,&msg_data,events[i].data.fd); //刪除節點 // List_DelNode(list_head,events[i].data.fd); //獲取名稱 List_getName(&msg_data, events[i].data.fd); //刪除節點 List_DelFd(events[i].data.fd); msg_data.type=2; // //將斷開連線的客戶端套接字從epoll函式監聽佇列裡刪除呼叫 event.data.fd = events[i].data.fd; //監聽的套接字 epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&event); close(event.data.fd); } if(msg_data.type==1) //好友上線的時候儲存一次名稱 { //儲存名稱 List_SaveName(&msg_data,events[i].data.fd); } //轉發訊息給其他好友 msg_data.number = List_GetCnt(); //當前線上好友人數 Server_SendMsgData(&msg_data,events[i].data.fd); } } } //退出程序 signal_work_func(0); return 0; }
EpollClient.cpp
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> #include <poll.h> #include <sys/epoll.h> //訊息結構體 struct MSG_DATA { char type; //訊息型別. 0表示有聊天的訊息資料 1表示好友上線 2表示好友下線 char name[50]; //好友名稱 int number; //線上人數的數量 unsigned char buff[100]; //傳送的聊天資料訊息 }; struct MSG_DATA msg_data; #define MAX_EVENTS 2 struct epoll_event ev, events[MAX_EVENTS]; int epollfd; int nfds; //檔案接收端 int main(int argc,char **argv) { if(argc!=4) { printf("./app <IP地址> <埠號> <名稱>\n"); return 0; } int sockfd; //忽略 SIGPIPE 訊號--方式伺服器向無效的套接字寫資料導致程序退出 signal(SIGPIPE,SIG_IGN); /*1. 建立socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); /*2. 連線伺服器*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[2])); // 埠號0~65535 addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址 if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0) { printf("客戶端:伺服器連線失敗.\n"); return 0; } /*3. 傳送訊息表示上線*/ msg_data.type=1; strcpy(msg_data.name,argv[3]); write(sockfd,&msg_data,sizeof(struct MSG_DATA)); int cnt; int i; //建立專用檔案描述符 epollfd = epoll_create(10); //新增要監聽的檔案描述符 ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev); ev.events = EPOLLIN; ev.data.fd = 0; //標準輸入檔案描述符 epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev); while(1) { //監聽事件 nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1); if(nfds) { for(i=0;i<nfds;i++) { if(events[i].data.fd==sockfd) //判斷收到伺服器的訊息 { cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //判斷伺服器是否斷開了連線 { printf("伺服器已經退出.\n"); goto SERVER_ERROR; } else if(cnt>0) { if(msg_data.type==0) { printf("%s:%s 線上人數:%d\n",msg_data.name,msg_data.buff,msg_data.number); } else if(msg_data.type==1) { printf("%s 好友上線. 線上人數:%d\n",msg_data.name,msg_data.number); } else if(msg_data.type==2) { printf("%s 好友下線. 線上人數:%d\n",msg_data.name,msg_data.number); } } } else if(events[i].data.fd==0) //表示鍵盤上有資料輸入 { gets(msg_data.buff); //讀取鍵盤上的訊息 msg_data.type=0; //表示正常訊息 strcpy(msg_data.name,argv[3]); //名稱 write(sockfd,&msg_data,sizeof(struct MSG_DATA)); } } } } SERVER_ERROR: close(sockfd); return 0; }