一、io多路复用
在上一篇文章中,实现了一请求一线程的模型。其优点是代码简单,容易实现。但是在高并发的情况下,该模型就很难达到所需的并发量。此时我们就需要采用io多路复用的模型,来实现高并发。
I/O多路复用的核心思想是通过一种机制,使得程序可以同时监视多个I/O事件,并在其中的一个或多个I/O事件就绪时,通知应用程序进行相应的处理。它允许一个进程或线程同时监听多个文件描述符的I/O事件,并在fd准备就绪时(可读可写)进行处理。这样,程序就无需为每个I/O操作都创建一个线程或进程,而是可以通过一个线程或进程来处理多个I/O操作,从而提高资源利用率和系统的高并发。在Linux系统中,I/O多路复用主要有三种实现方式:select、poll和epoll。
二、select
io多路复用中的select是一种常用的同步io模型,它允许单个线程监视多个文件描述符(socketfd),并在一个或多个fd就绪时(可读可写)进行相应的读写操作。
2.1 select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds | 被监听的sockfd数量(最大的sockfd加1) |
readfds | 指向可读文件描述符集合的指针 |
writeds | 指向可写文件描述符集合的指针 |
指向异常 | 指向异常条件文件描述符集和的指针 |
timeout | 等待时长,如果设置NULL,则一直阻塞 |
发生错误时 select函数返回-1,超时返回0。正常时返回发生事件的sockfd总数。需要说明的是,sockfd从3开始。其中fd 0、1、2、分别为标准输入、标准输出、和标准错误输出。
2.2 fd_set集合与宏操作
fd_set结构体用于表示文件描述符集合,它通常以一个位数组的形式实现,每一位代表一个文件描述符。对fd_set的操作通常由以下宏来完成:
FD_ZERO(fd_set *set);//清空指定的文件描述符集合set,将其所有位都置为0。
FD_SET(int fd, fd_set *set);//将指定的文件描述符fd添加到文件描述符集合set中,相应的位将被置为1。
FD_CLR(int fd, fd_set *set)
//将指定的文件描述符fd从文件描述符集合set中移除,相应的位将被清零(置为0)。
FD_ISSET(int fd, fd_set *set);
//检查指定的文件描述符fd是否在文件描述符集合set中,如果存在,则返回true;否则,返回false。
2.3 实现步骤
1、初始化:创建一个文件描述符集合,用于存放需要监视的文件描述符(socket)。
fd_set rfds, rset;//rfds为sockfd集和,rset为可读集和
FD_ZERO(&rfds);//对集合清零
FD_SET(sockfd, &rfds);//将sockfd与集和绑定(相应位置1)
int maxfd = sockfd;(fd的个数)
2、监听:调用select函数,将文件描述符集合传递给内核,让内核监视这些文件描述符的状态变化(如可读、可写、异常)。
int nready = select(maxfd+1,&rset, NULL, NULL, NULL);//返回可读可写fd的数量
3、阻塞等待:select函数会阻塞当前线程,直到一个或多个文件描述符就绪,或者超时。
if(FD_ISSET(sockfd, &rset)){
//如果rset集合中有fd被设置1(即变得可读),这意味着有新的客户端连接请求
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
//连接新的客户端
printf("accept finshed: %d\n", clientfd);
//打印新客户端的fd编号
FD_SET(clientfd, &rfds);
//将新的客户端fd加进rfds集合中
if(clientfd > maxfd)maxfd = clientfd;
//更新当前最大的fd数
}
4、处理客户端数据:进行相关读写操作
int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
//遍历从sockfd+1到maxfd的所有文件描述符(跳过监听套接字本身)。
if(FD_ISSET(i, &rset)){//如果某个文件描述符在rset集合中被设置,则尝试使用recv()接收数据。
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if(count == 0){//如果recv()返回0,表示客户端已关闭连接,此时关闭套接字并从rfds集合中移除。
printf("client disconnect: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV:%s\n", buffer);
//将收到的数据发送出去
count = send(i, buffer, 1024, 0);
printf("SEND:%d\n", count);
}
}
完整代码:
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2000);
if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))){
printf("bind failed:%s\n",strerror(errno));
}
listen(sockfd,10);
printf("listen finshed: %d\n", sockfd);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
//select
fd_set rfds, rset;
FD_ZERO(&rfds);//对集合清零
FD_SET(sockfd, &rfds);//将sockfd与集和绑定
int maxfd = sockfd;
while(1){
rset = rfds;
int nready = select(maxfd+1,&rset, NULL, NULL, NULL);//返回可读可写fd的数量
if(FD_ISSET(sockfd, &rset)){//是否置1,置1则进行accept
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
FD_SET(clientfd, &rfds);
if(clientfd > maxfd)maxfd = clientfd;
}
//recv
int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
if(FD_ISSET(i, &rset)){
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if(count == 0){
printf("client disconnect: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV:%s\n", buffer);
count = send(i, buffer, 1024, 0);
printf("SEND:%d\n", count);
}
}
}
getchar();
return 0;
}
三、poll
select虽然实现了io多路复用,但参数较多。poll是对select的一种改进。
3.1 结构体pollfd
struct pollfd {
int fd; // 委托内核监听的文件描述符 //
short events; // 委托内核检测文件描述符的什么事件,如输入、输出、错误等//
short revents; // 文件描述符实际发生的事件 -> 传出 //
};
3.2 函数poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds | pollfd类型的数组,存储了待检测的fd的信息 |
nfds | fd的个数(maxfd+1) |
timeout | 阻塞时间,-1时一直阻塞 |
poll函数返回-1时表示失败。返回一个大于0的整数时,表示检测的集合中已就绪的文件描述符的总个数。
3.2 实现步骤
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;//可读
int maxfd = sockfd;
//初始化pollfd数组//
while(1){
int nready = poll(fds, maxfd+1, -1);
if(fds[sockfd].revents & POLLIN){
//如果当 sockfd 的读缓冲区有数据时(即有新连接到来),进行连接//
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
//将客服端fd添加进pollfd数组//
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
//更新maxfd值//
if(clientfd > maxfd)maxfd = clientfd;
}
//遍历每个客户端fd//
int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
//可读时进行数据的收发//
if(fds[i].revents & POLLIN){
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
//没有数据时说明客户端断开连接//
if(count == 0){
printf("client disconnect: %d\n", i);
close(i);//释放客户端fd
//在pollfd数组中将客户端fd移除//
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
printf("RECV:%s\n", buffer);
count = send(i, buffer, 1024, 0);
printf("SEND:%d\n", count);
}
}
}
四、epoll
如今云主机大多都是选择Linux,是因为Linux引入了epoll这个机制。
4.1、 结构体epoll_event
epoll_event是Linux内核提供的一个数据结构,它在epoll机制中用于表示事件。
struct epoll_event {
uint32_t events; // Epoll 事件 //
epoll_data_t data; // 联合体,用于存储用户自定义的数据 //
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
用户可以使用data成员来存储与事件相关的自定义数据,当事件发生时,该数据会返回给用户,方便用户识别是哪一个文件描述符发生了事件。
4.2、函数
4.2.1、epoll_create
int epoll_create(int size);
用于创建一个epoll实例,返回一个文件描述符。size是内核的建议大小,实际中大于0即可。
4.2.2、epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用于控制epoll实例,添加、删除或修改监听的fd及其事件。
epfd | epoll实例的文件描述符 |
op | 操作类型如添加(EPOLL_CTL_ADD)删除(EPOLL_CTL_DEL)修改(EPOLL_CTL_MOD) |
fd | 要监听的fd |
event | 指向epoll_event结构体的指针,描述了监听fd的事件与数据 |
4.2.3、epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待 epoll实例上的事件发生,并返回就绪的文件描述符列表。
events | 用于存储就绪事件的数组 |
maxevents | 数组大小 |
timeout | 等待时间,-1为阻塞 |
4.3、代码流程
int epfd = epoll_create(1);//创建一个新的epoll实例
//设置epoll事件
struct epoll_event ev;
ev.events = EPOLLIN;//只关心读事件
ev.data.fd = sockfd;//将服务器fd与事件关联
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);//在epoll实例epfd中添加监听fd
while(1){
//创建事件集合
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);//等待事件发生,并返回就绪fd
int i = 0;
for(i = 0;i < nready; i++){//遍历事件集合
int connfd = events[i].data.fd;
if(connfd == sockfd){//如果事件对应服务器fd,则接收新连接
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr,&len);
printf("accept finshed: %d\n", clientfd);
//设置新连接fd的事件
ev.events = EPOLLIN;
ev.data.fd = clientfd;
//将客户端fd添加到实例epfd中
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
}else if(events[i].events & EPOLLIN){//如果事件是读事件,则进行读取数据
char buffer[1024] = {0};
int count = recv(connfd, buffer, 1024, 0);
if(count == 0){//如果读取到的数据量为0则断开disconnect
printf("client disconnect: %d\n", connfd);
close(connfd);
//将客户端从实例中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
printf("RECV: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}