上一篇文章中,我們講解了select的用法和弊端.
https://blog.csdn.net/zhangkai19890929/article/details/95165596
select的最大弊端就是:
就是每次都要遍歷整個數據,來知道這個數組裏到底哪些sockfd可以讀寫,這樣的效率會導致我們處理高併發的瓶頸C10K(client 10k,也就是最大處理併發數量爲10000)
所以linux內核後面又提出了改進的網絡IO複用模型:epoll.
假設有10000路併發,某一個時刻只有1000個用戶發送了數據
相對於select io我們需要遍歷這10000路併發,並判斷其中哪些sock是可讀的,但是改進後的epoll,我們可以直接知道,是這1000路來了數據,我們只管遍歷這1000路就可以了.
epll優勢:
1.使用鏈表實現,理論上同時可以的高併發數量是無數個.
2.只需要關心活躍的鏈接,不需要關係所有的鏈接.
epoll api介紹:
epoll_create
epoll_ctl
int efd = epoll_create
創建鏈表,內核在底層創建鏈表,efd可以理解爲鏈表的頭.
有了鏈表那麼我們就需要往鏈接裏添加數據,鏈表裏的元素是:epoll_event,讓我們看下結構體 :
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
對應的epoll_data_t結構體爲:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
OK,那麼我們來看看如何向鏈表中添加和刪除事件:
向鏈表裏添加:
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &event_usercoming);
向鏈表裏刪除:
epoll_ctl(efd, EPOLL_CTL_DEL, event.data.fd, &event);
下面看我們寫的demo,進行完整的測試工程分析:
epoll_server.c
編譯命令: gcc -o epoll_server epoll_server.c
#include <sys/epoll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <time.h>
#define SERVER_PORT 9995
#define MAX_SOCKETS 256
#define MAX_EPOLL_EVENTS 1000
int main(int argc,char *argv[])
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = 0;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(SERVER_PORT);
ret = bind(sfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (ret < 0)
{
fprintf(stderr, "Error bind(). \n");
goto close;
}
ret = listen(sfd, MAX_SOCKETS);
if (ret < 0)
{
fprintf(stderr, "Error listen(). \n");
goto close;
}
int efd = epoll_create(1); //創建鏈表,efd表示是鏈表的頭
if (efd < 0)
{
fprintf(stderr, "epoll create fail . \n");
goto close;
}
//添加事件
struct epoll_event newEvent;
newEvent.events = EPOLLIN;//代表有用戶進來了!
newEvent.data.fd = sfd;
epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &newEvent);
struct epoll_event *events;
int cnt_event;
struct timespec waittime;
int epoll_waittime = 0;
uint8_t recvbuf[1024];
while (1)
{
clock_gettime(CLOCK_MONOTONIC, &waittime);
epoll_waittime = (waittime.tv_sec + 10) * 1000;//忽略了納秒,這種寫法不是特別的精準.
cnt_event = epoll_wait(efd, events, MAX_EPOLL_EVENTS, epoll_waittime);
if (cnt_event == -1)
{
fprintf(stderr, "errno:%d \n", errno);
break;
}
fprintf(stderr,"epoll wait cnt_vent:%d \n" , cnt_event);
//遍歷事件
for (int i = 0 ; i < cnt_event ; i++)
{
struct epoll_event event = events[i];
if (event.events & EPOLLIN) //代表可讀
{
if (event.data.fd == sfd)//new user coming
{
fprintf(stderr, "new user coming .... \n");
struct sockaddr_in addr;
int len = 0;//這裏必須設置初始值,不然會返回errno = 22的錯誤提示.
int cfd = accept(sfd, (struct sockaddr *)&addr , (socklen_t*)&len);//這裏爲何會返回-1呢? errno 22
if (cfd<0)
{
fprintf(stderr, "accept fail , errno:%d \n" , errno );
continue;
}
struct epoll_event event_usercoming;
event_usercoming.events = EPOLLIN ;
event_usercoming.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &event_usercoming);
//怎麼通知內核,這個事件我已經處理好了???
}
else
{
if (read(event.data.fd, recvbuf, sizeof(recvbuf)) <= 0)
{
epoll_ctl(efd, EPOLL_CTL_DEL, event.data.fd, &event);
}
else
{
fprintf(stderr, "receive content:%s \n" , recvbuf);
}
}
}
else if (event.events & EPOLLOUT)//代表可寫 --- 這裏有一個疑問,理論上來說,用戶終端是永遠可寫的,所以這個。。。。
{
//理論上寫都是隨時的,但是一般要根據協議來決定什麼時候寫,給對方寫client需要的數據.
fprintf(stderr, "you can send data to sock fd:%d \n" , event.data.fd);
}
}
}
close:
close(efd);
return 0;
}
現在要重點介紹epoll裏的兩種事件模型:LT/ET.
LT: – epoll 默認的處理方式.
ev.events = EPOLLIN;
ET:
ev.events = EPOLLIN | EPOLLET;
LT/ET的主要區別在於事件通知的方式不同:
LT的意思就是,有事件觸發,我通知你了,你就必須處理,如果你不處理,我下次來還要通知你.
ET的意思是,有事件觸發了,我通知你了,你處理不處理那都是你的事情,下一次我是不會再通知你了.
怎麼來測試,很簡單:
針對上面的例子,如果有客戶端連接了:
我們在LT模式下,不去執行accept事件,那麼epoll_wait每次都會觸發.
在ET模式下,我們不執行accept事件,那麼epoll_wait就只會觸發一次.
實測下:
實測發現,如果代碼中沒寫accept模塊,epoll_wait會直接返回-1,erro 返回 14,操蛋呀。。。。。
有可能是ubuntu在編譯環節就針對這種情況做了優化,目前本人使用的ubuntu系統爲18.04.
針對read操作呢?
1.客戶端發送10字節
2.服務器每次讀取2個字節
LT:
epoll_wait在LT模式下持續通知了四次,直到客戶端把這客戶端發送過來的數據讀完!
ET:
實打實的真的就只獲取了一次.