I/O多路复用
I/O多路复用是在多线程或多进程编程中常用技术。主要是通过select/epoll/poll三个函数支持的,就是通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流,I/O 多路复用技术通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求
在之前我们运用多线程的方式也完成了一个可以同时处理客户端的TCP和UDP的简单服务器
线程实现可以同时处理客户端的服务器:https://blog.csdn.net/wfea_lff/article/details/104184974
那我们为什么要用I/O多路复用来完成一个可以同时处理客户端的服务器?? 这两者之间的差距在哪? 多线程缺点:
引入多线程后,系统的执行路径变成了多条,并且这多条执行路径在并发运行。那么首先程序的执行具有一定的不确定性,每次执行结果可能都会不同,因为程序交 替执行的顺序和时机不同了。其次,使得某些资源的访问出现了竞争,问题变得困难,需要资源进行同步。这会使得程序的可靠性和稳定性降低。
I/O多路复用优点:
与传统的多线程/多进程模型相比。I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
应用场景
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;
服务器需要同时处理多种网络协议的套接字。
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll和select的比较
select
select之前已经说过,这里就不多说了
select 讲解:https://blog.csdn.net/wfea_lff/article/details/104235186
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
epoll
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
补充:
在使用epoll时,我们可以通过以下操作来突破文件描述符最大为1024的限制
可以使用cat命令查看一个进程可以打开的socket描述符上限。
cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
sudo vi /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。
* soft nofile 65536
* hard nofile 100000
一、所需函数
1、epoll_create
函数原型
int epoll_create(int size)
所需头文件
#include <sys/epoll.h>
返回值
成功:指向新创建的红黑树的根节点的fd
失败:-1
参数
size:创建的红黑树的监听节点数量
2.epoll_ctl
函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
所需头文件
#include <sys/epoll.h>
返回值
成功:0
失败:-1
参数
epfd: 为epoll_creat的返回值
op:
表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
fd:待监听的fd
event:为结构体,告诉内核需要监听的事件
结构体为
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
结构体中
event:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
data:为上面结构体中定义的联合体
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
联合体中
fd:待监听的fd
3、epoll_wait
函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
所需头文件
#include <sys/epoll.h>
返回值
成功:返回满足监听条件的个数
失败:-1
参数
epfd:为epoll_creat的返回值
events:
用来存内核得到事件的集合【数组】,这是一个传出参数,传出那些满足监听条件的结构体
!补充:虽说与epoll_ctl中结构体的类型一样,但这里我们定义的时候定义的是数组,上面的event是结构体!
maxevents:
告之内核这个events有多大(定义events数组时的最大元素数目),这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
二、代码示例
服务器
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define OPEN_MAX 5000
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
printf("creat socket fail\n");
return -1;
}
else
{
printf("creat socket success , sockfd = %d\n",sockfd);
}
struct sockaddr_in seraddr,cliaddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5000);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t len = sizeof(struct sockaddr);
int bindret = bind(sockfd,(struct sockaddr *)&seraddr,len);
if(bindret == -1)
{
printf("bind client fail\n");
close(sockfd);
return -2;
}
else
{
printf("bind client success\n");
}
int listenret = listen(sockfd,OPEN_MAX);
if(listenret == -1)
{
printf("listen sockfd fail\n");
close(sockfd);
return -3;
}
else
{
printf("listen sockfd success\n");
}
int epollfd = epoll_create(OPEN_MAX);
if(epollfd == -1)
{
printf("creat epoll fail\n");
close(sockfd);
return -4;
}
else
{
printf("creat epoll success , epollfd = %d\n",epollfd);
}
struct epoll_event event,events[OPEN_MAX];
event.events = EPOLLIN;
event.data.fd = sockfd;
int ectlret = epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);
if(ectlret == -1)
{
printf("epoll add sockfd fail\n");
close(sockfd);
close(epollfd);
return -5;
}
else
{
printf("epoll add sockfd success\n");
}
while(1)
{
int sum = epoll_wait(epollfd,events,OPEN_MAX,-1);
if(sum == -1)
{
printf("epoll_wait fail\n");
close(sockfd);
close(epollfd);
return -6;
}
int i;
for(i=0;i<sum;i++)
{
if(events[i].data.fd == sockfd)
{
int confd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(confd == -1)
{
printf("connect client fail\n");
continue;
}
else
{
printf("connect client(%s) success\nconfd = %d\n",inet_ntoa(cliaddr.sin_addr),confd);
}
event.data.fd = confd;
event.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,confd,&event);
}
else
{
char buf[128];
int size = recv(events[i].data.fd,buf,128,0);
if(size == 0)
{
printf("client(%s) quit connect\n",inet_ntoa(cliaddr.sin_addr));
close(events[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
}
else if(size < 0)
{
printf("recv client(%s) message fail\n",inet_ntoa(cliaddr.sin_addr));
close(events[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
}
else
{
printf("client(%s) send message\n%s",inet_ntoa(cliaddr.sin_addr),buf);
if(strncmp(buf,"end",3) == 0)
{
close(events[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
printf("client(%s) quit connect\n",inet_ntoa(cliaddr.sin_addr));
memset(buf,0,128);
continue;
}
memset(buf,0,128);
}
}
}
}
return 0;
}
客户端
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
printf("creat socket fail\n");
return -1;
}
else
{
printf("creat socket success , sockfd = %d\n",sockfd);
}
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(5000);
cliaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(cliaddr.sin_zero,0,8);
int confd = connect(sockfd,(struct sockaddr *)&cliaddr,sizeof(struct sockaddr));
if(confd < 0)
{
printf("connect fail\n");
close(sockfd);
return -2;
}
else
{
printf("----------connect success-----------\nconfd = %d\n",confd);
}
char buf[128];
while(1)
{
memset(buf,0,128);
printf("send message to server\n");
fgets(buf,128,stdin);
send(sockfd,buf,strlen(buf),0);
if(strncmp(buf,"end",3) == 0)
{
break;
}
}
close(sockfd);
return 0;
}