基于epoll的簡單高并發伺服器程式
1.什麼是epoll
epoll,select,poll都是基于Linux/Unix的io複用技術。所謂io複用簡單來說就是讓核心來告知我們哪些檔案描述符讀或寫準備就緒。具體定義大家可以查閱各類書籍。
epoll相對于select和poll來說對核心資源的利用更高效,因為select和poll是需要将帶有檔案描述符的資料結構拷貝到核心,通知時再把就緒的檔案描述符從核心區拷貝到用區。而epoll使用共享記憶體存儲資料,與核心區共享檔案描述符。
除此之外,epoll周遊檔案描述符是以紅黑樹周遊,而select和poll是以線性表的形式周遊。
2.epoll的使用
/*
epoll使用步驟:
- 建立一個epoll模型 -> 一個函數
- 将需要檢測的檔案描述符添加到epoll模型中
- 開始使用epoll檢測檔案描述符的狀态
- 檢測到了狀态變化
- 判斷是不是監聽的fd
- accept接受連接配接, 得到新的通信fd, 将其添加到epoll樹上
- 負判斷是不是通信的fd
- read
- 傳回值為0 -> 對方關閉連接配接
- 将這個fd從樹上删除
- write
*/
函數:
// 建立一個epoll模型
#include <sys/epoll.h>
int epoll_create(int size);
參數:
- size: 保證其大于0即可, 沒有其他的實際意義
傳回值:
是一個檔案描述符, 了解為建立的epoll樹的根節點
// 對epoll樹操作函數, 添加節點/删除節點/修改節點屬性
typedef union epoll_data {
void *ptr;
int fd; // 通常使用這個變量, 和epoll_ctl第三個參數值相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events事件的取值為三個宏:
- EPOLLIN: 讀事件 -> 檢測是fd的讀緩沖區
- EPOLLOUT: 寫事件 -> 檢測是fd的寫緩沖區
- EPOLLERR: 異常 -> 檢測fd是否有異常
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數:
- epfd: epoll數的根節點, 通過 epoll_create(int size); 得到的, 傳回值
- op: 對檔案描述符的操作
EPOLL_CTL_ADD: 添加新節點
EPOLL_CTL_MOD: 修改節點是屬性
EPOLL_CTL_DEL: 删除現有的節點
- fd: 要操作的檔案描述符
- event: fd的屬性設定
- 如果是删除操作, 這個參數指定為NULL即可
// 委托核心檢測epoll樹上的檔案描述符狀态
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數:
- epfd: epoll數的根節點, 通過 epoll_create(int size); 得到的, 傳回值
- events: 傳出參數, 指向一個數組的位址, 存儲發送變化的檔案描述符的資訊
- maxevents: 參數events元素的個數
- timeout: 阻塞的時長, 機關是毫秒, ms
- 0: 直接傳回
- -1: 一直阻塞, 直到檢測的檔案描述符有狀态變化, 解除阻塞
- >0: 阻塞的毫秒數,
- 在阻塞期間如果檢測的檔案描述符有狀态變化, 解除阻塞
- 沒有變化, 時間到了解除阻塞
傳回值:
-1: 失敗
>0: 狀态有變化的檔案描述符的個數
代碼:
#include <cstdio>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
//1.建立套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd==-1)
{
perror("socket");
exit(0);
}
//2.lfd綁定本地的IP和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);//主機序轉網絡序
server_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*) &server_addr, sizeof(server_addr));
//注意第二參數要強制轉換為(struct socket*)類型
//3,監聽lfd
ret = listen(lfd, 128);
if (ret==-1)
{
perror("listen");
exit(0);
}
//4.建立epoll樹
int epfd=epoll_create(0);
//5.将監聽檔案描述符添加到epoll樹上
//一旦産生連接配接就會被核心檢測到,我們後面做處理
struct epoll_event lev;
lev.data.fd = lfd;
lev.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd,&lev);
if (ret==-1)
{
perror("epoll_ctl");
exit(0);
}
//6.建立epoll_event 結構體數組
struct epoll_event epr[100];
int size = sizeof(epr) / sizeof(struct epoll_event);
//7.開始循環檢測
while (true)
{
int count = epoll_wait(epfd, epr, size, -1);
//傳回值為epoll樹上檔案讀寫緩沖區有變化的檔案描述符個數
//将有變化的描述符放到epr數組裡
for (int i = 0; i < count; i++)
{
if (lfd==epr[i].data.fd)
{ //如果是監聽描述符則accept獲得通信描述符
//并将通信描述符上epoll樹
int cfd = accept(lfd, NULL, NULL);
lev.data.fd = cfd;
lev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &lev);
}
else
{ //如果不是監聽的就是通信的
char buf[100] = { 0 };
int len = read(epr[i].data.fd, buf, sizeof(buf));
//recv(epr[i].data.fd, buf, sizeof(buf), 0);也行
if (len>0)
{
//通信 回複"收到資料"
write(epr[i].data.fd,"收到資料", len);
//send(epr[i].data.fd, buf, len, 0);也行
}
else
{
perror("read");
exit(0);
}
}
}
}
}