LV035-多路复用IO-03-epoll
本文主要是网络编程——多路复用IO中的epoll的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、epoll简介
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制,它所支持的文件描述符上限是整个系统可以打开的文件数目。
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,并且每个fd上边都会有一个callback函数(回调函数),只有活跃的socket才会去主动调用这个回调函数,这属于一种通知机制,就是当事件发生的时候,则主动通知;通知机制的反面,就是轮询机制。
我们主要会用到以下三个函数,这里提前说一下,后边介绍原理的话可能会用到:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);epoll通过以上三个函数把原先的select/poll调用分成了3个部分:
- (1)调用
epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源); - (2)调用
epoll_ct()l向epoll对象中添加需要连接的socket套接字; - (3)调用
epoll_wait()收集发生的事件的连接。
这样做的好处?
假设现在有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百或者几千个TCP连接是活跃的,这种情况下如何实现高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大。对于select来说,在FD_SETSIZE 为1024的情况下,则我们至少需要创建1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到100万级别的并发访问,是一个很难完成的任务。poll与select其实是很类似的,它也是难以完成这样的高并发连接。
而我们使用epoll的话,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并不会一次性向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
二、epoll数据结构
关于epoll的源码我是找了一下,但是没有找到,也可能是自己查找的方式不对吧,但是这个嘛暂时不太重要,后边再补充吧。不过我在github上找到了一个项目,包含了linux内核源码:linux/eventpoll.c at master · torvalds/linux (github.com)
先来看一张网上找到的一张关于epoll数据结构的图,不过我修改成自己可以理解的样子啦(这里会提到红黑树,前边学习数据结构的时候好像没有了解这一种树,后边会在数据结构的笔记中补充):

