多路复用:
使用一个进程(且只有主线程)同监控若干个文件描述符的读写,这种读写模式叫多路复用。多用于TCP的服务端,用于监控客户端的连接和数据的收发
优点:不需要频繁地创建进程、销毁进程,从而节约了内存资源、时间资源,也避免了进程之间的竞争、等 待。
缺点:单个客户端的任务不能太耗时,否则其他客户端就会感知到
适合并发量高,但任务短小的情景,如:Web服务器
select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:监控多个文件描述符的读、写、异常 操作
fd_set:文件描述符的集合
void FD_CLR(int fd, fd_set *set);
功能:从集合中删除文件描述符
int FD_ISSET(int fd, fd_set *set);
功能:判断文件描述符是否在集合中
void FD_SET(int fd, fd_set *set);
功能:把文件描述符添加到集合中
void FD_ZERO(fd_set *set);
功能:清空集合
nfds:被监控的文件描述符中最大+1
readfds:监控读操作的文件描述符集合
writefds:监控写操作的文件描述符集合
exceptfds:监控异常操作的文件描述符集合
timeout:设置超时时间
返回值:监控到发生相关操作的描述符个数,超时返回0,错误返回-1
注意:
readfds、writefds、exceptfds 这三个参数既是输入也是输出,也就是说调用select函数时里面存 放着需要监控的文件描述符,函数返回后发生操作的文件描述符存在里面
select设计不合理的地方:
1、每次调用select时都需要向它传递被监控者的信息
2、调用结束后若想知道具体哪些文件描述符发生了相关操作,需要把所有文件描述符都测试一遍
select的优点:
它是最早的多路复用函数,几乎所有的操作系统都支持,程序的兼容性高
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc,const char* argv[])
{
// 创建socket对象
int svr_fd = socket(AF_INET,SOCK_STREAM,0);
if(0 > svr_fd)
{
perror("socket");
return EXIT_FAILURE;
}
// 准备通信地址
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(6789);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(addr);
// 绑定
if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
{
perror("bind");
return EXIT_FAILURE;
}
// 网络监听
if(listen(svr_fd,10))
{
perror("listen");
return EXIT_FAILURE;
}
// 定义一个文件描述符集合并清空
fd_set reads;
FD_ZERO(&reads);
// 把需要网络等待的socket描述符添加到集合中
FD_SET(svr_fd,&reads);
// 定义一个超时时间
struct timeval timeout = {5,500};
// 记录最大的文件描述符
int max_fd = svr_fd;
char buf[4096] = {};
size_t buf_size = sizeof(buf);
for(;;)
{
// 备份集合
fd_set reads_copy = reads;
// 调用select 最大描述符 读操作 超时时间
int ret = select(max_fd+1,&reads_copy,NULL,NULL,&timeout);
if(ret > 0)
{
// 先测试网络等待的socket描述符
if(FD_ISSET(svr_fd,&reads_copy))
{
// 调用accept连接客户端
int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
if(0 > cli_fd)
{
perror("accept");
}
else
{
// 把客户端的socket描述符添加到监控集合中
FD_SET(cli_fd,&reads);
if(cli_fd > max_fd)
{
max_fd = cli_fd;
}
}
}
else
{
// 测试其他socket描述符是发生读操作
for(int fd=3; fd<=max_fd; fd++)
{
if(FD_ISSET(fd,&reads_copy) && fd != svr_fd)
{
int ret = recv(fd,buf,buf_size,0);
if(0 >= ret)
{
FD_CLR(fd,&reads);
printf("客户端%d退出\n",fd);
continue;
}
printf("recv:%s bits:%d\n",buf,ret);
strcat(buf,":return");
ret = send(fd,buf,strlen(buf)+1,0);
if(0 >= ret)
{
FD_CLR(fd,&reads);
printf("客户端%d退出\n",fd);
}
}
}
}
}
}
// 注意:需要一个数组用于储存所有正在被监听的描述符
}
pselect
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
功能:与select类似
区别:
1、超时时间类型不同
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
2、pselect函数timeout,既是输入也是输出,它可以返回剩余时间,而select不会改变timeout
3、pselect监听时可以通过sigmask参数设置要屏蔽的信号,可以保障pselect的监听可以不被信号打扰
poll
struct pollfd {
int fd; // 被监听的文件描述符
short events; // 要监听的事件
short revents; // 实际监听到的时间
POLLIN 普通优先级的读事件
POLLPRI 高优先级的读事件
POLLOUT 普通优先级的写事件
POLLRDHUP 对方socket关闭
POLLERR 错误事件
POLLHUP 对方挂起
POLLNVAL 非法描述符
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:pollfd结构体数组
nfds:数组的长度
timeout:超时时间
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int add_fds(struct pollfd* fds,int nfds,int fd,short events)
{
for(int i=0; i<nfds; i++)
{
if(0 == fds[i].fd)
{
fds[i].fd = fd;
fds[i].events = events;
return 0;
}
}
return -1;
}
// TCP sever
int main(int argc,const char* argv[])
{
// 创建socket对象
int svr_fd = socket(AF_INET,SOCK_STREAM,0);
if(0 > svr_fd)
{
perror("socket");
return EXIT_FAILURE;
}
// 准备通信地址
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(6789);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(addr);
// 绑定
if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
{
perror("bind");
return EXIT_FAILURE;
}
// 网络监听
if(listen(svr_fd,10))
{
perror("bind");
return EXIT_FAILURE;
}
// 创建pollfd数组并初始化 calloc
struct pollfd* fds = calloc(sizeof(struct pollfd),10);
// 设置[0]位置要监听的描述符和事件
fds[0].fd = svr_fd;
fds[0].events = POLLIN;
// 定义超时时间
int timeout = 10000;
char buf[4096] = {};
size_t buf_size = sizeof(buf);
for(;;)
{
// 调用poll函数
int ret = poll(fds,10,timeout);
if(0 ==ret)continue;
if(0 > ret)
{
perror("poll");
return EXIT_FAILURE;
}
// 判断[0]位置是否有读事件产生
if(fds[0].revents & POLLIN)
{
// accept 连接客户端
int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
if(0 < cli_fd)
{
// 把客户端的socket描述符添加空pollfd数组的空位置,并设置事件
if(add_fds(fds,10,cli_fd,POLLIN))
{
printf("客户端数量已满");
}
}
}
else
{
// 遍历pollfd数组判断其它位置是否有读事件产生
for(int i=1; i<10; i++)
{
if(!(fds[i].revents&POLLIN))
{
continue;
}
int ret = recv(fds[i].fd,buf,buf_size,0);
if(0 >= ret)
{
printf("客户端%d退出\n",fds[i].fd);
fds[i].fd = 0;
fds[i].events = 0;
continue;
}
printf("recv:%s bits:%d\n",buf,ret);
strcat(buf,":return");
ret = send(fds[i].fd,buf,strlen(buf)+1,0);
if(0 >= ret)
{
printf("客户端%d退出\n",fds[i].fd);
fds[i].fd = 0;
fds[i].events = 0;
}
}
}
}
}
epoll(常用)
int epoll_create(int size);
功能:用于创建一个epoll对象,该对象可以保存被监控的描述符
size:epoll对象保存描述符的数量
返回值:epoll对象的描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll对象,添加、删除描述符
epfd:epoll对象描述符
op:
EPOLL_CTL_ADD 添加描述符
EPOLL_CTL_MOD 修改要监控的事件
EPOLL_CTL_DEL 删除描述符
fd:
要操作的描述符
event:
要监控的事件
返回值:成功 0 失败 -1
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能:监控文件描述符,并返回事件产生的文件描述符
epfd:epoll对象描述符
events:输出型参数,用于获取事件发生的文件描述符
maxevents:epoll对象的空间大小
timeout:超时时间
返回值:事件发生的描述符数量
优点:
1、只需要拷贝一次描述符
2、会把发生事件的描述符返回,不需要遍历所有的描述符
3、编程结构简介
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define EPOLL_SIZE (10)
// TCP sever
int main(int argc,const char* argv[])
{
// 创建socket对象
int svr_fd = socket(AF_INET,SOCK_STREAM,0);
if(0 > svr_fd)
{
perror("socket");
return EXIT_FAILURE;
}
// 准备通信地址
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(6788);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(addr);
// 绑定
if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
{
perror("bind");
return EXIT_FAILURE;
}
// 网络监听
if(listen(svr_fd,10))
{
perror("bind");
return EXIT_FAILURE;
}
// 创建epoll对象
int epfd = epoll_create(EPOLL_SIZE);
if(0 > epfd)
{
perror("epoll_create");
return EXIT_FAILURE;
}
// 向epoll对象中添加描述符
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = svr_fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,svr_fd,&event))
{
perror("epoll_ctl");
return EXIT_FAILURE;
}
// 定义存储监控结果的数组
struct epoll_event events[EPOLL_SIZE] = {};
char buf[4] = {};
size_t buf_size = sizeof(buf);
for(;;)
{
// 监听
int event_cnt = epoll_wait(epfd,events,EPOLL_SIZE,10000);
if(0 == event_cnt)continue;
printf(" epoll_wait 有事件触发!\n");
// 遍历监听的结果
for(int i=0; i<event_cnt; i++)
{
if(svr_fd == events[i].data.fd)
{
int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
if(0 < cli_fd)
{
event.events = EPOLLIN | EPOLLET;
event.data.fd = cli_fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cli_fd,&event);
}
}
else
{
int ret = 0;
while(-1 != (ret = recv(events[i].data.fd,buf,buf_size,MSG_DONTWAIT)))
{
if(0 == ret)
{
printf("客户端%d退出\n",events[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
continue;
}
printf("recv:%s bits:%d\n",buf,ret);
}
strcpy(buf,"return");
ret = send(events[i].data.fd,buf,strlen(buf)+1,0);
if(0 >= ret)
{
printf("客户端%d退出\n",events[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
}
}
}
}
}
epoll的条件触发和边缘触发
条件触发:当文件缓冲区中有要读取的数据就会触发事件
边缘触发:当数据发送过来时触发一次事件
1、事件设置为EPOLLET 边缘触发
1、要循环读取
2、要以非阻塞方式读取
3、当返回-1时说明数据读取完毕
优点:降低事件触发的次数