select
函数是一个在多路复用I/O中经常使用的系统调用,它允许你监视多个文件描述符(通常是套接字)的状态,以确定哪些文件描述符可以进行读取、写入或者已经发生了异常等情况。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//nfds:被监视的文件描述符的数量,通常设置为最大文件描述符加 1。
//readfds:指向一个 fd_set 结构的指针,用于监视可读事件(数据已经准备好读取的套接字)。
//writefds:指向一个 fd_set 结构的指针,用于监视可写事件(数据可以写入的套接字)。
//exceptfds:指向一个 fd_set 结构的指针,用于监视异常事件。
//timeout:设置 select 调用的超时时间,它是一个 struct timeval 结构,可以设置为 NULL 表示无限等待,也可以设置为一个时间间隔,使 select 在指定的时间内超时返回。
工作方式
select 函数的工作方式如下:
①当调用 select 时,它会检查被监视的文件描述符集合(readfds、writefds 和 exceptfds)以查看哪些文件描述符已经准备好了。如果有任何文件描述符处于就绪状态,select 返回;否则它会等待,直到有文件描述符变为就绪状态或者超时。
②一旦 select 返回,我们可以遍历文件描述符集合,检查每个文件描述符的状态以确定它们的就绪状态。
③通常,我们会使用 FD_ISSET 宏来检查文件描述符是否在集合中。如果在集合中,表示相应的操作可以执行(读、写或异常)。
fd_set结构体
fd_set 是一个用于表示文件描述符集合的数据结构,它通常用于 select 函数中,用来指定需要监视的文件描述符。
操作和用法
①初始化 fd_set:在使用 fd_set 之前,通常需要使用 FD_ZERO 来将其初始化为空集合,表示没有任何文件描述符。
fd_set readfds;
FD_ZERO(&readfds);
②添加文件描述符:当要监视一个文件描述符,可以使用 FD_SET 将其添加到集合中。
int sockfd = ...; // 假设有一个套接字
FD_SET(sockfd, &readfds);
③检查文件描述符状态:可以使用 FD_ISSET 宏来检查文件描述符是否在集合中,以确定其状态。通常,这是在 select 返回后用于检查就绪状态的操作。
if (FD_ISSET(sockfd, &readfds))
{
// sockfd 已经准备好进行读取操作
}
④从集合中移除文件描述符:如果你不再需要监视某个文件描述符,可以使用 FD_CLR 将其从集合中移除。
FD_CLR(sockfd, &readfds);
使用fd_set的注意事项
①fd_set 是一个有限大小的数据结构,通常大小由操作系统定义。因此,它不能包含所有可能的文件描述符,特别是在高并发的网络应用中可能会受到限制。
②在不同的操作系统上, fd_set 的大小可能会有所不同。你可以使用 FD_SETSIZE 宏来获取 fd_set 的大小限制。
③当使用 select 函数时,它会修改 fd_set,将其中不符合就绪条件的文件描述符从集合中移除,因此在每次调用 select 之前,通常需要重新初始化 fd_set。
④fd_set 通常是一个全局变量,因为它需要在 select 调用之前和之后都可见。
select实例
接下来写一个基于非阻塞 I/O 多路复用的简单服务器程序。它可以处理多个客户端连接,同时监听客户端发来的数据,然后回复 "ok"。
服务器端
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/socket.h>
#include<sys/select.h>
#include <arpa/inet.h>
#define MAXFD 10 //定义了最大文件描述符数量,即服务器能够处理的最大连接数。
using namespace std;
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if (sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;//地址族
saddr.sin_port = htons(6000);//端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//地址
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
cout << "bind error" << endl;
return -1;
}
res = listen(sockfd, 5);
if (res == -1)
{
return -1;
}
return sockfd;
}
//初始化文件描述符数组,将所有元素设为 -1
void fds_init(int fds[])
{
for (int i = 0; i < MAXFD; i++)
{
fds[i] = -1;
}
}
//向文件描述符数组中添加文件描述符
void fds_add(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
//从文件描述符数组中删除文件描述符
void fds_del(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
//接收客户端数据,如果客户端关闭连接,则从文件描述符数组中删除该套接字
void accept_client(int sockfd, int fds[])
{
int c = accept(sockfd, NULL, NULL);
if (c < 0)
{
return;
}
cout << "c= " << c << endl;
fds_add(fds, c);
}
void recv_data(int c, int fds[])
{
char buff[128] = { 0 };
int n = recv(c, buff, 127, 0);
if (n <= 0)
{
fds_del(fds, c);
close(c);
cout << "client close" << endl;
return;
}
cout << "buff " << c << "=" << buff << endl;
send(c, "ok", 2, 0);
}
int main()
{
// 创建套接字
int sockfd = socket_init();
//创建文件描述符集合
fd_set fdset;
//初始化文件描述符数组
int fds[MAXFD];
fds_init(fds);
//添加套接字到数组中
fds_add(fds, sockfd);
while (1)
{
// 清空文件描述符集合并找到最大的文件描述符
FD_ZERO(&fdset);
//确定监听范围,即确定有几个待处理文件描述符
int maxfd = -1;
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)// 说明此位置没有文件描述符
{
continue;
}
//将文件描述符添加到集合中
FD_SET(fds[i], &fdset);
if (maxfd < fds[i])
{
maxfd = fds[i];
}
}
struct timeval tv = { 5, 0 };//设定超时时间为5秒
int n = select(maxfd + 1, &fdset, NULL, NULL, &tv);
if (n == -1)
{
cout << "select error" << endl;
}
else if (n == 0)
{
cout << "time out" << endl;
}
else
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)
{
continue;
}
//检查文件描述符并确定其状态
if (FD_ISSET(fds[i], &fdset))
{
if (fds[i] == sockfd) // 表明有新连接请求
{
accept_client(sockfd, fds);
}
else//表明是已连接的客户端发送数据
{
recv_data(fds[i], fds);
}
}
}
}
}
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("Socket creation failed");
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
perror("Connect error");
exit(1);
}
while (1)
{
char buff[128] = {0};
printf("Input (type 'end' to exit):\n");
fgets(buff, 128, stdin);
if (strcmp(buff, "end\n") == 0)
{
break;
}
send(sockfd, buff, strlen(buff) - 1, 0);
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("Received: %s\n", buff);
}
close(sockfd);
exit(0);
}
运行结果:
poll
poll() 函数是一个用于进行多路复用的系统调用,它与 select() 类似.
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//fds: 是一个指向 pollfd 结构体数组的指针,每个结构体描述一个待监视的文件描述符以及监视的事件类型。
//nfds: 是 fds 数组的大小,即待监视的文件描述符的数量。
//timeout: 是超时时间,以毫秒为单位。如果设置为负数,poll() 会一直阻塞直到有事件发生。如果设置为0,poll() 会立即返回。如果设置为正数,poll() 会等待指定时间后返回,不管是否有事件发生。
pollfd 结构体
struct pollfd
{
int fd; // 文件描述符
short events; // 待监视的事件类型
short revents; // 实际发生的事件类型(由内核填充)
};
//events: 要监视的事件类型,可以是以下之一或它们的组合:
POLLIN | 文件描述符上有可读数据。 |
POLLOUT | 文件描述符上可以写入数据。 |
POLLPR | 有紧急数据可读(如带外数据) |
POLLERR | 发生错误 |
POLLHUP | 发生挂起事件 |
POLLNVAL | 文件描述符不是一个有效的打开文件 |
返回值:
大于0:表示有事件发生,返回值为就绪文件描述符的数量。
等于0:表示超时,没有文件描述符处于就绪状态。
小于0:表示出现错误
poll实例
服务器端
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/socket.h>
#include<sys/select.h>
#include <arpa/inet.h>
#include<poll.h>
#define MAXFD 10
using namespace std;
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if (sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;//地址族
saddr.sin_port = htons(6000);//端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//地址
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
cout << "bind error" << endl;
return -1;
}
res = listen(sockfd, 5);
if (res == -1)
{
return -1;
}
return sockfd;
}
void fds_init(struct pollfd fds[])
{
for (int i = 0; i < MAXFD;i++)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void fds_add(struct pollfd fds[],int fd)
{
for (int i = 0; i < MAXFD;i++)
{
if(fds[i].fd==-1)
{
fds[i].fd = fd;
fds[i].events = POLLIN;//r
fds[i].revents = 0;
break;
}
}
}
void fds_del(struct pollfd fds[],int fd)
{
for (int i = 0; i < MAXFD;i++)
{
if(fds[i].fd==fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
void accept_client(int sockfd,struct pollfd fds[])
{
int c = accept(sockfd, NULL, NULL);
if(c<0)
{
return;
}
cout << "accept c =" << c << endl;
fds_add(fds, c);
}
void recv_data(int c,struct pollfd fds[])
{
char buff[128] = {0};
int n = recv(c, buff, 127, 0);
if(n<=0)
{
fds_del(fds, c);
close(c);
cout << "client close" << endl;
return;
}
cout << "recv buff =" << buff << endl;
send(c, "ok", 2, 0);
}
int main()
{
int sockfd = socket_init();
if(sockfd==-1)
{
exit(1);
}
struct pollfd fds[MAXFD];
fds_init(fds);
fds_add(fds, sockfd);
while(1)
{
int n = poll(fds, MAXFD, 5000);//可嫩会阻塞在这
if(n==-1)
{
cout << "poll error" << endl;
}
else if(n==0)
{
cout << "time out" << endl;
}
else
{
for (int i = 0; i < MAXFD;i++)
{
if(fds[i].fd==-1)
{
continue;
}
if(fds[i].revents&POLLIN)
{
if(fds[i].fd==sockfd)//新客户
{
accept_client(sockfd, fds);
}
else//老客户
{
recv_data(fds[i].fd, fds);
}
}
}
}
}
}
客户端
与select实例中的相同
epoll
"epoll" 是 Linux 系统上用于高效 I/O 多路复用的一组系统调用和相关数据结构。它相比于传统的 select 和 poll 等方式,在处理大量连接时更加高效。
epoll 支持多种事件类型,其中常见的包括:
EPOLLIN | 表示文件描述符上有数据可读 |
EPOLLOUT | 表示文件描述符上可以写入数据 |
EPOLLET | 使用边缘触发模式(Edge Triggered),只通知一次事件 |
EPOLLRDHUP | 对端关闭连接或者半关闭 |
EPOLLERR | 发生错误 |
EPOLLHUP | 发生挂起事件 |
epoll_creat
int epoll_create(int size);//创建一个新的 epoll 实例的系统调用
//size:表示 epoll 实例可以管理的文件描述符的数量
工作方式
①调用 epoll_create 创建一个 epoll 实例。
②使用 epoll_ctl 添加要监视的文件描述符和事件类型,比如 EPOLLIN(读事件)或 EPOLLOUT(写事件)。
③调用 epoll_wait 等待事件的发生。这个函数会一直阻塞,直到有文件描述符上的事件发生或者达到设置的超时时间。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd:epoll 实例的文件描述符,是之前由 epoll_create 创建的。 //op:表示要执行的操作,可以是以下值之一:
EPOLL_CTL_ADD | 添加一个文件描述符到 epoll 实例中,使得 epoll 可以监视它 |
EPOLL_CTL_MOD | 修改一个文件描述符在 epoll 实例中的事件监听 |
EPOLL_CTL_DEL | 从 epoll 实例中删除一个文件描述符,不再监听它 |
//fd:要操作的文件描述符,可以是套接字、文件等。
//event:一个 struct epoll_event 结构,描述了要监听的事件类型,可以包括读事件、写事件等。
工作方式
①创建一个 struct epoll_event 结构,设置需要监听的事件类型,例如 EPOLLIN 表示读事件、EPOLLOUT 表示写事件等。 ②调用 epoll_ctl 函数来添加、修改或删除文件描述符的事件监听。通过设置 op 参数来指定具体的操作。 ③如果添加或修改操作成功,epoll_ctl 函数返回 0,否则返回 -1,并可以使用 errno 获取错误信息
epoll_wait
是 epoll 系列系统调用之一,用于等待文件描述符上的事件发生,并将就绪的文件描述符返回给调用者。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//epfd:epoll 实例的文件描述符,由 epoll_create 创建。 //events:用于存储就绪事件的数组,每个元素都是一个 struct epoll_event 结构体。 //maxevents:events 数组的大小,表示最多可以存储多少个就绪事件。 //timeout:超时时间,单位是毫秒,指定等待多长时间。传递 -1 表示一直等待,传递 0 表示立即返回,传递正整数表示等待指定的毫秒数。
工作方式
①创建一个 epoll 实例,通过 epoll_create1 或者 epoll_create 函数,得到一个 epoll 文件描述符 epfd,该描述符代表了一个 epoll 实例。
②使用 epoll_ctl 函数将需要监听的文件描述符添加到 epoll 实例中,同时指定关心的事件类型(如读、写、异常等)以及相关的事件数据。
③调用 epoll_wait 函数等待事件的发生。epoll_wait 会一直阻塞,直到指定的文件描述符上发生就绪事件、超时时间到达或者被信号中断。
④一旦有事件发生,epoll_wait 返回就绪事件的数量,并将这些事件存储在传入的 events 数组中。
⑤应用程序可以遍历 events 数组,处理每个就绪事件。
⑥如果需要继续监听事件,可以再次调用 epoll_wait。
epoll_wait 的主要优势在于它可以高效处理大量的文件描述符,因为它不需要遍历所有文件描述符,只返回就绪的文件描述符,从而减少了系统调用的次数,提高了性能。
epoll实例
服务器端
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAXFD 10
using namespace std;
void epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN; // r
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
{
cout << "epoll_ctl error" << endl;
}
}
void epoll_del(int epfd, int fd)
{
if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
{
cout << "epoll error1" << endl;
}
}
void accept_client(int sockfd, int epfd)
{
int c = accept(sockfd, NULL, NULL);
if (c < 0)
{
return;
}
cout << "accept c=" << c << endl;
epoll_add(epfd, c);
}
void recv_data(int c, int epfd)
{
char buff[128] = {0};
int n = recv(c, buff, 127, 0);
if (n <= 0)
{
epoll_del(epfd, c);
close(c);
cout << "client close" << endl;
return;
}
cout << "recv buff =" << buff << endl;
send(c, "ok", 2, 0);
}
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET; //地址族
saddr.sin_port = htons(6000); //端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (res == -1)
{
cout << "bind error" << endl;
return -1;
}
res = listen(sockfd, 5);
if (res == -1)
{
return -1;
}
return sockfd;
}
int main()
{
int sockfd = socket_init();
if (sockfd == -1)
{
exit(1);
}
int epfd = epoll_create(MAXFD); //创建内核事件表
if (epfd == -1)
{
exit(1);
}
epoll_add(epfd, sockfd);
struct epoll_event evs[MAXFD]; //存放就绪描述符
while (1)
{
int n = epoll_wait(epfd, evs, MAXFD, 5000); //阻塞,等就绪事件发生
if (n < 0)
{
cout << "epoll error" << endl;
continue;
}
else if (n == 0)
{
cout << "time out" << endl;
}
else
{
for (int i = 0; i < n; i++)
{
if (evs[i].events & EPOLLIN)
{
if (evs[i].data.fd == sockfd) //新客户
{
accept_client(sockfd, epfd);
}
else //老客户
{
recv_data(evs[i].data.fd, epfd);
}
}
}
}
}
}
客户端
与select中的相同。