文章目录
多路复用IO
1.学习背景
- 在昨天的学习中,我们已经把select的端对端通信实现,今天对于select并发功能进行学习
2.select实现客户端并发
1.实现步骤
-
准备表
fd_set readfds; FD_ZERO(&readfds);
-
添加要监控的文件描述符
FD_SET(listenfd,&reafds);
-
准备参数
maxfds = listenfd + 1; fd_set backfds;
-
开始循环
- 如果监听到了listenfd,那么就调用accept将这个客户端添加到监控表中,如果这个connfd+1大于maxfds,就将maxfds+1
- 如果是其他描述符,就说明有客户端数据可读,此时读取数据并处理
2.注意事项
- 每次select前都需要重新赋值,因为select会修改集合
- msxfds必须始终等于当前监控的最大文件描述符加一,否则select可能会漏掉新加入的描述符
3.代码实现
-
服务器
int main(void){ int fd; char tmp[256] = {0}; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return 1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int no =1; setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&no,sizeof(int)); if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } if((listen(fd,5)) < 0){ perror("fail to listen"); return -1; } struct sockaddr_in cliaddr; bzero(&cliaddr,0); socklen_t len = sizeof(cliaddr); int connfd; fd_set readfds; FD_ZERO(&readfds); FD_SET(0,&readfds); FD_SET(fd,&readfds); int maxfds = fd+1; fd_set backfds; while(1){ backfds = readfds; int ret = select(maxfds,&backfds,NULL,NULL,NULL); printf("ret = %d\n",ret); if(ret > 0){ int i = 0; for(i = 0;i < maxfds;++i){ if((FD_ISSET(i,&backfds))){ if(i == fd){ connfd = accept(fd,(struct sockaddr *)&cliaddr,&len); if(connfd < 0){ perror("fail to accept"); return -1; } FD_SET(connfd,&readfds); if(connfd + 1 > maxfds){ maxfds = connfd + 1; } } else{ recv(i,tmp,sizeof(tmp),0); printf("tmp = %s\n",tmp); if(strncmp(tmp,"quit",4) == 0){ FD_CLR(i,&readfds); close(i); } } } } } } close(fd); return 0; }
-
客户端
int main(){ int fd; char buf[256] = {0}; fd = socket(AF_INET,SOCK_STREAM,0); if(fd < 0){ perror("fail to socket"); return -1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } fd_set readfds; FD_ZERO(&readfds); FD_SET(0,&readfds); //FD_SET(1,&readfds); FD_SET(fd,&readfds); int maxfds = fd+1; fd_set backfds; while(1){ backfds = readfds; int ret = select(maxfds,&backfds,NULL,NULL,NULL); printf("ret = %d\n",ret); if(ret > 0){ int i = 0; for(i = 0;i < maxfds;++i){ if(FD_ISSET(i,&backfds)){ if(i == 0){ fgets(buf,sizeof(buf),stdin); buf[strlen(buf)-1] = '\0'; write(fd,buf,strlen(buf)); if(strncmp(buf,"quit",4) == 0){ printf("client quit\n"); return 0; } memset(buf,0,sizeof(buf)); } else{ read(fd,buf,sizeof(buf)); printf("recv from server:%s\n",buf); memset(buf,0,sizeof(buf)); } } } } } close(fd); return 0; }
4.缺点
- 最大监听数受限(默认1024个)
- 每次调用select时总会重置fd_set
- 用户态与内核态拷贝开销大
- 返回后仍需遍历所有fd才能知道哪个就绪
- 效率会随着fd数量增长下降明显
3.poll实现客户端并发
- 使用poll时,会创建pollfd数组,将所有要监控的文件描述符及其感兴趣的事件填入,然后调用poll同意监控。poll支持更多的文件描述符并且不需要每次都重新设置监控集合
1.相关函数
-
poll
原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout); 功能:用于检测一组文件描述符在指定时间是否有IO事件发生 参数: fds:指向pollfd结构体的指针,每个元素描述一个要监控的文件描述符 struct pollfd{ int fd; //要监控的文件描述符 short events; //指定希望监控的事件类型 short revents; //实际发生的事件类型 } event: POLLIN 读 POLLOUT 写 POLLERR 错误 nfds:fds数组的元素个数(要监控的文件描述符数量) timeout:等待时间发生的最长时间,若为0,立即返回,若为负数,则无限等待 返回值: 成功返回有事件发生的文件描述符数量 失败返回-1 超时返回0
2.实现步骤
将直接实现并发服务器省去点对点聊天步骤
-
准备表
struct pollfd fds[10];
-
添加要监控的文件描述符
int nfds = 0; fds[0].fd = listenfd; fds[0].events = POLL_IN; nfds++;
-
进入循环
- 调用poll函数,传入fds数组,nfds和超时时间
- poll返回后如果返回值大于0,那么说明有文件描述符就绪
- 遍历fds数组,检查每个元素的revents字段是否包含POLL_IN,如果有,则表示该描述符有可读事件
- 若果是listenfd,就调用accept连接,并将connfd添加到fds中,若是其他客户端套接字,说明有数据可读,读取数据并处理
3.代码实现
-
客户端
int main(){ int fd; char buf[256] = {0}; fd = socket(AF_INET,SOCK_STREAM,0); if(fd < 0){ perror("fail to socket"); return -1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } struct pollfd fds[10]; int nfds = 0; fds[0].fd = 0; fds[0].events = POLL_IN; nfds++; fds[1].fd = fd; fds[1].events = POLL_IN; nfds++; char tmp[256] = {0}; int t = 3000; while(1){ int ret = poll(fds,nfds,t); printf("ret = %d\n",ret); if(ret > 0){ int i = 0; for(i = 0;i < nfds;++i){ if(fds[i].revents&POLL_IN){ if(fds[i].fd == 0){ fgets(tmp,sizeof(tmp),stdin); write(fd,tmp,strlen(tmp)); if(strncmp(tmp,"quit",4) == 0){ close(fd); exit(0); } bzero(tmp,sizeof(tmp)); } else{ recv(fds[i].fd,tmp,sizeof(tmp),0); printf("recv data : %s\n",tmp); bzero(tmp,sizeof(tmp)); } } } } } close(fd); return 0; }
-
服务器
int main(void){ int fd; char tmp[256] = {0}; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return 1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int no =1; setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&no,sizeof(int)); if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } if((listen(fd,5)) < 0){ perror("fail to listen"); return -1; } struct sockaddr_in cliaddr; bzero(&cliaddr,0); socklen_t len = sizeof(cliaddr); struct pollfd fds[10]; int nfds = 0; fds[0].fd = fd; fds[0].events = POLL_IN; nfds++; int connfd; int t = 3000; while(1){ int ret = poll(fds,nfds,t); printf("ret = %d\n",ret); if(ret > 0){ int i = 0; for(i = 0;i < nfds;++i){ if(fds[i].revents&POLL_IN){ if(fds[i].fd == fd){ connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); if(connfd < 0){ perror("fail to accept"); return -1; } printf("addr = %s,port = %d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port)); fds[nfds].fd = connfd; fds[nfds].events = POLL_IN; nfds++; } else{ recv(fds[i].fd,tmp,sizeof(tmp),0); printf("tmp = %s\n",tmp); if(strncmp(tmp,"quit",4) == 0){ fds[i].fd = -1; close(fds[i].fd); } } } else{ int newfds = 0; int j = 0; for(j = 0;j < nfds;j++){ if(fds[j].fd >= 0){ fds[newfds].fd = fds[j].fd; newfds++; } } nfds = newfds; } } } } close(fd); return 0; }
4.特点
- 相对于select来说,
- 没有1024的限制,只要系统允许打开足够多的fd,就可以无限加入
- 无需重置集合,events和revents分离
- 拥有更清晰的事件机制
- 效率高,只用遍历传入的数组,不用遍历整个fd范围
- 缺点
- 每次调用仍需将整个fds拷贝到内核
- 返回后仍需遍历所有元素查找就绪fd
- 时间复杂度 O ( n ) O(n) O(n),连接数多时性能下降
4.epoll
- epoll内核维护一个事件表,用户只在有变化时通知内核,而不是每次都将所有描述符从用户态拷贝到内核态,极大减少了数据拷贝和开销
1.相关函数
-
epoll_creat
原型:int epoll_create(int size); 功能:创建一个epoll实例 参数: size:建议的监听文件描述符数量 返回值: 成功返回epoll实例的文件描述符 失败返回-1
-
epoll_ctl
原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 功能:对epoll实例进行控制操作 参数: epfd:epoll_creat返回的epoll文件描述符 op:操作类型 EPOLL_CTL_ADD //添加 EPOLL_CTL_MOD //修改 EPOLL_CTL_DEL //删除 fd:要操作的文件描述符 event:指向epoll_event结构体的指针,指定/返回事件类型 typedef union epoll_data{ void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t; struct epoll_event{ uint32_t events; //指定/返回感兴趣的事件 epoll_data_t data; 存储用户自定义的数据 } events: EPOLLIN 可读 EPOLLOUT 可写 EPOLLERR 出错 EPOLLET 边沿触发 返回值: 成功返回0 失败返回-1
-
epoll_wait
原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 功能:等待epoll实例中注册的事件发生,将就绪事件信息写入events数组 参数: epfd:epoll_creat返回的epoll文件描述符 events:用于保存就绪事件的epoll_event数组 maxevents:events数组的大小 timeout:超时时间 返回值: 成功返回就绪的文件描述符数量 失败返回-1
2.实现步骤
- 创建 epoll 对象:通过 epoll_create 创建一个 epoll 实例,返回一个 epoll 文件描述符。
- 注册/修改/删除事件:通过 epoll_ctl 向 epoll 对象中添加、修改或删除需要监控的文件描述符及其感兴趣的事件(如 EPOLLIN、EPOLLOUT)。
- 等待事件发生:通过 epoll_wait 阻塞等待,直到有文件描述符就绪。返回的数组只包含实际发生事件的 fd,无需遍历所有 fd。
习题
利用epoll实现客户端并发
-
客户端
int add_fd(int fd,int epfd){ struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = fd; if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)){ perror("fail to add fd"); return -1; } return 0; } int del_fd(int fd,int epfd){ if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)){ perror("epoll_ctl del fail"); return -1; } return 0; } int main(){ int fd; char buf[256] = {0}; fd = socket(AF_INET,SOCK_STREAM,0); if(fd < 0){ perror("fail to socket"); return -1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } int epfd = epoll_create(2); add_fd(0,epfd); add_fd(fd,epfd); while(1){ struct epoll_event ret_ev[10]; int ret = epoll_wait(epfd,ret_ev,3,-1); if(ret > 0){ int i = 0; for(i = 0;i < ret;++i){ if(ret_ev[i].data.fd == 0){ fgets(buf,sizeof(buf),stdin); buf[strlen(buf)-1] = '\0'; if(strncmp(buf,"quit",4) == 0){ printf("client quit\n"); del_fd(fd,epfd); close(fd); exit(0); } write(fd,buf,sizeof(buf)); bzero(buf,sizeof(buf)); } else{ read(fd,buf,sizeof(buf)); printf("buf = %s\n",buf); bzero(buf,sizeof(buf)); } } } } close(fd); return 0; }
-
服务器
int add_fd(int fd,int epfd){ struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = fd; if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)){ perror("fail to add fd"); return -1; } return 0; } int del_fd(int fd,int epfd){ if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)){ perror("epoll_ctl del fail"); return -1; } return 0; } int main(void){ int fd; char tmp[256] = {0}; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return 1; } struct sockaddr_in seraddr; bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int no =1; setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&no,sizeof(int)); if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){ perror("fail to connect"); return -1; } if((listen(fd,5)) < 0){ perror("fail to listen"); return -1; } struct sockaddr_in cliaddr; bzero(&cliaddr,0); socklen_t len = sizeof(cliaddr); int epfd = epoll_create(2); add_fd(0,epfd); add_fd(fd,epfd); // 保存所有客户端fd int client_fds[1024] = {0}; int client_count = 0; while(1){ struct epoll_event ret_ev[10]; int ret = epoll_wait(epfd,ret_ev,10,-1); if(ret > 0){ int i = 0; for(i = 0;i < ret;++i){ if(ret_ev[i].data.fd == fd){ int connfd = accept(fd,(struct sockaddr *)&cliaddr,&len); if(connfd < 0){ perror("fail to accept"); return -1; } add_fd(connfd,epfd); client_fds[client_count++] = connfd; printf("addr = %s,port = %d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port)); } else if(ret_ev[i].data.fd == 0) { if(fgets(tmp, sizeof(tmp), stdin) != NULL){ tmp[strcspn(tmp, "\n")] = '\0'; for(int j=0;j<client_count;++j){ if(client_fds[j] > 0){ write(client_fds[j], tmp, strlen(tmp)+1); } } bzero(tmp,sizeof(tmp)); } } else { int cfd = ret_ev[i].data.fd; int n = read(cfd, tmp, sizeof(tmp)); if(strncmp(tmp,"quit",4) == 0 || n <= 0){ printf("client fd %d quit\n", cfd); del_fd(cfd, epfd); close(cfd); int j; for(j=0;j<client_count;++j){ if(client_fds[j] == cfd){ client_fds[j] = 0; break; } } } else { printf("from client %d: %s\n", cfd, tmp); bzero(tmp,sizeof(tmp)); } } } } } close(fd); return 0; }