C语言socket编程-epoll
前言
epoll是Linux内核的可扩展I/O事件通知机制。于Linux 2.5.44首度登场,它设计目的旨在取代既有POSIX select与poll系统函数,让需要大量操作文件描述符的程序得以发挥更优异的性能。
一、简介
相比select/poll的主动查询,epoll模型采用基于事件的通知方式,事先为建立连接的句柄注册事件,一旦该句柄就绪,内核会采用回调机制将句柄加入到epoll的指定的句柄集合中,之后进程再根据该集合中句柄的数量,对客户端请求逐一进行处理。
虽然epoll机制中返回的同样是就绪句柄的数量,但epoll中的集合只存储了就绪的句柄,服务器进程无需再对所有的句柄进行扫描;且epoll机制使用内存映射机制(类似共享内存),不必再将内核中的句柄集合复制到内存空间;此外,epoll机制不受进程可打开最大句柄数量的限制(只与系统内存有关),可连接远超过默认FD_SETSIZE的进程。
二、相关系统调用
1、epoll_create
函数原型:
int epoll_create(int size)
函数功能:用于创建一个epoll句柄
参数说明:
- size:该epoll中可监听的文件描述符的最大个数
- 返回值:返回一个用于引用epoll的句柄,调用失败后返回-1
2、epoll_ctl
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能:注册监听事件,将fd注册到epfd中
参数说明:
- epfd:epoll实例的句柄
- op:操作类型,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL
- fd:需要注册的句柄
- event:指向epoll_event结构体的指针,用于描述事件类型和数据
- 返回值:成功返回0,失败返回-1
epoll_event 是 Linux 内核中 epoll 事件处理机制的核心数据结构,声明如下:
struct epoll_event {
uint32_t events; // 表示事件类型
epoll_data_t data; // 用户数据,可以是一个指针或一个文件描述符
};
events表示的事件类型
EPOLLIN
:表示对应的文件描述符可以读取(包括对端已经关闭连接)。EPOLLOUT
:表示对应的文件描述符可以写入。EPOLLERR
:表示对应的文件描述符发生错误。EPOLLRDHUP
:表示对端已经关闭连接,或者关闭了写端。EPOLLHUP
:表示对应的文件描述符被挂起。
3、epoll_wait
函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能:等待监听的事件发生
参数说明:
- epfd:epoll实例的句柄
- events:存储IO事件信息的数组
- maxevents:数组的最大长度
- timeout:等待IO事件的超时时间
- 返回值:发生IO事件的句柄数量,超时返回-1
三、代码示例
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#define ERR 1
#define OK 0
#define ADDR "127.0.0.1"
#define PORT 23
#define BACKLOG 1024
#define MAX_SIZE 1024
#define CLIENT_NUM 1024
int main()
{
int sockFd, newSockFd, iRet, maxIndex, clientFds[CLIENT_NUM], efd, nReady, flag, j;
unsigned int iLocalAddr;
struct sockaddr_in localAddr, remoteAddr;
socklen_t addrlen;
struct epoll_event tep, eps[CLIENT_NUM];
char buf[MAX_SIZE] = {0};
// 创建socket
sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd < 0) {
printf("create socket failed, errno: %d\n", errno);
return ERR;
}
// 配置地址信息
localAddr.sin_family = AF_INET;
localAddr.sin_port = htons(PORT);
inet_pton(AF_INET, ADDR, &iLocalAddr);
localAddr.sin_addr.s_addr = iLocalAddr;
// 绑定地址和socket
iRet = bind(sockFd, &localAddr, sizeof(localAddr));
if (iRet < 0) {
printf("bind socket failed, errno: %d\n", errno);
return ERR;
}
// 监听socket的链接请求
iRet = listen(sockFd, BACKLOG);
if (iRet < 0) {
printf("listen failed, errno: %d\n", errno);
return ERR;
}
maxIndex = 0;
// 初始化客户端链接
for (int i = 0; i < CLIENT_NUM; i++) {
clientFds[i] = -1;
}
// 创建epoll实例
efd = epoll_create(CLIENT_NUM);
if (efd < 0) {
printf("create epoll fd failed, errno: %d\n", errno);
return ERR;
}
tep.events = EPOLLIN;
tep.data.fd = sockFd;
// 注册socket句柄的监听事件
iRet = epoll_ctl(efd, EPOLL_CTL_ADD, sockFd, &tep);
if (iRet < 0) {
printf("add default event failed, errno: %d\n", errno);
return;
}
while (1) {
// 等待有事件发生
nReady = epoll_wait(efd, eps, CLIENT_NUM, -1);
if (nReady == -1) {
printf("epoll_wait failed, errno: %d\n", errno);
continue;
}
for (int i = 0; i < nReady; i++) {
if (!(eps[i].events & EPOLLIN)) {
continue;
}
if (eps[i].data.fd == sockFd) {
// socket句柄有事件发生,接收客户端链接,创建链接句柄
newSockFd = accept(sockFd, &remoteAddr, &addrlen);
if (newSockFd < 0) {
printf("accept failed, errno: %d\n", errno);
continue;
}
flag = -1;
// 将新的连接句柄添加到集合中
for (j = 0; j < CLIENT_NUM; j++) {
if (clientFds[i] == -1) {
clientFds[i] = newSockFd;
flag = j;
break;
}
}
if (flag == -1) {
char *fullMsg = "the client pool is full";
write(newSockFd, fullMsg, strlen(fullMsg));
} else {
printf("client[%d] fd[%d] connect succ\n", j, clientFds[j]);
}
if (maxIndex < j) {
maxIndex = j;
}
// 注册链接句柄的监听事件,等待客户端发送消息
tep.events = EPOLLIN;
tep.data.fd = newSockFd;
iRet = epoll_ctl(efd, EPOLL_CTL_ADD, newSockFd, &tep);
if (iRet < 0) {
printf("add event failed, fd: %d, errno: %d\n", newSockFd, errno);
continue;
}
} else {
// 收到客户端的消息
int tmpFd = eps[i].data.fd;
iRet = read(tmpFd, buf, MAX_SIZE);
if (iRet < 0) {
printf("read failed, errno: %d\n", errno);
} else if (iRet == 0) {
for (j = 0; j < MAX_SIZE; j++) {
if (clientFds[j] == tmpFd) {
clientFds[j] = -1;
}
}
printf("client[%d] fd[%d] disconnected\n", j, tmpFd);
close(tmpFd);
} else {
printf("recv msg: %s\n", buf);
write(tmpFd, "this is server msg!", strlen("this is server msg!"));
}
}
}
}
}
总结
epoll是Linux操作系统提供的一种I/O多路复用机制,可以用于高效地管理大量的网络连接。它相比于传统的select和poll机制,具有更高的性能和更好的可扩展性。
epoll的核心是一个epoll文件描述符,通过epoll_ctl函数向其注册文件描述符,并指定需要监听的事件类型。当某个文件描述符上发生指定的事件时,epoll_wait函数会返回该文件描述符的信息,应用程序可以据此进行相应的处理。
需要注意的是,epoll并不是万能的,它适用于大量的连接和少量的数据交换,对于大量数据交换的场景,还需要使用其他的技术来提高性能。此外,epoll在使用时需要注意一些细节,如避免文件描述符泄漏、正确使用EPOLLONESHOT等。
总之,epoll是一个非常重要的网络编程工具,深入理解并正确使用它可以提高应用程序的性能和可靠性。