文章目录
- select
- fd_set
- struct timeval
- 实例-select服务器
- socket
- 初始化
- 服务器初始化
- 位图初始化
- 开始监听
- 超时测试
- 事件处理
- select 的特点
- poll
- struct pollfd
- 事件 events 和 revents
- 实例-poll服务器
- socket
- 初始化
- 开始监听
- 事件处理
- poll 的特点
- epoll-重点
- epoll_create
- epoll_ctl
- epoll_event
- 事件
- epoll_wait
- epoll工作原理-重点
- epoll服务器
- socket
- 初始化
- 开始监听
- 事件处理
- epoll 特点
- epoll工作方式
- 水平触发 LT-Level Triggered
- 边缘触发 ET-Edge Triggered
- LT和ET
- ET 和非阻塞
select
一个多路转接接口,同时监视多个文件描述符的上的事件是否就绪,核心工作就是等
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- nfds:需要监视的最大文件描述符+1,例如文件描述符有1,7,10,那么nfds设置为10+1=11
- readfds:输入输出型参数,监视文件描述符的读事件是否就绪,返回的读事件已经就绪。
- writefds:输入输出型参数,监视文件描述符的写事件是否就绪,返回的写事件已经就绪
- exceptfds:输入输出型参数,监视文件描述符的异常事件是否就绪,返回的异常事件已经就绪
- timeout:输入输出型参数,设置select的等待时间,返回时表示timeout的剩余时间
- NULL/nullptr:永不超时
- 0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回
- 特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回
返回值
-
成功,则返回就绪的文件描述符个数
-
timeout时间耗尽,则返回0
-
失败,则返回-1,同时错误码会被设置
-
EBADF
:文件描述符为无效的或该文件已关闭 -
EINTR
:此调用被信号所中断 -
EINVAL
:参数nfds为负值 -
ENOMEM
:核心内存不足
-
编写流程
- select之前要进行所有参数重置FD_SET
- 需要定义第三方数组保存合法fd更新fd,方便select批量处理
- select之后要便利所有合法参数进行检测FD_ISSET
fd_set
一个位图结构,最多监控1024歌文件描述符
- 输入:用户告诉操作系统,需要帮我监控那几个文件描述符,在需要监控的文件描述符上置 1
- 输出:系统告诉用户,那些文件描述符的相关事件就绪了
void FD_CLR(int fd, fd_set *set); // 清空位图中fd的位
int FD_ISSET(int fd, fd_set *set);// 判断相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 设置对应位置的fd的位
void FD_ZERO(fd_set *set); // 清空整个位图
struct timeval
用来计算select 等待的时间是秒与微秒的和
struct timeval {long tv_sec; /* 秒 */long tv_usec; /* 微秒 */
};
实例-select服务器
- 每次调用select函数之前都需要对readfds进行重新设置,定义fdsArray数组用于保存监听套接字和已经与客户端建立连接的套接字
- 循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作
- 定义一个读文件描述符集readfds,并将fdsArray当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪
- 就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fdsArray数组当中
- 就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出
socket
先初始化服务器,完成套接字的创建、绑定和监听。
setsockopt函数是让端口可以被复用,适用于服务器快速重启的情况
#include <stdio.h>
#include <vector>
#include <unistd.h>
#include <iostream>
#include <fstream>
#include <set>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include <vector>
#include <queue>
#include <pthread.h>
#include <ctime>
#include<semaphore.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>using namespace std;
class Sock
{
public://创建套接字static int SocketInit(){int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){exit(1);}// 设置端口复用int opt = 1;setsockopt(sock , SOL_SOCKET, SO_REUSEADDR , &opt, sizeof(opt));return sock ;}//绑定static void Bind(int sock, int port){struct sockaddr_in local; memset(&local, 0, sizeof (local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;// 2.2 本地socket信息, 写入sock_对应的内核区域if (bind(sock, (const struct sockaddr *)&local, sizeof(local)) < 0){exit(2);}}//监听static void Listen(int sock, int backlog){if (listen(sock, backlog) < 0){exit(3);}}//获取连接static int Accept(int sock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int serviceSock = accept(sock, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取链接失败return -1;}if(clientport) *clientport = ntohs(peer.sin_port);if(clientip) *clientip = inet_ntoa(peer.sin_addr);return serviceSock;}
};
初始化
服务器初始化
运行并指明端口号
static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5); //位图初始化
}
位图初始化
数组用于保存监听套接字和已经与客户端建立连接的套接字,初始化为-1,第一个下标设置为listensock
//位图初始化int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存需要被监视读事件是否就绪的文件描述符,最多1024int fdsArraySz = sizeof(fdsArray) / sizeof(fdsArray[0]);// 将数组里面的文件描述符都初始化为默认值, 并将第一个下标设置为listensocketfor (int i = 0; i < fdsArraySz; i++){fdsArray[i] = -1;}fdsArray[0] = listensock;// 开始监听
开始监听
- 传入select函数的参数都是输入输出参数,所以每次调用select函数都要对参数重新设置
- 每次调用select函数之前,需要对readfds进行重新设置,并将fdsArray当中的文件描述符依次设置进readfds当中
- 循环开始就调用select函数,检测读事件是否就绪
- 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接
// 开始监听fd_set readfds;while (1){int maxFd = -1;FD_ZERO(&readfds);// 清空位图// 设置超时时间为5秒struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;// 遍历全局数组, 将有效的fd都添加进去, 并更新maxfdfor (int i = 0; i < fdsArraySz; i++){// 1. 过滤不合法的fdif (fdsArray[i] == -1)continue;// 2. 添加所有的合法的fd到readfds中, 方便select统一进行就绪监听FD_SET(fdsArray[i], &readfds);if (maxFd < fdsArray[i]){// 3. 更新出fd最大值maxFd = fdsArray[i]; }}// 调用select开始监听int sret = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;//HandlerEvent(listensock,readfds,fdsArraySz,fdsArray);break;}}
超时测试
运行服务器发现5秒超时跳出阻塞态
[aaa@VM-8-14-centos file]$ ./test 端口
time out ... :
time out ... :
time out ... :
使用 telnet 命令来链接当前服务,select 检测到 listensock 文件描述符就绪,会立刻返回
//第一个窗口
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
//第二个窗口
[aaa@VM-8-14-centos file]$
wait success: 1
wait success: 1
事件处理
HandlerEvent函数,当读事件就绪后就调用该函数进行事件处理
- 遍历fdsArray数组当中的文件描述符,依次判断各个文件描述符对应的事件是否就绪
- 处理已有连接,调用accept函数将底层的连接获取上来。将对应的文件描述符添加到fdsArray数组当中,托管给select函数
- 处理新连接,调用read函数读取客户端发来的数据,如果读取成功则将读到的数据在服务器端进行打印。如果调用read函数读取失败或者客户端关闭了连接,那么select服务器也应该调用close函数关闭对应的连接,但此时光光关闭连接也是不够的,还应该将该连接对应的文件描述符从fdsArray数组当中清除
打印数组中的文件描述符
//打印数组中的文件描述符
static void ShowArray(int *arr, int num)
{cout << "当前合法sock list: ";for (int i = 0; i < num; i++){if (arr[i] == -1)continue;elsecout << arr[i] << " ";}cout << endl;
}
遍历整个链接数组,并判断当前位置是否是有效的文件描述符,无效直接跳过
static void HandlerEvent(int listensock, fd_set &readfds, int fdsArraySz, int *fdsArray)
{for (int i = 0; i < fdsArraySz; i++)//遍历所有参数{if (fdsArray[i] == -1)continue; // 跳过无效的位置//listensock}
}
处理新连接
- 判断是否有在 select 中监听该文件描述符
- 开始进行 accept 获取新的链接
- 不能直接 read/write, 而是应该通过数组交付给 select 帮我们监听事件
//处理新连接if (fdsArray[i] == listensock){// 判断listensocket有没有事件监听if (!FD_ISSET(listensock, &readfds)){cerr << "listensocket not set in readfds" << endl;continue;}// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;// 这里我们不能直接对这个socket进行独写, 因为新链接来了并不代表新数据一并过来了// 所以需要将新的文件描述符利用全局数组, 交付给select// select 帮我们监看socket上的读事件是否就绪int i = 0;for (i = 0; i < fdsArraySz; i++){if (fdsArray[i] == -1)break;}// 达到上限了if (i == fdsArraySz){cerr << "reach the maximum number of connections" << endl;close(sock);}else // 没有达到{fdsArray[i] = sock; // 新的链接, 插入到数组中, 下次遍历就会添加到select监看中ShowArray(fdsArray, fdsArraySz);}}// 处理已有连接
处理已有连接,当读事件就绪的时候,我们通过 read 读取已有的数据
// 处理已有连接else{if (FD_ISSET(fdsArray[i], &readfds)){char buffer[1024];ssize_t s = read(fdsArray[i], buffer, sizeof(buffer)-1); // 不会阻塞if (s > 0)//读取成功{buffer[s] = 0;cout << "client[" << fdsArray[i] << "]# " << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client[" << fdsArray[i] << "] quit, server close " << fdsArray[i] << endl;close(fdsArray[i]);fdsArray[i] = -1; // 去除对该文件描述符的select事件监听ShowArray(fdsArray, fdsArraySz);}else // 异常了{cout << "client[" << fdsArray[i] << "] error, server close " << fdsArray[i] << endl;close(fdsArray[i]);fdsArray[i] = -1; // 去除对该文件描述符的select事件监听ShowArray(fdsArray, fdsArraySz);}}}
测试
用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收
//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
当前合法sock list: 3 4
wait success: 1
listensocket not set in readfds
client[4]# 123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123
select 的特点
- 占用资源少
- 每次循环都得遍历整个数组,效率较低
- select可监控的文件描述符数量太少
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合
-
nfds:表示fds数组的长度
-
timeout:表示poll函数的超时时间,单位是毫秒
-
-1:阻塞等待
-
0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回
-
特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回
-
返回值
-
调用成功,则返回有事件就绪的文件描述符个数
-
timeout时间耗尽,则返回0
-
调用失败,则返回-1
-
EFAULT
:fds数组不包含在调用程序的地址空间中。 -
EINTR
:此调用被信号所中断。 -
EINVAL
:nfds值超过RLIMIT_NOFILE值。 -
ENOMEM
:核心内存不足。
-
struct pollfd
struct pollfd {int fd; /* 文件描述符 fd */short events; /* 用户告诉内核需要监看的事件 events */short revents; /* 内核返回的就绪事件 revents */
};
事件 events 和 revents
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
实例-poll服务器
socket
先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码
初始化
不需要自己维护一个 int 文件描述符数组了,定义一个fds数组,该数组当中的每个位置都是一个struct pollfd结构
static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5);// 位图初始化struct pollfd fdsArray[1024];for (int i = 0; i < 1024; i++){fdsArray[i].fd = -1;fdsArray[i].events = 0;fdsArray[i].revents = 0;}//服务器刚开始运行时只需要监视监听套接字的读事件fdsArray[0].fd = listensock;fdsArray[0].events = POLLIN;// 开始监听}
开始监听
poll服务器就不断调用poll函数监视读事件是否就绪
// 开始监听while (1){// 设置超时时间为5毫秒int timeout = 5;// 调用select开始监听int sret = poll(fdsArray, 1024, timeout);switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;HandlerEvent(listensock, fdsArray);break;}}
事件处理
遍历fds数组,跳过无效的位置,判断处理新连接还是旧连接
static void HandlerEvent(int listensock, struct pollfd *fds)
{for (int i = 0; i < 1024; i++) // 遍历所有参数{if (fds[i].fd == -1)continue; // 跳过无效的位置// 处理新连接}
}
处理新连接,调用accept函数将底层建立好的连接获取上来,并将获取到的套接字添加到fds数组当中
// 处理新连接if (fds[i].fd == listensock && fds[i].revents == POLLIN){// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;int i = 0;for (i = 0; i < 1024; i++){if (fds[i].fd == -1)break;}// 达到上限了if (i == 1024){cerr << "reach the maximum number of connections" << endl;close(sock);}else // 没有达到{fds[i].fd = sock; // 将sock添加到数组中fds[i].events = POLLIN;fds[i].revents = 0;}}// 处理已有连接
处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则poll服务器也直接关闭对应的连接,并将该连接对应的文件描述符从fds数组当中清除
// 处理已有连接else{if (fds[i].revents == POLLIN){char buffer[1024];ssize_t s = read(fds[i].fd, buffer, sizeof(buffer) - 1); // 不会阻塞if (s > 0)// 读取成功{buffer[s] = 0;cout << "client:" << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client quit" << endl;close(fds[i].fd);fds[i].fd = -1;fds[i].events = 0;fds[i].revents = 0;}else // 异常了{cerr << "read error" << endl;close(fds[i].fd);fds[i].fd = -1;fds[i].events = 0;fds[i].revents = 0;}}}
测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收
//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
time out ... :
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
time out ... :
wait success: 1
client:123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123
poll 的特点
- poll 没有最大文件描述符限制
- poll 无需用户额外维护一个单独的文件描述符数组,直接沿用该结构体数组即可
- 需要遍历检测,数据多的时候遍历时间长
epoll-重点
epoll_create
创建 epoll 文件句柄
#include <sys/epoll.h>
int epoll_create(int size);
- size的值必须设置为大于0的值
- 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符
- 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符
epoll_ctl
对 epoll 中需要监看的文件描述符进行设置
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
epfd:epoll_create 的返回值
-
op:表示具体的动作
-
EPOLL_CTL_ADD
:将新的文件描述符添加到 epfd 中 -
EPOLL_CTL_MOD
:修改已有文件描述符的监听事件 -
EPOLL_CTL_DEL
:删除已有文件描述符
-
-
fd:目标文件描述符
-
event:需要监视该文件描述符上的哪些事件
返回值
- 函数调用成功返回0,调用失败返回-1
epoll_event
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; /* 需要监视的事件 */epoll_data_t data; /* 联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符 */
};
事件
events 可以是下面的这些选项
事件 | 说明 |
---|---|
EPOLLIN | 对应的文件描述符可以读 (包括对端 SOCKET 正常关闭); |
EPOLLOUT | 对应的文件描述符可以写; |
EPOLLPRI | 对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); |
EPOLLERR | 对应的文件描述符发生错误; |
EPOLLHUP | 对应的文件描述符被挂断; |
EPOLLET | 将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的; |
EPOLLONESHOT | 只监听一次事件, 当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要手动再次把这个 socket 加入到 EPOLL 队列里; |
epoll_wait
收集在 epoll 监控的事件中,已经就绪的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- epfd:epoll_create 的返回值
- events:是一个输出型参数,将已经就绪的事件拷贝到events数组当中
- maxevents:events数组中的元素个数,不可以超过 epoll_create 的 size;
- timeout:超时时间,也是毫秒(0 非阻塞,-1 阻塞 特定的时间值)
返回值
- 成功返回 IO 事件就绪的文件描述符数量,0 代表超时,负数代表失败
epoll工作原理-重点
调用 epoll_create 时,在底层创建一个epoll模型,内部包含了就绪队列和一个红黑树
- 调用 epoll_ctl 的时候,红黑树存放需要监视的事件
- 添加到红黑树中的事件都会与设备的网卡驱动程序建立回调消息,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
- 调用 epoll_wait的时候,就绪队列存放需要返回给用户的事件,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可,获取就绪事件的时间复杂度是 O(1)
epoll服务器
socket
先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码
初始化
初始化socket和epoll初始化
static void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// 初始化socket, 获取socket fd并绑定端口int listensock = Sock::SocketInit();Sock::Bind(listensock, atoi(argv[1]));Sock::Listen(listensock, 5);// epoll初始化int epfd;epfd = epoll_create(256);if (epfd < 0){std::cerr << "epoll_create error" << std::endl;exit(5);}// 开始监听
}
开始监听
- 在epoll服务器开始死循环调用epoll_wait函数之前,需要先调用epoll_ctl将监听套接字添加到epoll模型当中
- epoll服务器就不断调用epoll_wait函数监视读事件是否就绪,并返回就绪的文件描述符数量
- epoll_wait函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次epoll_wait调用即可
// 开始监听struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listensock;epoll_ctl(epfd, EPOLL_CTL_ADD, listensock, &ev);while (1){struct epoll_event revs[64];int sret = epoll_wait(epfd, revs, 64, -1);//返回就绪fd个数switch (sret){case 0: // 等待超时cout << "time out ... : " << endl;break;case -1: // 等待失败cerr << errno << " : " << strerror(errno) << endl;break;default:// 等待成功,正常的事件处理cout << "wait success: " << sret << endl;HandlerEvent(revs, sret);break;}}
事件处理
遍历所有文件描述符,说明该文件描述符对应的读事件就绪,判断该文件描述符是监听套接字还是与客户端建立的套接字
static void HandlerEvent(struct epoll_event revs[], int sret, int listensock, int epfd)
{for (int i = 0; i < sret; i++) // 遍历所有文件描述符{int fd = revs[i].data.fd; // 就绪的文件描述符// 处理新连接}
}
处理新连接,调用accept函数将底层建立好的连接获取上来,调用epoll_ctl函数将获取到的套接字添加到epoll模型当中
// 处理新连接if (fd == listensock && revs[i].events == EPOLLIN){// 具有了一个新链接cout << "get new connection" << endl;string clientip;uint16_t clientport = 0;int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞if (sock < 0)return; // 出错了, 直接返回// 成功获取新连接cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;//将获取到的套接字添加到epoll模型中,并关心其读事件struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sock;epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);}// 处理已有连接
处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则调用epoll_ctl函数将该连接对应的文件描述符从epoll模型中删除
// 处理已有连接else{if (revs[i].events == EPOLLIN){char buffer[1024];ssize_t s = read(fd, buffer, sizeof(buffer) - 1); // 不会阻塞if (s > 0) // 读取成功{buffer[s] = 0;cout << "client:" << buffer << endl;}else if (s == 0) // 对端关闭{cout << "client quit" << endl;close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);}else // 异常了{cerr << "read error" << endl;close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);}}}
测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收
//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:48782 | sock: 5
wait success: 1
client:123//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123
epoll 特点
- 数据拷贝轻量:调用epoll_ctl处理文件描述符和事件,select和poll每次都需要重新将需要监视的事件从用户拷贝到内核
- 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中
- 没有数量限制:文件描述符没有数量限制
- 线程安全:有mutex 锁
- 接口使用方便,虽然拆分了 3 个函数,但是每个函数的功能非常明确
epoll工作方式
水平触发 LT-Level Triggered
- 只要底层有事件就绪,epoll就会一直通知用户,只有所有的数据都被处理完毕,epoll 才不会继续通知
- epoll默认状态下就是LT工作模式
- select和poll其实就是工作是LT模式下的
- 支持阻塞和非阻塞读写
边缘触发 ET-Edge Triggered
- 只有底层就绪事件数量发生变化的时候,epoll才会通知用户
- 只有当电平由低变高的那一瞬间才会触发,将epoll改为ET工作模式,则需要在添加事件时设置
EPOLLET
选项 - 在ET工作模式下,底层就绪事件发生变化的时候会通知用户,只有一次机会,所以收到事件后必须立即处理
- ET 模式下 epoll_wait 返回的次数更少,所以 ET 的性能远高于 LT
- 只支持非阻塞读写。
LT和ET
- ET 模式下,事件就绪就必须一次性处理完数据
- ET 的代码复杂度会增加
ET 和非阻塞
为什么 ET 必须要将文件描述符设置成非阻塞呢?
答:ET模式只有在变化的时候才会通知用户,如果是阻塞模式,客户端通知服务器取数据,然后服务器只取部分数据,剩下缓冲区中数据没有取出,文件描述符不会返回给客户端,客户端没有收到应答而不会继续发送数据,所以必须要采用循环读取 + 非阻塞的方式来将缓冲区读完