学习视频链接
目录
select 出来的比较早,其缺点是监听散乱的文件描述符效率会低一点。
所以使用 poll,但是 poll 效率没改进多少,所以又改进为 epoll
一、poll 函数
1.1 poll函数原型
1.2 流程
循环里面执行的就是 poll 监听 lfd 和所有的 cfd,如果是 lfd 就执行 accept,如果是 cfd 就执行 read/write
1.3 实现
1、代码讲解
进入死循环前,数组是一个这样的状态
if(client[0].revents & POLLIN) 是用来处理 listenfd
如果有新的连接,就会去在数组里面找空闲的位置 for(i=1;i<OPEN_MAX;i++),对应到图上就是这样的:
设置完成后就跳出循环,或者遍历到 1024 后跳出循环。如果遍历到 1024 会报错,没有便利到 1024 就会在刚刚修改 fd 的位置,再设置 events 等于 POLLIN 就完成了
for 循环是用来处理 cfd
上来先做一个异常的处理,保证代码健壮性
if(Client[].revents & POLLIN) 为真,表示读事件满足了,就判断读的返回值
read 的返回值有以下这些
如果返回值等于 -1,其实应该依次处理这些内容,但是函数中只对 ECONNRESET 这种情况进行了处理,其他的直接打印出错
如果返回值等于 0,就停用对应的文件描述符
如果返回值大于 0,就说明读到了数据,就进行处理
2、代码
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
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);
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
for (i = 1; i < OPEN_MAX; i++) {
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
}
maxi = 0; /* client[]数组有效元素中最大元素下标 */
for ( ; ; ) {
nready = poll(client, maxi+1, -1); /* 阻塞 */
if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
clilen = sizeof(cliaddr);
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));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
break;
}
}
if (i == OPEN_MAX) {
perr_exit("too many clients");
}
client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
if (i > maxi) {
maxi = i; /* 更新client[]中最大元素下标 */
}
if (--nready <= 0) {
continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
}
}
for (i = 1; i <= maxi; i++) { /* 检测client[] */
if ((sockfd = client[i].fd) < 0) {
continue;
}
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) { /* 当收到 RST标志时 */
/* connection reset by client */
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1;
}
else {
perr_exit("read error");
}
}
else if (n == 0) {
/* connection closed by client */
printf("client[%d] closed connection\n", i);
Close(sockfd);
client[i].fd = -1;
}
else {
for (j = 0; j < n; j++) {
buf[j] = toupper(buf[j]);
}
Writen(sockfd, buf, n);
}
if (--nready <= 0) {
break; /* no more readable descriptors */
}
}
}
}
return 0;
}
1.4 优点和缺点
二、epell 函数
2.1 简介
epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了
目前 epoll 是 linux 大规模并发网络程序中的热门首选模型。
epoll 除了提供 select/poll 那种 IO 事件的电平触发 (Level Triggered) 外,还提供了边沿触发 (Edge Triggered),这就使得用户空间程序有可能缓存 lO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。
2.2 文件描述符上限
1、查看文件描述符上限方法
cat /proc/sys/fs/file-max (当前计算机所能打开的最大文件个数,受硬件影响)
ulimit -a (当前用户下的进程,默认可以打开文件描述符个数)
2、如有需要,可以通过修改配置文件的方式修改,当前用户下的进程默认可以打开文件描述符个数
sudo vi /etc/security/limits.conf
在文件尾部写入以下配置,soft 软限制(没有设置软限制的话是 1024),hard 硬限制(和前面用 cat 查询的不一样,这里只是用来设定 soft 软限制的最高值)。如下图所示。
* soft nofile 65536
* hard nofile 100000
现在可以用命令修改 soft,但是在这里修改不能超过我们设定的 hard(hard 和我们的硬件并没有关系)
同时要注意,我们再调节从 17000 调节到 15000 没有问题(往下调节);如果是往上调节就会出现问题(17000 到 18000),必须要注销用户后才能调节
2.3 API
前面只要一个函数就可以了,epoll 需要三个函数
第一个函数是 poll_create
第二个函数是 epoll_ctl
第三个函数是 epoll_wait
数组中存储就是连接上的内容,每次只要轮询传出数组,然后处理相应请求就可以了
2.4 代码
首先代码创建一个根结点,再创建 lfd 结点,用于监听,把 lfd 插入到根结点的子节点上。根据epoll_wait 返回的数字,循环遍历数组,处理读事件。判断读事件是否是请求联立连接,如果是请求连接的,就使用 epoll_ctl 插入结点,其他的就是数据读写事件。具体代码如下
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 8192
#define SERV_PORT 8000
#define OPEN_MAX 5000
void perr_exit(const char* str)
{
perror(str);
exit(1);
}
int main(void)
{
int i, listenfd, connfd, sockfd, n;
int num = 0;
ssize_t nready, efd, res;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];
listenfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口复用
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);
efd = epoll_create(OPEN_MAX); // 创建epoll模型,efd指向红黑书根节点
if (efd == -1) {
perr_exit("epoll_create error");
}
// 指定lfd的监听事件为 读
tep.events = EPOLLIN;
tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); // 将lfd及对应的结构体设置到树上,efd可找到该树
if(res == -1) {
perr_exit("epoll_ctl error");
}
for ( ; ; ) {
// epoll为server阻塞监听事件,ep为struct epoll_event类型数组,OPEN_MAX为数组容量,-1表永久阻塞
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
if (nready == -1) {
perr_exit("epoll_wait error");
}
for (i = 0; i < nready; i++) {
if (!(ep[i].events & EPOLLIN)) { // 如果并不是 读 事件,就继续循环
continue;
}
if (ep[i].data.fd == listenfd) { // 判断满足事件的fd是不是lfd
clilen = sizeof(cliaddr);
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("cdf %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) {
perr_exit("epoll_ctl error");
}
}
else { // 不是lfd
sockfd = ep[i].data.fd;
n = read(sockfd, buf, MAXLINE);
if (n == 0) { // 读到0,说明客户端关闭连接
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); // 将该文件描述符从红黑树摘除
if (res == -1) {
perr_exit("epoll_stl error");
}
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 (i = 0; i < n; i++) { // 实际读到的字节数
buf[i] = toupper(buf[i]); // 转大写,写会给客户端
}
write(STDOUT_FILENO, buf, n);
write(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
三、epoll 事件模型
3.1 分类
ET模式:边沿触发
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回
event.events = EPOLLIN | EPOLLET;
LT模式:水平触发 —— 默认采用模式
缓冲区剩余未读尽的数据会导致 epoll_wait 返回
event.events = EPOLLIN
3.2 查看他们之间的区别
1、ET模式每 5 秒写一次,缓冲区中还有数据,但是不会去读数据,只有等到下次有10个字符数据来的时候才会继续读缓冲区后面 5 个字符的数据
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 10
int main(void)
{
int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';
pipe(pfd);
pid = fork();
if(pid == 0) { // 子进程 写
close(pfd[0]);
while (1) {
for (i = 0; i < MAXLINE / 2; i++) {
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
for ( ; i < MAXLINE; i++) {
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
write(pfd[1], buf, sizeof(buf));
sleep(5);
}
close(pfd[1]);
}
else if (pid > 0) { // 父进程 读
struct epoll_event event;
struct epoll_event resevent[10]; // epoll_wait就绪返回event
int res, len;
close(pfd[1]);
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; // ET边沿触发
//event.events = EPOLLIN; // LT水平触发(默认)
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == pfd[0]) {
len = read(pfd[0], buf, MAXLINE / 2);
write(STDOUT_FILENO, buf, len);
}
}
close(pfd[0]);
}
return 0;
}
2、LT模式只要缓冲区中还有数据,就回去读数据,每次读 5 个字节的数据,一次性连续读两次。
//event.events = EPOLLIN | EPOLLET; // ET边沿触发
event.events = EPOLLIN; // LT水平触发(默认)
3.3 网络中的两种模式
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#define MAXLINE 10
#define SERV_PORT 9000
int main(void)
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (1) {
for (i = 0; i < MAXLINE / 2; i++) {
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
for ( ; i < MAXLINE; i++) {
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
write(sockfd, buf, sizeof(buf));
sleep(5);
}
return 0;
}
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 10
#define SERV_PORT 9000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int efd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
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);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; // ET边沿触发
//event.events = EPOLLIN; // 默认LT水平触发
printf("Aceepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == connfd) {
len = read(connfd, buf, MAXLINE / 2);
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
如下图,在 ET 模式中,也是每次客户端发送过来信息,服务端才执行一次写操作
改成 LT 模式后,每次发送消息后,服务端先打印出 5 个字符,然后服务端再次触发 epoll_wait 返回,又会再次打印剩下的 5 个字符
// event.events = EPOLLIN | EPOLLET; // ET边沿触发
event.events = EPOLLIN; // 默认LT水平触发
3.4 ET+非阻塞模式
ET 模式的应用场景就是我们只读文件头,后面的内容都丢弃了的情况。
ET 对于 epoll_wait 返回的读事件,需要使用 while 循环把所有字节都读取完毕,也就是读到缓冲区为空为止,这样最后一次读取一定会导致阻塞,因此需要把 ET 的读取设置成非阻塞的。
现在给文件描述符加上非阻塞,用来防止没读完数据后,卡在第 64 行
3.5 总结
epoll 的 ET 模式,高效模式,但是只支持非阻塞模式
非阻塞模式
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_ NONBLOCK;
fcntl(cfd, F_SETFL, flg);
epoll 优点
高效。突破1024文件描述符。
epoll 缺点
不能跨平台。Linux.