- 前言
多进程和多线程模型在实现中相对简单, 但其开销和CPU高度比较大, 一般不用多线程和多进程来实现服务器多路模型.
select由于其跨平台, 但其最高上限默认为1024, 修改突破1024的话需要重新编译linux内核, poll虽然解决了select1024的限制, 但由于poll本质实现上也是轮询机制, 所以对于客户端的增加也会使效率降低.
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率
- 分析epoll API
//生成一个专用的文件描述符, size写多少就是多少, 但是如果运行过程中添加的越来越多, 后面的其实还会监听
int epoll_create(int size);
//用于控制某个epoll文件描述符事件,可以注册、修改、删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd: 生成的专用描述符, 相当于一个树(红黑树)的根节点, 如果再有其他的添加进来, 则系统将其会挂载到其节点下面
//op: EPOLL_CTL_ADD -- 注册, EPOLL_CTL_MOD -- 修改, EPOLL_CTL_DEL -- 删除
//fd: 关联的文件描述符
//event: struct epoll_event 结构体类型的指针, 告诉内核要监听什么事件
//其中event结构体分析如下
struct epoll_event {
uint32_t events; //结构体
epoll_data_t data; //联合体
}
/*
events:
- EPOLLIN - 读
- EPOLLOUT - 写
- EPOLLERR - 异常
*/
/*
typedef union epoll_data {
void *ptr;
int fd; //常用
uint32_t u32;
uint64_t u64;
} epoll_data_t;
*/
/*等待IO事件发生 - 可以设置阻塞的函数, 对应select和poll函数
该函数能够告诉我们是哪个文件描述符发生变化, 并放到下面的数组中
因为传进去的时候结构体中包含结构体中含有fd和event*/
int epoll_wait(int epfd,
struct epoll_event* events, //数组
int maxevents,
int timeout);
/*参数:
epfd: 要检测的句柄
events:用于回传待处理事件的数组
maxevents:告诉内核这个events的大小
timeout:为超时时间
-1: 永久阻塞; 0: 立即返回; >0: 阻塞多久*/
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 8192
#define SERV_PORT 8000
#define OPEN_MAX 1000
int main(int argc, char *argv[]) {
// 记录是第几个连接上来的客户端
int num = 0;
// 创建监听的套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// 端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//绑定
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd, 20);
//创建epoll模型, efd指向红黑树根节点
int efd = epoll_create(OPEN_MAX);
if(efd == -1) {
perror("epoll_create error");
exit(1);
}
// tep: epoll_ctl参数
struct epoll_event tep;
//指定lfd的监听时间为"读"
tep.events = EPOLLIN;
tep.data.fd = listenfd;
//将lfd及对应的结构体设置到树上,efd可找到该树
int res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if(res == -1) {
perror("epoll_ctl error");
exit(1);
}
socklen_t clilen;
struct sockaddr_in cliaddr;
// ep[] : epoll_wait参数
struct epoll_event ep[OPEN_MAX];
char buf[MAXLINE], str[INET_ADDRSTRLEN];
while(1) {
/*epoll为server阻塞监听事件, ep为struct epoll_event类型数组, OPEN_MAX为数组容量, -1表永久阻塞*/
int nready = epoll_wait(efd, ep, OPEN_MAX, -1);
if(nready == -1) {
perror("epoll_wait error");
exit(1);
}
for (int i = 0; i < nready; i++) {
//如果不是"读"事件, 继续循环
if (!(ep[i].events & EPOLLIN)) {
continue;
}
//判断满足事件的fd是不是lfd
if (ep[i].data.fd == listenfd) {
clilen = sizeof(cliaddr);
// 接受连接请求
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
printf("cfd %d---client %d\n", connfd, ++num);
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if(res == -1) {
perror("epoll_ctl error");
exit(1);
}
} else { // 不是监听的文件描述符, 通信的fd
int sockfd = ep[i].data.fd;
int n = read(sockfd, buf, MAXLINE);
//读到0,说明客户端关闭链接
if (n == 0) {
//将该文件描述符从红黑树摘除
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if(res == -1) {
perror("epoll_ctl error");
exit(1);
}
//关闭与该客户端的链接
close(sockfd);
printf("client[%d] closed connection\n", sockfd);
} else if (n < 0) { //出错
perror("read n < 0 error: ");
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
} else { // 实际读到了字节数
for (int j = 0; j < n; j++) {
buf[j] = toupper(buf[j]); //转大写,写回给客户端
}
write(STDOUT_FILENO, buf, n); //发送到终端, 也可以打印出来
write(sockfd, buf, n); //发送给客户端
}
}
}
}
close(listenfd);
close(efd);
return 0;
}