Linux IO多路复用
1、什么是I/O多路复用??
I/O复用无非就是多个进程共同使用一个I/O输入输出流。一旦发现进程指定的一个或者多个描述符可进行无阻塞IO访问时,它就通知该进程。
服务器端工作流程:
调用 socket() 函数创建套接字 用 bind() 函数将创建的套接字与服务端IP地址绑定
调用listen()函数监听socket() 函数创建的套接字,等待客户端连接 当客户端请求到来之后
调用 accept()函数接受连接请求,返回一个对应于此连接的新的套接字,做好通信准备
调用 write()/read() 函数和 send()/recv()函数进行数据的读写,通过 accept() 返回的套接字和客户端进行通信 关闭socket(close)
客户端工作流程:
调用 socket() 函数创建套接字
调用 connect() 函数连接服务端
调用write()/read() 函数或者 send()/recv() 函数进行数据的读写
关闭socket(close)
此技术的目的:I/O 多路复用是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程或线程不阻塞于某个特定的 I/O 系统调用。
什么?听不懂啥意思? --->没事,点击这里:I/O复用的理解
2、IO多路复用适用以下场合:
(1) 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2) 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3) 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4) 如果一个服务器既要处理TCP,又要处理UDP,一般要使用I/O复用。
(5) 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减少了系统的开销。
对于应用层来说,使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。
总的来说,I/O处理的模型有5种:
● 阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。
● 非阻塞I/O模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且立即返回。非阻塞I/O使用户可以调用不会阻塞的I/O操作,如open()、write()和read()。如果该操作不能完成,则会立即返回出错(如打不开文件)或者返回0(如在缓冲区中没有数据可以读取或者没空间可以写入数据)。
● I/O多路转接模型:在这种模型下,如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而是让其中的一个函数等待,在此期间,I/O还能进行其他操作。如本小节要介绍的select()和poll()函数,就是属于这种模型。
● 信号驱动I/O模型:在这种模型下,进程要定义一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个I/O操作决定的。
它是非阻塞的。当有就绪的数据时,内核就向该进程发送SIGIO信号。 无论我们如何处理SIGIO信号,这种模型的好处是当等待数据到达时,可以不阻塞。主程序继续执行,只有收到SIGIO信号时才去处理数据即可。
● 异步I/O模型:在这种模型下,进程先让内核启动I/O操作,并在整个操作完成后通知该进程。这种模型与信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知进程I/O操作何时完成的。现在,并不是所有的系统都支持这种模型。
可以看到,select()和poll()的I/O多路转接模型是处理I/O复用的一个高效的方法。它可以具体设置程序中每一个所关心的文件描述符的条件、希望等待的时间等,从select()和poll()函数返回时,内核会通知用户已准备好的文件描述符的数量、已准备好的条件(或事件)等。通过使用select()和poll()函数的返回结果(可能是检测到某个文件描述符的注册事件或是超时,或是调用出错),就可以调用相应的I/O处理函数了。
select系统调用的用途:在一段时间内,监听用户感兴趣的文件描述符上的可读,可写和异常等事件
select原理:
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
poll系统调用:在制定时间内轮询一定数量的文件描述符,已测试其中是否有就绪者
poll原理:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
epoll系列系统调用:
epoll原理:
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll是linux特有的I/O复用函数
文件描述符(用来标识内核中的事件表)的创建有epoll_creat来完成
3、select()函数的语法要点
select()函数根据希望进行的文件操作对文件描述符进行了分类处理,这里对文件描述符的处理主要涉及4个宏函数,如下表所示。
select()文件描述符处理函数
当应用程序使用FD_ZERO/FD_SET/FD_CLR宏设置好要监听的文件描述符集合后,调用select()函数执行监听,如果没有一个描述符准备好IO并且没有指定超时时间,那么select()函数会一直等待下去不会返回。
当函数正常返回后,监听的文件描述符集合中没有准备好的文件描述符会被删除,只剩下已经准备好的文件描述符(但是不知道是哪一个,导致后面要用FD_ISSET来判断),之后可以使用FD_ISSET(fd, set);宏来判断set集合中是否有fd文件描述符来判断fd是否准备好IO。
一般来说,在每次使用select()函数之前,首先使用FD_ZERO()和FD_SET()来初始化文件描述符集(在需要重复调用select()函数时,先把一次初始化好的文件描述符集备份下来,每次读取它即可)。在select()函数返回后,可循环使用FD_ISSET()来测试描述符集,在执行完对相关文件描述符的操作后,使用FD_CLR()来清除描述符集。
另外,select()函数中的timeout是一个struct timeval类型的指针,该结构体如下所示:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}
当使用select()函数时,存在一系列的问题,例如,内核必须检查多余的文件描述符,每次调用select()之后必须重置被监听的文件描述符集,而且可监听的文件个数受限制(使用FD_SETSIZE宏来表示fd_set结构能够容纳的文件描述符的大数目)等。
基本流程,如图所示:
多个客户端与服务端同时通信:
/* update_select_server.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的头文件,不能用errno.h
const int BUFFER_SIZE = 2048;
const int SERVER_PORT = 4321;
const int CLIENT_MAX_COUNT = 15;//客户端最大链接数
inline int max(int a,int b){ return (a>b ? (a+1) : (b+1)); }//内联函数
int exit_size = 0;
int main(int argc, char const *argv[])
{
int server_socket_fd;//服务端套接字标识符
int maxfd;//最大标识符
int connfd,sockfd;//客户端连接的标识符
int real_client_count = 0;//客户端真实连接个数
int client_socket_fd[CLIENT_MAX_COUNT];//客户端的标识符
int j;
struct sockaddr_in server_addr;//服务端的信息
struct sockaddr_in client_addr;//客户端的信息
char buffer[BUFFER_SIZE] = {0};//接受客户端的消息
fd_set fds,allset;//文件标识符集
int ret;//返回值
int n;//接受的字节数
struct timeval tv;
/* 创建套节字 */
server_socket_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 将套接字和IP、端口绑定 */
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具体的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
/* 创建套接字监听队列 */
if (listen(server_socket_fd,CLIENT_MAX_COUNT) <0 ) {//监听队列最大的长度为15
fprintf(stderr,"listen ERROR:%s\n",strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客户端信息的长度
maxfd = server_socket_fd;
for (int i = 0; i < CLIENT_MAX_COUNT; i++)
{
/* code */
client_socket_fd[i] = -1;//初始化为-1
}
FD_ZERO(&fds);//清空文件描述符集
FD_SET(0,&allset);//把标准输入加进集合中
FD_SET(server_socket_fd,&allset);
tv.tv_sec = 100;//100秒钟
tv.tv_usec = 0;//0微秒
while (1)
{
/* code */
FD_ZERO(&fds);//清空文件描述符集
fds = allset;
switch (select(maxfd+1, &fds, NULL, NULL, NULL))
{
case -1:
perror("select():");
exit(1);
break;
case 0:
printf("拐子,搞快点设,时间超时了!\n");
break;
default:
if (FD_ISSET(server_socket_fd, &fds)) {//客户端有连接请求
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
} else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
for (j = 0; j < CLIENT_MAX_COUNT; j++) {
/* 把当前连接的客户端的标识符加入到数组中 */
if (-1 == client_socket_fd[j]) {
client_socket_fd[j] = connfd;
real_client_count++;
break;
}
}
FD_SET(connfd,&allset);//添加此客户端标识符给allset集合
if (connfd > maxfd) {
maxfd = connfd;
}
if (real_client_count > CLIENT_MAX_COUNT) {
real_client_count = CLIENT_MAX_COUNT;//真实连接不能超过最大连接
}
}
break;
}
if (FD_ISSET(0, &fds)) {
memset(buffer, 0, BUFFER_SIZE);
fgets(buffer, BUFFER_SIZE, stdin);
printf("从标准输入的数据为:%s\n", buffer);
if (0 == strncmp(buffer, "exit", 4)) {
printf("I will exit!\n");
for (int x = 0; x < real_client_count; ++x) {
if (client_socket_fd[x] != -1) {
close(client_socket_fd[x]);
}
}
return 0;
}
}
//对客户端进行读操作
printf("对客户端进行操作\n");
for (int k = 0;k < real_client_count; ++k) {
if ((sockfd = client_socket_fd[k]) < 0) {
continue; // 没有连接数
}
if (FD_ISSET(sockfd, &fds)) {
n = recv(sockfd,buffer,BUFFER_SIZE,0);
printf("从客户端传来的数据为:%d个\n",n);
if (n >0) {
buffer[n] = '\0';
printf("来自客户端%d的数据为:%s\n",k+1,buffer);
}else if (0 == n) {
printf("客户端%d退出\n",k+1);
FD_CLR(client_socket_fd[k], &allset);
client_socket_fd[k] = -1;
continue;
}
}
}
break;
}
}
/* 关闭套接字 */
close(server_socket_fd);
return 0;
}
/* multiplex_client1.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
const int BUFFER_SIZE = 2048;
int main(int argc, char const *argv[])
{
/* code */
int client_socket_fd;
int result;
struct sockaddr_in server_addr;//服务端的信息
/* 创建套节字 */
client_socket_fd = socket(AF_INET,SOCK_STREAM,0);
/* 将套接字和IP、端口绑定 */
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(4321); //端口
result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
char sendmsg[BUFFER_SIZE] = {0};//发送数据
while (1)
{
/* 发送数据 */
memset(sendmsg,0,BUFFER_SIZE);
printf("请输入你要发给服务端的数据:\n");
fgets(sendmsg,BUFFER_SIZE,stdin);
//write(client_socket_fd,sendmsg,BUFFER_SIZE);
send(client_socket_fd,sendmsg,strlen(sendmsg),0);
if (0 == strncmp(sendmsg,"exit",4)) {
break;
}
}
/* 关闭套接字 */
close(client_socket_fd);
return 0;
}
客户端二:
/* multiplex_client2.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
const int BUFFER_SIZE = 2048;
int main(int argc, char const *argv[])
{
/* code */
int client_socket_fd;
int result;
struct sockaddr_in server_addr;//服务端的信息
/* 创建套节字 */
client_socket_fd = socket(AF_INET,SOCK_STREAM,0);
/* 将套接字和IP、端口绑定 */
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(4321); //端口
result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
char sendmsg[BUFFER_SIZE] = "你好,我是客户端2";//发送数据
while (1)
{
/* 发送数据 */
send(client_socket_fd,sendmsg,strlen(sendmsg),0);
sleep(4);
}
/* 关闭套接字 */
close(client_socket_fd);
return 0;
}
当然,可以把客户端一进程开多个与服务端进行通信。
select的优点:
1、select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,这样所带来的缺点是::
1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
4、poll()函数的语法要点
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
标志 | 解释 |
---|---|
POLLIN | 有数据可读。 |
POLLRDNORM | 有普通数据可读。 |
POLLRDBAND | 有优先数据可读。 |
POLLPRI | 有紧迫数据可读。 |
POLLOUT | 写数据不会导致阻塞。 |
POLLWRNORM | 写普通数据不会导致阻塞。 |
POLLWRBAND | 写优先数据不会导致阻塞。 |
POLLMSGSIGPOLL | 消息可用。 |
POLLER | 指定的文件描述符发生错误。 |
POLLHUP | 指定的文件描述符挂起事件。 |
POLLNVAL | 指定的文件描述符非法。 |
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
使用poll()和select()不一样,你不需要显式地请求异常情况报告。
poll的优点:
1、它没有最大连接数的限制,原因是它是基于链表来存储的。
poll的缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
select()和poll()函数的比较
select()和poll()函数本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有大文件描述符数量的限制。并且select()返回后,之前没有准备好的文件描述符会从集合当中删除,这样如果下次需要再次添加所有文件描述符或者使用两个相同的文件描述符集合,一个用于备份,一个用于监听,比较复杂。poll不需要这个复杂的操作。poll和select同样存在一个缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而无论这些文件描述符是否就绪。它的开销随着文件描述符数量的增加而线性增加。
一个服务端与多个客户端进行通信----socket---poll
/*************************************************************************
> File Name: socket_poll.cpp
> Author: XiaZhaoJian
> Mail: xiazhaojian@iauto.com
> Created Time: 2019年 08月 09日 星期五 13:49:29 CST
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的头文件,不能用errno.h
const int BUFFER_SIZE = 2048;//缓冲池大小,用于接受消息的
const int SERVER_PORT = 4321;//端口号
const int CLIENT_MAX_COUNT = 50;//客户端最大链接数
int main(int argc, char const *argv[])
{
int server_socket_fd;//服务端套接字标识符
int connfd,sockfd;//客户端连接的标识符
int real_client_index = 0;//客户端client_socket_fd最大不空闲下标
int n;//接受的字节数
int i;
char buffer[BUFFER_SIZE] = {0};//接受客户端的消息
struct sockaddr_in server_addr;//服务端的信息
struct sockaddr_in client_addr;//客户端的信息
struct pollfd client_socket_fd[CLIENT_MAX_COUNT];//客户端的标识符
/* 创建套节字 */
server_socket_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 将套接字和IP、端口绑定 */
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具体的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
/* 创建套接字监听队列 */
if (listen(server_socket_fd,CLIENT_MAX_COUNT) <0 ) {//监听队列最大的长度为15
fprintf(stderr,"listen ERROR:%s\n",strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客户端信息的长度
for (i = 0; i < CLIENT_MAX_COUNT; i++)
{
/* code */
client_socket_fd[i].fd = -1;//初始化为-1
}
client_socket_fd[0].fd = server_socket_fd;//把服务端当前Socket套接字加入监听中
client_socket_fd[0].events = POLLIN;//数据可读标志事件
while (1) {
switch (poll(client_socket_fd,real_client_index+1,-1))
{
case -1:
perror("poll():");
exit(1);
break;
case 0:
printf("拐子,搞快点设,时间超时了!\n");
break;
default:
if(client_socket_fd[0].revents & POLLIN) {//客户端有连接请求
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
} else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
for (i = 0; i < CLIENT_MAX_COUNT; i++) {
/* 把当前连接的客户端的标识符加入到数组中 */
if (-1 == client_socket_fd[i].fd) {
client_socket_fd[i].fd = connfd;
if (i > real_client_index) {
real_client_index = i;
}
break;
}
}
if (i == CLIENT_MAX_COUNT) {
fprintf(stderr, "too many clients\n");
exit(1);
}
client_socket_fd[i].events = POLLIN;//添加此客户端标识符读事件
}
break;
}
for (i = 0; i <= real_client_index; ++i) {
if ((sockfd = client_socket_fd[i].fd) < 0) {
continue; // 没有连接数
}
if (client_socket_fd[i].revents & POLLIN) {
n = recv(sockfd,buffer,BUFFER_SIZE,0);
if (n < 0) {
perror("recv():");
exit(1);
}
printf("从客户端传来的数据为:%d个\n",n);
if (n >0) {
buffer[n] = '\0';
printf("来自客户端%d的数据为:%s\n",i+1,buffer);
}else if (0 == n) {
printf("客户端%d退出\n",i+1);
client_socket_fd[i].fd = -1;
close(sockfd);
continue;
}
}
}
break;
}
}
/* 关闭套接字 */
close(server_socket_fd);
return 0;
}
5、epoll(epoll生动讲解)
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll接口是linux中特有的,其他平台是不支持的。epoll接口可以检测的描述符的个数要远大于select,而且使用的灵活性更高。接下来我们来分析一下epoll的函数API,它主要包括三个函数:epoll_create、epoll_ctl、epoll_wait。
int epoll_create(int size);
功能:创建一个epoll的标示符。
参数:size 现在已经无用,并不表示检测的大值。
返回值:就是获得的epoll的标示符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制需要检测的文件描述符。
参数:
epfd: epoll_create得到的标识符。
op:表示此次调用要执行的操作,包括添加描述符、删除和修改属性。
EPOLL_CTL_ADD 添加描述符
EPOLL_CTL_DEL 删除描述符
EPOLL_CTL_MOD 修改描述符触发的属性
fd: 要检测的文件描述符的值
event: 执行要检测的描述符的特性,结构体成员如下
struct epoll_event
{
uint32_t events; /* Epoll events ,指定描述符被检测的条件*/
epoll_data_t data; /* User data variable */
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:等待就绪的文件描述符
参数:epfd: epoll_create得到的标识符
events: 用来存储就绪的文件描述符状态的结构体数组指针
maxevents: 指定要检测的描述符的大个数
timeout: 指定超时检测的时间(如果不指定超时,将该参数指定为-1即可)
返回值:就绪的文件描述符的个数。
基于socket-epoll,一对多通信:
/*************************************************************************
> File Name: socket_epoll.cpp
> Author: XiaZhaoJian
> Mail: xiazhaojian@iauto.com
> Created Time: 2019年 08月 09日 星期五 16:49:59 CST
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/errno.h> ///errno的头文件,不能用errno.h
const int BUFFER_SIZE = 2048;//缓冲池大小,用于接受消息的
const int SERVER_PORT = 4321;//端口号
const int MAX_EPOLL_EVENTS = 520;//最大连接数
const int CLIENT_MAX_COUNT = 50;//客户端最大链接数
//epoll描述符
int epollfd;
//事件数组
struct epoll_event eventList[MAX_EPOLL_EVENTS];
int main(int argc, char const *argv[])
{
int server_socket_fd;//服务端套接字标识符
int connfd,sockfd;//客户端连接的标识符
int ret;//epoll_wait的返回值
int n;//接受的字节数
int i;
//当前的连接数
int currentClient = 0;
char buffer[BUFFER_SIZE] = {0};//接受客户端的消息
struct sockaddr_in server_addr;//服务端的信息
struct sockaddr_in client_addr;//客户端的信息
/* 创建套节字 */
server_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_socket_fd < 0) {
fprintf(stderr,"create socket ERROR:%s\n",strerror(errno));
exit(1);
}
/* 将套接字和IP、端口绑定 */
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0初始化
server_addr.sin_family = AF_INET;//使用ipv4地址
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //具体的IP地址
server_addr.sin_port = htons(SERVER_PORT); //端口
bind(server_socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
/* 创建套接字监听队列 */
if (listen(server_socket_fd, CLIENT_MAX_COUNT) <0 ) {//监听队列最大的长度为15
fprintf(stderr, "listen ERROR:%s\n", strerror(errno));
close(server_socket_fd);
exit(1);
}
printf("I am server.waiting!!!!\n");
socklen_t client_addr_size = sizeof(struct sockaddr_in);//客户端信息的长度
/* 初始化epoll */
epollfd = epoll_create(MAX_EPOLL_EVENTS);//生成epoll标识符
if (-1 == epollfd) {
fprintf(stderr, "epoll_create failed!!!:%s\n", strerror(errno));
exit(1);
}
struct epoll_event myevent;
myevent.data.fd = server_socket_fd;
myevent.events = EPOLLIN | EPOLLET;
/* 添加 */
if (-1 == epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket_fd,&myevent)) {
fprintf(stderr, "epoll_ctl failed!!!:%s\n", strerror(errno));
exit(1);
}
int timeout = -1;//超时时间
/* 循环监听-epoll */
while (1) {
/* 返回就绪的文件描述符的个数 */
ret = epoll_wait(epollfd,eventList,MAX_EPOLL_EVENTS,timeout);
switch (ret)
{
case -1:
fprintf(stderr, "epoll_wait error!!!:%s\n", strerror(errno));
exit(1);
break;
case 0:
printf("the waiting time is running out!\n");
break;
default:
for (int i = 0; i < ret; ++i) {
if (eventList[i].data.fd == server_socket_fd) {//表示有客户端的连接请求了
connfd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&client_addr_size);
if (-1 == connfd) {
perror("socket accept:");
exit(1);
}else {
printf("connect form %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
myevent.data.fd = connfd;
myevent.events = EPOLLIN|EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &myevent);
}
}else {
n = recv(eventList[i].data.fd,buffer,BUFFER_SIZE,0);
if (n < 0) {
perror("recv():");
exit(1);
}
printf("从客户端传来的数据为:%d个\n",n);
if (n >0) {
buffer[n] = '\0';
printf("来自客户端的数据为:%s\n",buffer);
}else if (0 == n) {
printf("客户端%d退出\n",i+1);
close(eventList[i].data.fd);
continue;
}
}
}
break;
}
}
/* 关闭套接字 */
close(epollfd);
close(server_socket_fd);
return 0;
}
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll在原理上与select和poll的不同之处
在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
注意:
如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。