当某一进程调用epoll_create()创建epoll对象的时候,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,这两个成员是rdllist和rbr,这两个成员的类型如下所示:
struct eventpoll
{
...
/* List of ready file descriptors */
struct list_head rdlist;
...
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
...
};rdlist:该成员中存放的是将要通过epoll_wait返回给用户的满足条件的事件,这些事件被存储在一个双向链表中,而rdlist存放的就是这个双向链表的首地址。rbr:该成员中存放的是红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件。
每一个epoll对象都会有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,这样的话,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lg(n),其中n为树的高度)。
所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,其中有两个成员:
struct epitem
{
union
{
/* RB tree node links this structure to the eventpoll RB tree */
struct rb_node rbn;
/* Used to free the struct epitem */
struct rcu_head rcu;
};
/* List header used to link this structure to the eventpoll ready list */
struct list_head rdllink;
...
};rbn:该成员表示挂载到eventpoll的红黑树节点,这些节点存储着我们添加到epoll的所有事件。rdllink:该成员表示挂载到eventpoll.rdllist的节点,这些节点存储着已经准备好的事件。
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
总结下来就是,通过红黑树和双链表数据结构,并结合回调机制,使epoll更加高效。
三、相关API
1. epoll_create()
1.1 函数说明
在linux下可以使用man 2 epoll_create命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/epoll.h>
/* 函数声明 */
int epoll_create(int size);【函数说明】该函数用于创建一个epoll句柄,也就是新创建一个epoll对象。
【函数参数】
size:int类型,在初始epoll_create()实现中,size参数告知内核调用者希望添加到epoll对象的文件描述符的数量。从Linux内核2.6.8版本起,不再需要这个参数了(内核不需要该参数就可以动态地调整所需数据结构的大小),但是size仍然必须大于零,以便在旧内核上运行新的epoll应用程序时确保向后兼容性。
【返回值】int类型,成功将返回一个epoll句柄,这其实就是一个文件描述符(非负整数)。如果发生错误,则返回-1,并设置errno来表示错误类型。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <sys/epoll.h>
/* 至少应该有的语句 */
#define MAX_EVENTS 500
int epollFd = -1;
epollFd = epoll_create(MAX_EVENTS);【注意事项】当我们创建好epoll句柄后,它就会占用一个文件描述符,当我们使用完毕后,需要调用close来关闭这个epoll句柄,否则可能会导致fd不足。
1.2 使用实例
暂无
2. epoll_ctl()
2.1 函数说明
在linux下可以使用man 2 epoll_ctl命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/epoll.h>
/* 函数声明 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);【函数说明】该函数用于控制 epoll 对象,主要涉及 epoll 红黑树上节点的一些操作,比如添加节点,删除节点,修改节点事件。
【函数参数】
epfd:int类型,通过epoll_create()创建的epoll对象句柄。op:int类型,表示动作类型,是对红黑树的操作,添加节点、删除节点、修改节点事件。op 可取的值:
| EPOLL_CTL_ADD | 注册新的 fd 到 epoll 对象中,相当于往红黑树添加一个节点。 |
| EPOLL_CTL_MOD | 修改已注册的 fd 的监听事件,相当于把红黑树上监听的 socket 对应的监听事件做修改。 |
| EPOLL_CTL_DEL | 从 epoll 对象中删除一个 fd,相当于取消监听 socket 的事件。 |
我们在使用man手册的时候,下边会有该参数类型的说明:
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 */
};(1)events:uint32_t类型,代表要监听的 epoll 事件类型,有读事件,写事件等。
| events取值 | 含义 |
| EPOLLIN | 表示对应的文件描述符可读。例如,如果客户端发送消息过来,代表服务器收到了可读事件。 |
| EPOLLOUT | 表示对应的文件描述符可写。如果 fd 对应的发数据内核缓冲区不为满,只要监听了写事件,就会触发可写事件。 |
| EPOLLPRI | 表示对应的文件描述符有紧急数据可读(带外数据)。 |
| EPOLLERR | 表示对应的文件描述符发生错误。 |
| EPOLLHUP | 表示对应的文件描述符被挂起。 |
| EPOLLET | 将对应的文件描述符设置为边缘触发(edge-triggered),这是相对于水平触发(level-triggered)而言的。 |
| EPOLLRDHUP | 监听套接字关闭或半关闭事件,Linux 内核 2.6.17 后可用。 |
【返回值】int类型,成功返回0;失败返回-1,并设置errno来表示错误类型。
【使用格式】一般情况基本使用格式如下:
/* 需要包含的头文件 */
#include <sys/epoll.h>
/* 至少应该有的语句 */
struct epoll_event event; /* 定义epoll事件 */
epfd = epoll_create(1); /* 创建epoll对象 */
event.data.fd = socket_fd; /* 填充事件的 fd */
event.events = EPOLLIN; /* 填充事件类型 */
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);/* 把socket_fd 添加到epoll中 */【注意事项】none
2.2 使用实例
暂无
3. epoll_wait()
3.1 函数说明
在linux下可以使用man 2 epoll_wait命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/epoll.h>
/* 函数声明 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);【函数说明】该函数用于等待事件发生,返回事件集合,也就是获取内核的事件通知。其实就是遍历双向链表,把双向链表里的节点数据拷贝出来,拷贝完毕后就从双向链表移除。
【函数参数】
epfd:int类型,通过epoll_create()创建的epoll对象句柄。events:struct epoll_event类型结构体指针变量,这是一个传出参数,用于获取从内核传出的已经符合用户要求的事件集合,一般这里我们会定义一个struct epoll_event结构体数组,并将数组首地址赋值给该变量。maxevents:int类型,代表可以存放的事件个数,就是每次可以处理的最大事件数,也就是events数组的大小。注意这个值要大于0,并且在以前的时候不能大于epoll_create()中参数size的大小,后来epoll_create()忽略size参数后,好像就没有这个限制了。timeout:int类型,表示超时时间,单位为毫秒(ms)。 timeout 取值说明:
| timeout = 0 | 立即返回 |
| timeout = -1 | 阻塞等待 |
| timeout > 0 | 超时后返回 |
- 返回
-1:表示发生错误,并会设置errno表示错误类型。 - 返回
0:超时时间timeout到了,但是监听的事件中依然没有符合我们要求的事件发生。 - 返回正整数:表示符合我们要求的事件发生了,返回值代表这些事件的个数。
【使用格式】一般情况基本使用格式如下:
/* 需要包含的头文件 */
#include <sys/epoll.h>
/* 至少应该有的语句 */
epret = epoll_wait(epfd, events, 20, -1); /* 开始监听事件 */
if(epret < 0)
{
perror("epoll_wait error");
continue;
}
for(i = 0; i < epret; i++) /* 查询事件并处理 */
{
/* 如果事件的fd是监听的accept等待连接的socket fd 说明有客户端连接 */
if(events[i].data.fd == socket_fd)
{
...
}
else /* 其他情况的话就是数据的处理 */
{
...
}
}【注意事项】none
3.2 使用实例
暂无
4. epoll_pwait()
4.1 函数说明
在linux下可以使用man 2 epoll_pwait命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/epoll.h>
/* 函数声明 */
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);【函数说明】该函数与epoll_wait()一样,只是多了一个信号掩码的参数,是一个防止信号干扰的增强型 epoll_wait()函数。
【函数参数】
epfd:int类型,通过epoll_create()创建的epoll对象句柄。events:struct epoll_event类型结构体指针变量,这是一个传出参数,用于获取从内核传出的已经符合用户要求的事件集合。maxevents:int类型,代表可以存放的事件个数,就是每次可以处理的最大事件数,也就是events数组的大小。注意这个值要大于0,并且不能大于epoll_create()中参数size的大小。timeout:int类型,表示超时时间,单位为毫秒(ms)。
| timeout = 0 | 立即返回 |
| timeout = -1 | 阻塞等待 |
| timeout > 0 | 超时后返回 |
【返回值】int类型,可能会有。以下三种情况:
- 返回
-1:表示发生错误,并会设置errno表示错误类型。 - 返回
0:超时时间timeout到了,但是监听的事件中依然没有符合我们要求的事件发生。 - 返回正整数:表示符合我们要求的事件发生了,返回值代表这些事件的个数。
【使用格式】none
【注意事项】none
4.2 使用实例
暂无
四、工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger),其中LT模式是默认模式。
LT模式
LT(Level Triggered),水平触发模式。该模式为epoll的默认工作模式,它同时支持阻塞和非阻塞的socket。在该种模式下,当epoll_wait检测到描述符事件发生并将此事件通知进程,进程可以不立即处理该事件,当下次调用epoll_wait时,会再次响应进程并通知此事件,直到该事件被进程处理。在后边的例子中,当数据不被处理的话,可能会导致epoll_wait()函数不再阻塞,从而一直通知进程有新数据到达。
ET模式
ET(Edge Triggered),边缘触发模式。该模式是一种高速处理模式,当且仅当状态发生变化时进程才会获得通知。当epoll_wait检测到描述符事件发生并将此事件通知进程,进程必须立即处理该事件,如果不处理,或者只是处理了一部分的数据,并没有全部处理,那么下次调用epoll_wait时,不会再次响应进程并通知此事件。需要注意的是,每个使用ET模式的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。
ET模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。但是需要注意 epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
五、使用实例
下边的程序中,我只在server中使用了epoll,客户端的程序使用的还是select。
1. 基本使用实例
1.1 server服务器端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——epoll(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/
#include <sys/epoll.h> /* epoll_create epoll_ctl epoll_wait */
void usage(char *str); /* 提示信息打印函数 */
int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 1.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.将套接字与指定端口号和IP进行绑定 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 2.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*3.调用listen()把主动套接字变成被动套接字 */
if (listen(socket_fd, 5) < 0)
{
perror("listen");
exit(-1);
}
printf ("Server starting....OK!\n");
/*4.阻塞等待客户端连接请求相关变量定义 */
int client_fd = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
/* 5.打印成功连接的客户端的信息相关变量定义 */
char ipv4_addr[16];
/* 6.数据读写相关变量定义 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
/* 7.epoll实现多路复用相关变量定义 */
int epfd, epret, i;
struct epoll_event event; /* 定义epoll事件 */
struct epoll_event events[20]; /* 定义epoll事件集合 */
epfd = epoll_create(1); /* 创建epoll对象 */
event.data.fd = socket_fd; /* 填充事件的 fd */
event.events = EPOLLIN; /* 填充事件类型 */
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);/* 把socket_fd 添加到epoll中 */
/* 8.数据处理 */
while(1)
{
/* 开始监听事件 */
epret = epoll_wait(epfd, events, 20, -1);
if(epret < 0)
{
perror("epoll_wait error");
continue;
}
/* 查询事件并处理 */
for(i = 0; i < epret; i++)
{
/* 如果事件的fd是监听的accept等待连接的socket fd 说明有客户端连接 */
if(events[i].data.fd == socket_fd)
{
if ((client_fd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
{
perror("accept");
exit(-1);
}
event.data.fd = client_fd; /* 填充事件的 fd */
event.events = EPOLLIN | EPOLLET; /* 填充事件类型 */
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);/* 把socket_fd 添加到epoll中 */
if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf ("Clinet(%s:%d) is connected successfully![client_fd=%d]\n", ipv4_addr, ntohs(cin.sin_port), client_fd);
}
else /* 其他情况的话就是数据的处理 */
{
bzero(buf, BUFSIZ);
bzero(replay, BUFSIZ);
do{
ret = read(events[i].data.fd, buf, BUFSIZ - 1);
}while(ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
exit(-1);
}
if(!ret) /* 对方已经关闭,客户端断开 */
{
close(events[i].data.fd);
printf("closed client: %d \n", events[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &event);/* 把 events[i].data.fd 从 epoll 中移除 */
}
else
{
printf("Receive data[client_fd=%d]: %s", events[i].data.fd, buf);
strcat(replay, buf);
ret = send(events[i].data.fd, replay, strlen(replay), 0);
if(ret < 0)
{
perror("send");
continue;
}
}
}
}
}
/* 9.关闭文件描述符 */
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}1.2 client客户端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——epoll(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */
#include <sys/types.h> /* socket connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/
#include <string.h> /* bzero strncasecmp strlen */
#include <sys/epoll.h> /* epoll_create epoll_ctl epoll_wait */
void usage(char *str); /* 提示信息打印函数 */
/* 主函数 */
int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
int portClient = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
portClient = atoi(argv[3]);
if (port < 5000 || portClient < 5000 || (port == portClient))
{
usage(argv[0]);
exit(-1);
}
/* 1.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.连接服务器 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 绑定固定的IP 和 端口号 */
struct sockaddr_in sinClient;
bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
sinClient.sin_family = AF_INET; /* 协议族 */
sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
{
perror("bind");
exit(-1);
}
/* 2.2连接 */
if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(-1);
}
printf("Client staring...OK!\n");
/* 3.数据读写相关变量定义 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
/* 4.select实现多路复用相关变量定义 */
fd_set rset; /* 用于检测读状态的文件描述符集 */
int maxfd = -1;/* 最大文件描述符编号 */
struct timeval tout;/* 超时时间结构体变量 */
tout.tv_sec = 5; /* 设置超时时间秒数 */
tout.tv_usec = 0; /* 设置超时时间微秒数 */
/* 5.开始数据传输 */
while (1)
{
FD_ZERO(&rset); /* 清空文件描述符集 */
FD_SET(0, &rset); /* 将文件描述符0(标准输入)添加到文件描述符集 */
FD_SET(socket_fd, &rset); /* 将用于监听的socket套接字添加到文件描述符集 */
maxfd = socket_fd; /* 重置最大文件描述符集大小 */
/* 5.1开始检测文件描述符 */
ret = select(maxfd + 1, &rset, NULL, NULL, &tout);
if(ret < 0)
{
perror("select");
continue;
}
/* 5.2标准键盘文件描述符就绪时的处理 */
if(FD_ISSET(0, &rset))
{
bzero(buf, BUFSIZ);/* 清空buf */
do{
ret = read(0, buf, BUFSIZ - 1);/* 从标准输入获取数据 */
}while(ret < 0 && EINTR == errno);
if (ret < 0)/* 获取数据失败 */
{
perror ("read() from stdin");
continue;
}
/* 获取数据成功,但是标准输入中没有数据,不需要写入,继续循环即可 */
if (!ret) continue;
/* 向服务器发送数据 */
if(write(socket_fd, buf, strlen(buf)) < 0)/* 将从标准输入获取的数据写入到socket 网络套接字中 */
{
perror ("write to socket error");
continue;
}
/* 判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 5.3socket文件描述符就绪时的处理(服务器有数据发送过来) */
if(FD_ISSET(socket_fd, &rset))
{
bzero (replay, BUFSIZ);
do{
ret = recv(socket_fd, replay, BUFSIZ, 0);
}while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("recv");
continue;
}
/* 若服务器已关闭,则直接退出客户端 */
if (ret == 0) break;
printf("server replay:%s", replay);
if (!strncasecmp(buf, "quit", strlen("quit")))
{
printf ("Sender Client is exiting... ...!\n");
break;
}
}
}
/* 6.关闭文件描述符 */
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}1.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc
DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)
# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)
%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)
clean_o :
@rm -vf *.o
clean_out :
@rm -vf *.out1.4 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
make然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:
gcc -g -O2 -Wall client.c -o client
gcc -g -O2 -Wall server.c -o server对于服务器端,我们执行以下命令启动服务器进程:
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口对于客户端,我们执行以下命令启动客户端进程:
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003然后我们就会看到如下现象:

