Linux socket通信-- poll和epoll
Linux socket通信-- poll和epoll
1 poll 函数
1.1 poll函数用法
poll函数用于检测一组文件描述符(File Descroptor, 简称 fd)上的可读可写和出错事件,其函数签名如下:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数解析如下:
-
fds:指向一个结构体数组首个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定检测某个指定的fd的条件。
struct pollfd结构体如下:struct pollfd { int fd; // 待检测事件的fd short events; // 关心的事件组合 short revents; // 检测后得到的事件类型 }
struct pollfd
的events
字段是由开发者设置的,告诉内核我们关注什么事件,而revents
字段是poll函数返回时内核设置的,说明该fd发生了什么事件。取值如下:事件宏 事件描述 是否可以作为输入(events) 是否可以作为输出(revents) POLLIN 数据可读(普通数据&优先数据) 是 是 POLLOUT 数据可写(普通数据&优先数据) 是 是 POLLRDNORM 等同于POLLIN 是 是 POLLRDBAND 优先级带数据可读(一般用于Linux) 是 是 POLLPRI 高优先级数据可读,列如TCP带外数据 是 是 POLLWRNORM 等同于POLLOUT 是 是 POLLWRBAND 优先级带数据可写 是 是 POLLRDHUP TCP连接被对端关闭,或者关闭了写操作由GUN引入 是 是 POPPHUP 挂起 否 是 POLLERR 错误 否 是 POLLNVAR 文件描述符没有打开 否 是 -
nfds:参数fds结构体数组的长度。nfds_t 在本质上是unsigned long int,其定义如下:
typedef unsigned long int nfds_t;
-
timeout:表示poll的函数的超时时间,单位为毫秒。
1.2 poll和select比较
- poll不要求开发者计算最大文件描述符加1的大小;
- 与select相比,poll在处理大数量的文件描述符时速度更快;
- poll没有最大连接数量的限制,因为其存储fd的数组没有长度限制;
- 在调用poll函数时,只需对参数进行一次设置就好了。
1.3 poll实例
poll server实例
/**
* @file poll_server.cpp
* @author your name (you@domain.com)
* @brief 演示poll 函数的用法,poll_server.cpp
* @version 0.1
* @date 2021-12-16
*
* @copyright Copyright (c) 2021
*
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>
#define INVALID_FD -1
int main()
{
// 创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if ( listenfd == INVALID_FD ) {
std::cout << "Socket create error: " << strerror(errno) << std::endl;
return -1;
}
// 将监听socket设置为非阻塞的
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) {
close(listenfd);
std::cout << "set listenfd to nonblock error:" << strerror(errno) << std::endl;
return -1;
}
// 复用地址和端口号
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &on, sizeof(on));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &on, sizeof(on));
// 初始化服务器的地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if(bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) {
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
// 启动监听
if (listen(listenfd, SOMAXCONN) == -1) {
std::cout << "listen socket error." << std::endl;
close(listenfd);
return -1;
}
struct sockaddr_in listendAddr;
unsigned int listendAddrLen = sizeof(listendAddr);
//获取监听的地址和端口
if(getsockname(listenfd, (struct sockaddr *)&listendAddr, &listendAddrLen) == -1){
std::cout<< "getsockname error." << std::endl;
return -1;
}
std::cout << "listen address = " << inet_ntoa(listendAddr.sin_addr) << ":" << ntohs(listendAddr.sin_port) << std::endl;
std::vector<pollfd> fds;
pollfd listen_fd_info;
listen_fd_info.fd = listenfd;
listen_fd_info.events = POLLIN;
listen_fd_info.revents = 0;
fds.push_back(listen_fd_info);
// 是否存在无效的fd标志
bool exist_invalid_fd;
int n;
while(true) {
exist_invalid_fd = false;
n = poll(&fds[0], fds.size(), 1000);
if (n < 0) {
// 被信号中断
if (errno == EINTR)
continue;
// 出错,退出
break;
} else if (n == 0) {
// 超时
continue;
}
for(size_t i = 0; i < fds.size(); i++) {
// 事件可读
if (fds[i].revents & POLLIN) {
if (fds[i].fd == listenfd) {
// 监听 socket,接受新连接
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// 接受客户端连接并将产生的clientfd加入fds的集合中
int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen);
if (clientfd != -1) {
// 将客户端socket 设置为非阻塞的
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {
close(clientfd);
std::cout << "set clientfd to nonblock error." << std::endl;
} else {
struct pollfd client_fd_info;
client_fd_info.fd = clientfd;
client_fd_info.events = POLLIN;
client_fd_info.revents = 0;
fds.push_back(client_fd_info);
std::cout << "new client accepted, clientfd:" << clientfd << std::endl;
}
}
} else {
// 普通clientfd,收取数据
char buf[64] = {0};
int m = recv(fds[i].fd, buf, 64, 0);
if (m <= 0) {
if (errno != EINTR && errno != EWOULDBLOCK) {
// 出错或对端关闭了连接,关闭对应的clientfd,并设置了无效标志位
for(std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); iter++) {
if (iter->fd == fds[i].fd) {
std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl;
close(fds[i].fd);
iter->fd = INVALID_FD;
exist_invalid_fd = true;
break;
}
}
}
} else {
std::cout << "recv from client:" << buf << " , clientfd:" << fds[i].fd << std::endl;
}
}
} else if (fds[i].revents & POLLERR) {
// TODO :暂且不处理
std::cout << "error: << " << fds[i].revents << std::endl;
}
}
if (exist_invalid_fd) {
// 统一清理无效的fd
for (auto iter = fds.begin(); iter != fds.end(); iter++) {
if (iter->fd == INVALID_FD) {
iter = fds.erase(iter);
} else {
iter++;
}
}
}
}
// 关闭所有socket
for (auto iter = fds.begin(); iter != fds.end();iter++) {
close(iter->fd);
}
return 0;
}
2 epoll 函数
2.1 epoll 接口
头文件:
#include <sys/epoll.h>
2.1.1 epoll_create
获取epoll 的fd:
int epoll_create(int size);
创建一个epoll句柄,size用来告诉内核这个监听的数目多大;注意:
当创建好epoll句柄后,返回fd值,可在linux下通过/proc/进程/fd/
,可以看到这个fd。
- 参数说明:
- size:监听数目多少
- 函数返回:
- epollfd:epoll_create 返回fd;
2.1.2 epoll_ctl
epoll_ctl epoll的事件注册函数,它不同与select() 是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数。
int epoll_ctl(int epollfd, int op, int fd, struct epoll_event *event);
- 参数说明:
- epollfd: epoll_create 返回值
- op: 表示监听动作
+ EPOLL_CTL_ADD:注册新的fd到epfd中;
+ EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
+ EPOLL_CTL_DEL:从epfd中删除一个fd; - fd:需要监听的fd
- 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 events */ epoll_data_t data; /* User data variable */ };
+ EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
+ EPOLLOUT:表示对应的文件描述符可以写;
+ EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
+ EPOLLERR:表示对应的文件描述符发生错误;
+ EPOLLHUP:表示对应的文件描述符被挂断;
+ EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
+ EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 - 参数返回:
- 返回运行结果,0未成功,非0 失败
2.1.3 epoll_wait
等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符
int epoll_wait(int epollfd, struct epoll_event * events, int maxevents, int timeout)
-
参数说明:
- epollfd: epoll的描述符。
- events:events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)
- maxevents:本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的
- timeout:timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
-
函数返回:
- epoll_wait若调用成功,则会返回有事件的fd数量;若返回0,则表示超时;若调用失败,则返回-1。
-
使用示例
while (true) { epoll_event epoll_events[1024]; int n = epoll_wait(epollfd, epoll_events, 1024, 10000); if (n < 0) { // 被信号中断 if (errno == EINTR) { continue; } // 出错,退出 break; } else if (n == 0) { //超时,继续 } for(size_t i = 0; i < n; i++) { if (epoll_events[i].events & EPOLLIN) { // 处理可读事件 }else if (epoll_events[i].events & EPOLLOUT) { // 处理可写事件 }else if (epoll_events[i].events & EPOLLERR) { // 处理出错事件 } } }
2.2 epoll_wait 与 poll函数的区别
通过对 poll 与 epoll_wait 函数的介绍可以发现:我们在 epoll_wait 函数调用完成后,可以通过参数 event 拿到所有有事件就绪的 fd(参数 event 仅仅是个输出参数);而 poll函数的事件集合参数(poll函数的第1个参数)在调用前后数量都不会改变,只不过调用前通过pollfd结构体的events字段设置待检测的事件,调用后通过pollfd结构体的revents字段检测就绪的事件(参数fds既是入参也是出参)。
2.3 LT 模式和 ET模式
与poll模式的事件宏相比,epoll模式新增了一个事件宏EPOLLET,即边缘触发模式(Edge Trigger,ET),我们称默认的模式为水平触发模式(Level Trigger,LT)。这两种模式的区别在于:
- LT:对于水平触发模式,一个事件只要有,就会一直触发;
- ET:对于边缘触发模式,在一个事件从无到有时才会触发。
理解:fd 上有数据的状态认为是高电平状态,将没有数据的状态认为是低电平状态,将fd可写状态认为是高电平状态,将fd不可写状态认为是低电平状态。
- LT:水平模式的触发条件:①低电平→高电平;②处于高电平状态。
- ET:边缘模式的触发条件:低电平→高电平。
2.4 epoll 实例
2.4.1 server 实例
server epoll实例
/**
* @file testepoll.cpp
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2022-03-23
*
* @copyright Copyright (c) 2022
*
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>
int main()
{
// 创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
std::cout << "create listen socket error" << std::endl;
return -1;
}
// 设置重用IP地址和端口号
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
// 将监听socket 设置为非阻塞的
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) {
close(listenfd);
std::cout << "set listenfd to nonblock error" << std::endl;
return -1;
}
// 初始化服务器的地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) {
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
// 启动监听
if (listen(listenfd, SOMAXCONN) == -1) {
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
// 创建 epollfd
int epollfd = epoll_create(1);
if (epollfd == -1) {
std::cout << "create epollfd error." << std::endl;
close(listenfd);
return -1;
}
epoll_event listen_fd_event;
listen_fd_event.data.fd = listenfd;
listen_fd_event.events = EPOLLIN;
// 若取消注释掉这一行,则使用ET模式
// listen_fd_event.events |= EPOLLET;
// 将监听socket绑定到epollfd上
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) {
std::cout << "epoll_ctl error" << std::endl;
close(listenfd);
return -1;
}
int n;
while(true) {
epoll_event epoll_events[1024];
n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0) {
// 被信号中断
if(errno == EINTR) {
continue;
}
// 出错,退出
break;
} else if (n == 0) {
// 超时, 继续
continue;
}
for(size_t i = 0; i < n; i++) {
// 事件可读
if (epoll_events[i].events & EPOLLIN) {
if (epoll_events[i].data.fd == listenfd) {
// 监听socket,接受新连接
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
if (clientfd != -1) {
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {
std::cout << "set clientfd to nonblocking error." << std::endl;
close(clientfd);
} else {
epoll_event client_fd_event;
client_fd_event.data.fd = clientfd;
client_fd_event.events = EPOLLIN;
// 若取消注释掉这一行,则使用ET模式
// listen_fd_event.events |= EPOLLET;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) {
std::cout << "new client accepted, clientfd:" << clientfd << std::endl;
} else {
std::cout << "add client fd to epollfd error." << std::endl;
close(clientfd);
}
}
}
} else {
std::cout << "client fd:" << epoll_events[i].data.fd << " recv data." << std::endl;
// 普通clientfd
char ch;
// 每次只接收一个字符
int m = recv(epoll_events[i].data.fd, &ch, 1, 0);
if (m == 0) {
// 对端关闭了连接,从epollfd上移除clientfd
if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) {
std::cout << "client disconnected, clientfd:" << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
} else if (m < 0) {
// 出错
if( errno != EWOULDBLOCK && errno != EINTR) {
if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) {
std::cout << "client disconnected, clientfd:" << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
}
} else {
// 正常收到数据
std::cout << "recv from client:" << epoll_events[i].data.fd << " , " << ch <<std::endl;
}
}
} else if (epoll_events[i].events & EPOLLERR) {
// TODO: 暂不处理
}
}
}
close(listenfd);
return 0;
}
3 socket对端关闭
socket对端关闭判断条件:
- recv 收到对端的数据为0
- recv 收到对端的数据小于0:判断 errno != EWOULDBLOCK && erno != EINTR 为true
注意如果发送0数据,实际对端是收不到任何数据,即不触发recv
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!