目錄
一、概念
二、語法
1.select
1.1 select函數的語法
1.2 文件描述符集合操作
1.3 select函數的優缺點
2.epoll
2.1 epoll語法
2.2 epoll的工作模式
2.3 epoll的優缺點
三、select服務端代碼
四、epoll服務端代碼
五、客戶端代碼
?
一、概念
IO多路復用是一種同步的I/O模型,它允許一個進程同時監視多個文件描述符,一旦某個文件描述符就緒(可以進行I/O操作),就能夠通知程序進行相應的讀寫操作。
IO多路復用有三種實現方式:select、poll、epoll。
?
二、語法
1.select
select是最早的I/O多路復用技術之一,它使用fd_set數據結構來存儲和跟蹤文件描述符。
select是基于線性方式處理待檢測集合的,因此每次都要遍歷集合;對于返回的集合,還需要判斷文件描述符是否就緒。
?
1.1 select函數的語法
select ( int nfds,
? ? ? ? ? ? fd_set *readfds,
? ? ? ? ? ? fd_set *writefds,
? ? ? ? ? ? fd_set *exceptfds,
? ? ? ? ? ? struct timeval *timeout );
- nfds:監控的文件描述符集里最大文件描述符加1。
- readfds:文件描述符集合,內核會監視此集合中的文件是否有數據可讀,傳入傳出參數。
- writefds:文件描述符集合,內核會監視此集合中的文件是否有數據可寫,傳入傳出參數。
- exceptfds:文件描述符集合,用于監視此集合中的文件是否有異常情況,傳入傳出參數。
- timeout:設置為NULL:阻塞,若檢測到就緒的文件描述符則返回其數量;設置時間:等待固定時間,若沒有就緒的文件描述符則返回0。
?
1.2 文件描述符集合操作
- FD_ZERO(fd_set *set):清空文件描述符集合。
- FD_SET(int fd, fd_set *set):將文件描述符fd添加到集合set中。
- FD_CLR(int fd, fd_set *set):從集合set中移除文件描述符fd。
- FD_ISSET(int fd, fd_set *set):檢查文件描述符fd是否在集合set中。
?
1.3 select函數的優缺點
- 優點:適用于多種操作系統,具有較好的兼容性。
- 缺點:當文件描述符數量較多時,效率會比較低。此外,select有一個固定的文件描述符數量限制,通常為1024。
?
2.epoll
epoll是一種高效且可擴展的I/O多路復用技術。epoll是基于紅黑樹來處理待檢測集合的,使用回調機制,處理效率高;其返回的集合中的文件描述符都是就緒的,無需再次進行檢測。
epoll主要通過以下三個函數來實現其功能:epoll_create、epoll_ctl、epoll_wait。
?
2.1 epoll語法
①epoll_create函數
epoll_create (int size);
創建一個epoll對象,返回一個文件描述符,用于后續的操作,參數size大于0即可。
②epoll_ctl函數
epoll_ctl ( int epfd, int op, int fd,
? ? ? ? ? ? ? ?struct epoll_event *event );
向epoll對象中添加、修改或刪除文件描述符。
- epfd:epoll_create函數返回的epoll文件描述符。
- op:操作類型,EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(刪除)。
- fd:要監聽的文件描述符。
- event:指向epoll_event結構體的指針,用于指定要監聽的事件類型。
epoll_event和epoll_data:
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_event結構體用于描述事件,成員events表示事件類型:
- EPOLLIN:讀事件,接收數據,檢測讀緩沖區,如果可讀則該文件描述符已就緒。
- EPOLLOUT:寫事件,發送數據,檢測寫緩沖區,如果可寫則該文件描述符已就緒。
- EPOLLET:設置為邊沿模式,默認是水平模式。
epoll_data為聯合體,通常使用int類型的fd,用于存儲發生對應事件的文件描述符。
?③epoll_wait函數
epoll_wait ( int epfd, struct epoll_event *events,
? ? ? ? ? ? ?? int maxevents, int timeout );
等待epoll對象中的事件發生,返回發生事件的文件描述符集合。
- epfd:epoll_create函數返回的epoll文件描述符。
- events:一個數組,用來存儲就緒的事件信息。
- maxevents:數組events的大小,即最多可以返回多少個就緒的文件描述符。
- timeout:表示epoll_wait阻塞等待的時間(毫秒)。若為負數則會阻塞,直到有事件就緒;若為0則不阻塞,立即返回當前已就緒的事件。
?
2.2 epoll的工作模式
①水平觸發(LT)模式:
水平觸發是epoll的默認工作模式。在這種模式下,當文件描述符上的事件就緒時,epoll_wait函數會返回,并且如果該事件沒有被處理,epoll_wait函數會在下一次調用時再次返回,直到該事件被處理。例如,當一個套接字上有數據可讀時,epoll_wait會返回,并且如果數據沒有被讀取,epoll_wait會在下一次調用時再次返回,直到數據被讀取。
②邊沿觸發(ET)模式:
邊沿觸發模式下,epoll_wait函數只會在文件描述符的狀態發生變化時返回。例如,當一個套接字上有數據可讀時,epoll_wait會返回,但是如果數據沒有被讀取,epoll_wait不會在下一次調用時再次返回,直到有新的數據到達。因此在邊沿觸發模式下,程序需要在一次epoll_wait調用返回后,立即處理所有就緒的事件。
③總結:
- 邊沿觸發模式通常比水平觸發模式的性能更高,因為它調用epoll_wait函數的次數比較少。
- 水平觸發模式更適合處理阻塞式I/O,而邊沿觸發模式更適合處理非阻塞式I/O。
epoll在邊沿模式下,必須將套接字設置為非阻塞模式,此時需要循環讀取讀緩沖區的數據,讀取完后recv函數會返回-1,此時需要特殊處理。
?
2.3 epoll的優缺點
- 優點:高效的事件驅動模型,支持大量并發連接,沒有固定的文件描述符數量限制。
- 缺點:僅適用于Linux系統,不具備跨平臺性。
?
三、select服務端代碼
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>using namespace std;int main(int argc, char* argv[])
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(8080);local.sin_addr.s_addr = INADDR_ANY;if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {cerr << "error" << endl;return -1;}if (listen(lfd, 128) == -1) {cerr << "error" << endl;return -1;}fd_set readset;FD_ZERO(&readset);FD_SET(lfd, &readset);int maxfd = lfd;while (true) {fd_set tmp = readset;int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);if (FD_ISSET(lfd, &tmp)) {int cfd = accept(lfd, NULL, NULL);FD_SET(cfd, &readset);maxfd = max(maxfd, cfd);cout << "connect: " << cfd << endl;}for (int i = 0; i <= maxfd; i++) {if (i != lfd && FD_ISSET(i, &tmp)) {char buffer[1024] = { 0 };int len = recv(i, buffer, sizeof buffer, 0);if (len == -1) {cerr << "error" << endl;return -1;}else if (len == 0) {cout << "client disconnect: " << i << endl;FD_CLR(i, &readset);close(i);break;}cout << buffer << endl;for (int i = 0; i < len; i++) {buffer[i] = toupper(buffer[i]);}len = send(i, buffer, strlen(buffer) + 1, 0);if (len == -1) {cerr << "error" << endl;return -1;}}}}close(lfd);return 0;
}
?
四、epoll服務端代碼
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>using namespace std;int main(int argc, char* argv[])
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(8080);local.sin_addr.s_addr = INADDR_ANY;if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {cerr << "error" << endl;return -1;}if (listen(lfd, 128) == -1) {cerr << "error" << endl;return -1;}int epfd = epoll_create(1);struct epoll_event event;event.events = EPOLLIN;event.data.fd = lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);struct epoll_event events[1024];while (true) {int fds = epoll_wait(epfd, events, 1024, -1);for (int i = 0; i < fds; i++) {int fd = events[i].data.fd;if (fd == lfd) {int cfd = accept(lfd, NULL, NULL);struct epoll_event client_event;int flag = fcntl(cfd, F_GETFL);flag |= O_NONBLOCK;fcntl(cfd, F_SETFL, flag);client_event.events = EPOLLIN | EPOLLET;client_event.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &client_event);cout << "connect: " << cfd << endl;}else {while(true) {char buffer[128] = { 0 };int len = recv(fd, buffer, sizeof buffer, 0);if (len == 0) {cout << "client disconnect: " << fd << endl;epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);close(fd);break;}else if (len > 0) {cout << buffer << endl;for (int i = 0; i < len; i++) {buffer[i] = toupper(buffer[i]);}len = send(fd, buffer, strlen(buffer) + 1, 0);if (len == -1) {cerr << "error" << endl;return -1;}}else if (len == -1) {if (errno = EAGAIN) {cout << "Data read complete." << endl;break;}else {cerr << "error" << endl;return -1;}}}}}}close(lfd);return 0;
}
?
五、客戶端代碼
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>using namespace std;int main(void)
{int client_socket = socket(AF_INET, SOCK_STREAM, 0);if (client_socket == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in target;target.sin_family = AF_INET;target.sin_port = htons(8080);inet_pton(AF_INET, "127.0.0.1", &target.sin_addr.s_addr);if (connect(client_socket, (struct sockaddr*)&target, sizeof target) == -1) {cerr << "error" << endl;close(client_socket);return -1;}while (true) {char buffer1[1024] = { 0 };cout << "enter: ";cin >> buffer1;send(client_socket, buffer1, strlen(buffer1), 0);char buffer2[1024] = { 0 };int ret = recv(client_socket, buffer2, sizeof buffer2, 0);if (ret <= 0) {cout << "server disconnect." << endl;}cout << buffer2 << endl;}close(client_socket);return 0;
}
?
參考內容:
愛編程的大丙