可以发现我们发送的数据,在客户端都被打印了出来,说明客户端与服务器端可以进行正常通信。
2. 工作模式测试
下边的例子依然只是修改了服务器的代码,我们在启动服务器的时候选择epoll工作模式,并删除服务器端读取数据部分的代码。
2.1 server服务器端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——epoll(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/
#include <sys/epoll.h> /* epoll_create epoll_ctl epoll_wait */
void usage(char *str); /* 提示信息打印函数 */
int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 1.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.将套接字与指定端口号和IP进行绑定 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 2.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*3.调用listen()把主动套接字变成被动套接字 */
if (listen(socket_fd, 5) < 0)
{
perror("listen");
exit(-1);
}
int mode = 0;
printf("please select epoll mode(1:LT 2:ET):");
scanf("%d", &mode);
printf ("Server starting...OK, epoll mode is %d(1:LT 2:ET)!\n", mode);
/*4.阻塞等待客户端连接请求相关变量定义 */
int client_fd = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
/* 5.打印成功连接的客户端的信息相关变量定义 */
char ipv4_addr[16];
/* 6.数据读写相关变量定义 */
int count = 0;
/* 7.epoll实现多路复用相关变量定义 */
int epfd, epret, i;
struct epoll_event event; /* 定义epoll事件 */
struct epoll_event events[20]; /* 定义epoll事件集合 */
epfd = epoll_create(1); /* 创建epoll对象 */
event.data.fd = socket_fd; /* 填充事件的 fd */
event.events = EPOLLIN; /* 填充事件类型 */
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);/* 把socket_fd 添加到epoll中 */
/* 8.数据处理 */
while(1)
{
/* 开始监听事件 */
epret = epoll_wait(epfd, events, 20, -1);
if(epret < 0)
{
perror("epoll_wait error");
continue;
}
/* 查询事件并处理 */
for(i = 0; i < epret; i++)
{
/* 如果事件的fd是监听的accept等待连接的socket fd 说明有客户端连接 */
if(events[i].data.fd == socket_fd)
{
if ((client_fd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
{
perror("accept");
exit(-1);
}
event.data.fd = client_fd; /* 填充事件的 fd */
if(mode == 2)
event.events = EPOLLIN | EPOLLET; /* ET 模式*/
else
event.events = EPOLLIN; /* LT 模式 */
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);/* 把socket_fd 添加到epoll中 */
if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf ("Clinet(%s:%d) is connected successfully![client_fd=%d]\n", ipv4_addr, ntohs(cin.sin_port), client_fd);
}
else /* 其他情况的话就是数据的处理,为测试两种模式,这里并不读取数据 */
{
count++;
printf("new data arrive!count = %d\n",count);
if(count > 10)
exit(0);
}
}
}
/* 9.关闭文件描述符 */
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}2.2 client客户端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——epoll(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */
#include <sys/types.h> /* socket connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/
#include <string.h> /* bzero strncasecmp strlen */
#include <sys/epoll.h> /* epoll_create epoll_ctl epoll_wait */
void usage(char *str); /* 提示信息打印函数 */
/* 主函数 */
int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
int portClient = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
portClient = atoi(argv[3]);
if (port < 5000 || portClient < 5000 || (port == portClient))
{
usage(argv[0]);
exit(-1);
}
/* 1.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.连接服务器 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 绑定固定的IP 和 端口号 */
struct sockaddr_in sinClient;
bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
sinClient.sin_family = AF_INET; /* 协议族 */
sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
{
perror("bind");
exit(-1);
}
/* 2.2连接 */
if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(-1);
}
printf("Client staring...OK!\n");
/* 3.数据读写相关变量定义 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
/* 4.select实现多路复用相关变量定义 */
fd_set rset; /* 用于检测读状态的文件描述符集 */
int maxfd = -1;/* 最大文件描述符编号 */
struct timeval tout;/* 超时时间结构体变量 */
tout.tv_sec = 5; /* 设置超时时间秒数 */
tout.tv_usec = 0; /* 设置超时时间微秒数 */
/* 5.开始数据传输 */
while (1)
{
FD_ZERO(&rset); /* 清空文件描述符集 */
FD_SET(0, &rset); /* 将文件描述符0(标准输入)添加到文件描述符集 */
FD_SET(socket_fd, &rset); /* 将用于监听的socket套接字添加到文件描述符集 */
maxfd = socket_fd; /* 重置最大文件描述符集大小 */
/* 5.1开始检测文件描述符 */
ret = select(maxfd + 1, &rset, NULL, NULL, &tout);
if(ret < 0)
{
perror("select");
continue;
}
/* 5.2标准键盘文件描述符就绪时的处理 */
if(FD_ISSET(0, &rset))
{
bzero(buf, BUFSIZ);/* 清空buf */
do{
ret = read(0, buf, BUFSIZ - 1);/* 从标准输入获取数据 */
}while(ret < 0 && EINTR == errno);
if (ret < 0)/* 获取数据失败 */
{
perror ("read() from stdin");
continue;
}
/* 获取数据成功,但是标准输入中没有数据,不需要写入,继续循环即可 */
if (!ret) continue;
/* 向服务器发送数据 */
if(write(socket_fd, buf, strlen(buf)) < 0)/* 将从标准输入获取的数据写入到socket 网络套接字中 */
{
perror ("write to socket error");
continue;
}
/* 判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 5.3socket文件描述符就绪时的处理(服务器有数据发送过来) */
if(FD_ISSET(socket_fd, &rset))
{
bzero (replay, BUFSIZ);
do{
ret = recv(socket_fd, replay, BUFSIZ, 0);
}while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("recv");
continue;
}
/* 若服务器已关闭,则直接退出客户端 */
if (ret == 0) break;
printf("server replay:%s", replay);
if (!strncasecmp(buf, "quit", strlen("quit")))
{
printf ("Sender Client is exiting... ...!\n");
break;
}
}
}
/* 6.关闭文件描述符 */
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}2.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc
DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)
# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)
%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)
clean_o :
@rm -vf *.o
clean_out :
@rm -vf *.out2.4 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
make然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:
gcc -g -O2 -Wall client.c -o client
gcc -g -O2 -Wall server.c -o server对于客户端,我们执行以下命令启动客户端进程:
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002LT模式启动
对于服务器端,我们执行以下命令启动服务器进程:
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口然后会得到以下提示:
please select epoll mode(1:LT 2:ET):我们选择输入1并按下enter后,服务器端中用于通信的文件描述符将会使用默认的工作模式,也就是LT模式,此时我们来看一下现象:

可以发现我们发送的数据,并没有被读取,而服务器端会一直提醒有数据到达,提醒11次后,程序退出,这是因为我设置了超过10次就结束进程,否则会一直打印一直打印,这也说明了,LT模式下,不处理数据的话,内核会一直通知进行,并且epoll_wait()函数也不再阻塞了。
ET模式启动
对于服务器端,我们执行以下命令启动服务器进程:
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口然后会得到以下提示:
please select epoll mode(1:LT 2:ET):我们选择输入2并按下enter后,服务器端中用于通信的文件描述符将会使用ET模式,此时我们来看一下现象:

可以发现我们发送的数据,并没有被读取,然后我们发送一次就提示一次,即便数据没有被处理。