目錄
epoll LT模式實現的io多路轉接 服務器端代碼模板示例:
epoll ET模式實現的io多路轉接 服務器端代碼模板示例:
1. 端口複用
端口複用最常用用途:
- 防止服務器重啓時之前綁定的端口還未釋放
- 程序突然退出而系統沒有釋放端口
setsockopt函數
setsockopt函數有很多功能,這裏只講端口複用的功能。
#include <sys/types.h>
#include <sys/socket.h>
// 設置套接字屬性
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
參數:
- sockfd: 要操作的文件描述符
- level: 級別 -> SOL_SOCKET (端口複用的級別)
- optname: 端口複用的級別(二選一,隨便選,對於端口複用都可以)
- SO_REUSEADDR
- SO_REUSEPORT
- optval: 端口複用-> 對應的是整形數
- 1: 可以複用
- 0: 不能複用
- optlen: optval參數對應的內存大小
// 設置端口複用, 設置的時機: 服務器綁定端口之前, 設置端口複用。例如:
setsockopt();
bind();
2. IO多路轉接
使用進程/線程方式實現併發:
- 共同點:
有一個父親線程/進程 -> accept 某些情況下是阻塞的
子進程/子線程 -> 通信 -> read/write 某些情況下是阻塞的
- 不同點:
線程更節省系統資源, 一般寫程序, 考慮線程的實現方式
在一個進程中, 調用另一個進程 -> exec
進程/線程方式實現併發代碼模板:https://blog.csdn.net/qq_35883464/article/details/103643720
改進思路: 所有的阻塞狀態, 程序猿都不去判斷, 委託內核判斷, 得到內核回覆之後進行後續處理
- 程序猿是不能直接操作內核, 間接 -> 使用系統函數
- select
- poll
- epoll
IO多路轉接核心思想: 不再由應用程序自己監視客戶端連接和數據通信,取而代之由內核替應用程序監視文件。
2.1 select
select主旨思想:
- 先構造一張有關文件描述符的列表, 將要監聽的文件描述符添加到該表中
- 調用一個函數,監聽該表中的文件描述符,直到這些描述符表中的一個進行I/O操作時,該函數才返回。
- 函數對文件描述符的檢測操作是由內核完成的
- 在返回時,它告訴進程有多少(哪些)描述符要進行I/O操作。
#include <sys/select.h>
#include <unistd.h>
// sizeof(fd_set) = 128
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數:
- nfds: 委託內核檢測的最大文件描述符的值 + 1
- readfds: 讀集合, 委託內核檢測哪些文件描述符的讀屬性(讀的是對方發送過來的數據)
- 傳入傳出的參數
- fd_set: 寫集合, 委託內核檢測哪些文件描述符的寫屬性
- 傳入傳出的參數
- 委託內核檢測寫緩衝區是不是還可以寫數據(不滿就可以寫)
- exceptfds: 異常集合, 委託內核檢測哪些文件描述符出現了異常
- 傳入傳出的參數
- timeout:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 毫秒 */
};
- NULL: 永久阻塞, 直到檢測到了文件描述符有變化
- tv_sec = 0, tv_usec = 0, 不阻塞
- tv_sec > 0 || tv_usec > 0, 阻塞對應的時間長度
返回值:
-1: 失敗
>0(n): 檢測的集合中有n個文件描述符發送的變化
// 將參數文件描述符fd對應的標誌位, 設置爲0
void FD_CLR(int fd, fd_set *set);
// 判斷fd對應的標誌位到底是0還是1, 返回值: fd對應的標誌位的值, 0, 返回0, 1->返回1
int FD_ISSET(int fd, fd_set *set);
// 將參數文件描述符fd對應的標誌位, 設置爲1
void FD_SET(int fd, fd_set *set);
// fd_set 共有1024bit, 全部初始化爲0
void FD_ZERO(fd_set *set);
select函數演示:
fd_set表是自己設置的,和文件描述符表一樣。
值爲1:要讀的文件描述符。值爲0:不關心的文件描述符。
之後,select會修改文件描述符表,值爲1:要讀的文件描述符有數據。值爲0:不關心的文件描述符和沒有數據的文件描述符。
io多路轉接(select函數)服務器代碼模板
圖示代碼流程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
int main()
{
// 1.創建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 綁定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 監聽
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待連接 -> 循環
// 檢測 -> 讀緩衝區, 委託內核去處理
// 數據初始化, 創建自定義的文件描述符集
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1)
{
// 委託內核檢測
tmp = rdset;
ret = select(maxfd+1, &tmp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select");
exit(0);
}
// 檢測的度緩衝區有變化
// 有新連接
if(FD_ISSET(lfd, &tmp))
{
// 接收連接請求
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 這個accept是不會阻塞的
int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
// 委託內核檢測connfd的讀緩衝區
FD_SET(connfd, &rdset);
maxfd = connfd > maxfd ? connfd : maxfd;
}
// 通信, 有客戶端發送數據過來
for(int i=lfd+1; i<=maxfd; ++i)
{
// 如果在集合中, 說明讀緩衝區有數據
if(FD_ISSET(i, &tmp))
{
char buf[128];
int ret = read(i, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
exit(0);
}
else if(ret == 0)
{
printf("對方已經關閉了連接...\n");
FD_CLR(i, &rdset);
close(i);
}
else
{
printf("客戶端say: %s\n", buf);
write(i, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
3.2 poll
#include <poll.h>
struct pollfd {
int fd; /* 委託內核檢測的文件描述符 */
short events; /* 委託內核檢測文件描述符的什麼事件 */
short revents; /* 文件描述符實際發生的事件 */
};
例子:
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數:
- fds: 這是一個struct pollfd數組, 這是一個要檢測的文件描述符的集合
- nfds: 這是第一個參數數組中最後一個有效元素的下標 + 1
- timeout: 阻塞時長
0: 不阻塞
-1: 阻塞, 檢測的fd有變化解除阻塞
>0: 阻塞時長
返回值:
-1: 失敗
>0(n): 檢測的集合中有n個文件描述符發送的變化
2.3 epoll
- 內核檢測epoll傳遞的fd集合, 是以紅黑樹的形式遍歷的
-
epoll_create 創建函數
#include <sys/epoll.h>
// 創建一棵紅黑樹
int epoll_create(int size);
參數:
size: 沒意義, 隨便寫個數就行
返回值;
>0: 文件描述符, 操作epoll樹的根節點
-
epoll_ctl 控制函數
#include <sys/epoll.h>
typedef union epoll_data {
void *ptr; // 複雜
int fd; // 簡單 一般就使用這個就好,文件描述符傳入即可;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 上面這個共用體中,一般使用fd */
};
Epoll檢測的事件:
- EPOLLIN 讀
- EPOLOUT 寫
- EPOLLERR 錯誤
// 對epoll樹進行管理: 添加節點, 刪除節點, 修改已有的節點屬性
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數:
- epfd: epoll_create的返回值, 通過這個值就可以找到紅黑樹
- op: 要進行什麼樣的操作
EPOLL_CTL_ADD: 註冊新節點, 添加到紅黑樹上
EPOLL_CTL_MOD: 修改檢測的文件描述符的屬性
EPOLL_CTL_DEL: 從紅黑樹上刪除節點
- fd: 要檢測的文件描述符的值
- event: 檢測文件描述符的什麼事件
epoll_ctl函數工作流程:
-
epoll_wait 檢測函數
#include <sys/epoll.h>
struct epoll_event events[1000];
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數:
- epfd: epoll_create的返回值, 通過這個值就可以找到紅黑樹
- events: 傳出參數, 保存了發生變化的文件描述符的信息
- maxevents: 第二個參數結構體數組的大小
- timeout: 阻塞時間
- 0: 不阻塞
- -1: 一直阻塞, 知道檢測的fd有狀態變化, 解除阻塞
- >0: 阻塞的時長(毫秒)
返回值:
- 成功: 有多少個文件描述符狀態發生了變化 > 0
- 失敗: -1
2.4 epoll 的工作模式
2.4.1 LT模式(默認)
LT(level triggered)是缺省的工作方式,並且同時支持block(阻塞)和no-block (非阻塞)socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然後可以對這個就緒的fd進行IO操作。只要有數據,內核會一直通知
例如:
假設委託內核檢測讀事件 -> 檢測fd的讀緩衝區
- 讀緩衝區有數據 -> epoll檢測到了會給用戶通知
- 用戶就是不讀, 數據一直在緩衝區中, epoll會一直通知
- 用戶讀了一部分, epoll會通知用戶
- 緩衝區數據被讀完了, 就不在通知了
-
epoll LT模式實現的io多路轉接 服務器端代碼模板示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
int main()
{
// 1.創建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 綁定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 監聽
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 創建epoll樹
int epfd = epoll_create(0);
if(epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 將監聽lfd添加到樹上
struct epoll_event ev;
// 檢測事件的初始化
ev.events = EPOLLIN ;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event events[1024];
// 開始檢測
while(1)
{
int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
printf("numbers = %d\n", nums);
// 遍歷狀態變化的文件描述符集合
for(int i=0; i<nums; ++i)
{
int curfd = events[i].data.fd;
// 有新連接
if(curfd == lfd)
{
struct sockaddr_in clisock;
int len = sizeof(clisock);
int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
if(connfd == -1)
{
perror("accept");
exit(0);
}
// 將通信的fd掛到樹上
//ev.events = EPOLLIN | EPOLLOUT;
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 通信
else
{
// 讀事件觸發, 寫事件觸發
if(events[i].events & EPOLLOUT)
{
continue;
}
char buf[5];
int count = read(curfd, buf, sizeof(buf));
if(count == 0)
{
printf("client disconnect ...\n");
close(curfd);
// 從樹上刪除該節點
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
}
else if(count == -1)
{
perror("read");
exit(0);
}
else
{
// 正常情況傳輸數據,根據客戶端寫
//printf("client say: %s\n", buf);
write(STDOUT_FILENO, buf, count);
write(curfd, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
2.4.3 ET模式(邊沿觸發模式)
ET(edge-triggered)是高速工作方式,只支持no-block(非阻塞) socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll通知。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了。`但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)`。
ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高`。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。例子:
假設委託內核檢測讀事件 -> 檢測fd的讀緩衝區
- 讀緩衝區有數據 -> epoll檢測到了會給用戶通知
- 用戶就是不讀, 數據一直在緩衝區中, epoll下次檢測的時候就不通知了
如何設置ET模式:把 epoll_event 結構體中的 events 參數並上 EPOLLET
// 設置爲邊沿觸發模式, 修改事件的屬性
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
Epoll檢測的事件:
- EPOLLIN
- EPOLOUT
- EPOLLERR
- EPOLLET -> 設置邊沿觸發模式
設置通信文件描述符爲非阻塞:
// 獲取文件描述flag屬性
int flag = fcntl(fd, F_GETFL);
// 添加非阻塞屬性
flag = flag | O_NONBLOCK;
// 將新的flag屬性設置給fd
fcntl(fd, F_SETFL, flag);
-
epoll ET模式實現的io多路轉接 服務器端代碼模板示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
// 1.創建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 綁定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 監聽
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 創建epoll樹
int epfd = epoll_create(1000);
if(epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 將監聽lfd添加到樹上
struct epoll_event ev;
// 檢測事件的初始化
ev.events = EPOLLIN ;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event events[1024];
// 開始檢測
while(1)
{
int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
printf("numbers = %d\n", nums);
// 遍歷狀態變化的文件描述符集合
for(int i=0; i<nums; ++i)
{
int curfd = events[i].data.fd;
// 有新連接
if(curfd == lfd)
{
struct sockaddr_in clisock;
int len = sizeof(clisock);
int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
if(connfd == -1)
{
perror("accept");
exit(0);
}
// 將通信的fd掛到樹上
//ev.events = EPOLLIN | EPOLLOUT;
// 設置爲邊沿觸發
// 設置fd屬性爲非阻塞
int flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 通信
else
{
// 讀事件觸發, 寫事件觸發
if(events[i].events & EPOLLOUT)
{
continue;
}
char buf[5];
int count = 0;
while( (count = read(curfd, buf, sizeof(buf))) > 0)
{
// 處理read數據
write(STDOUT_FILENO, buf, count);
// 直接發送回客戶端
write(curfd, buf, count);
}
if(count == 0)
{
printf("client disconnect...\n");
}
else if(count == -1)
{
if(errno == EAGAIN)
{
printf("date over....\n");
}
else
{
perror("read");
exit(0);
}
}
}
}
}
close(lfd);
return 0;
}