Linux网络编程三、 IO操作
当从一个文件描述符进行读写操作时,accept、read、write这些函数会阻塞I/O。在这种会阻塞I/O的操作好处是不会占用cpu宝贵的时间片,但是如果需要对多个描述符操作时,阻塞会使同一时刻只能处理一个操作,从而使程序的执行效率大大降低。一种解决办法是使用多线程或多进程操作,但是这浪费大量的资源。另一种解决办法是采用非阻塞、忙轮询,这种办法提高了程序的执行效率,缺点是需要占用更多的cpu和系统资源。所以,最终的解决办法是采用IO多路转接技术。
IO多路转接是先构造一个关于文件描述符的列表,将要监听的描述符添加到这个列表中。然后调用一个阻塞函数用来监听这个表中的文件描述符,直到这个表中有描述符要进行IO操作时,这个函数返回给进程有哪些描述符要进行操作。从而使一个进程能完成对多个描述符的操作。而函数对描述符的检测操作都是由系统内核完成的。
linux下常用的IO转接技术有:select、poll和epoll。
select:
头文件:#include <sys/select.h>、#include <sys/time.h>、#include <sys/types.h>、#include <unistd.h>
函数:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:要检测的文件描述符中最大的fd+1,nfds最大值为1024。select最多只能检测1024个文件描述符。
readfds:读集合。读缓冲区中有数据时,readfds写入数据。fd_set文件描述符集类型,具体实现见下面。
writefds:写集合。通常设为NULL。
exceptfds:异常集合。通常设为NULL。
timeout:设置超时返回。为NULL时只有检测到fd变化时返回。struct timeval a; a.tv_sec=10; a.tv_usec=0;
返回值:成功返回要操作的描述符个数,超时返回0,失败返回-1。
select最多只能检测1024个文件描述符,是由于fd_set在内核代码中的设置所限制
1 //部分fd_set的内核代码 2 3 #define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS) 4 #define __FD_SETSIZE 1024 5 #define __NFDBITS (8 * sizeof(unsigned long)) 6 typedef __kernel_fd_set fd_set; 7 typedef struct { 8 unsigned long fds_bits [__FDSET_LONGS]; 9 } __kernel_fd_set;
void FD_CLR(int fd, fd_set *set); 从set集合中删除文件描述符fd。
int FD_ISSET(int fd, fd_set *set); 判断文件描述符fd是否在set集合中。
void FD_SET(int fd, fd_set *set); 将fd添加到set集合中。
void FD_ZERO(fd_set *set); 清空set集合。
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <sys/socket.h> 5 #include <arpa/inet.h> 6 #include <string.h> 7 #include <unistd.h> 8 #include <sys/select.h> 9 #include <sys/time.h> 10 #include <stdlib.h> 11 int main() 12 { 13 int fd=socket(AF_INET,SOCK_STREAM,0); 14 struct sockaddr_in serv; 15 memset(&serv,0,sizeof(serv)); 16 serv.sin_addr.s_addr=htonl(INADDR_ANY); 17 serv.sin_port=htons(8888); 18 serv.sin_family=AF_INET; 19 bind(fd,(struct sockaddr*)&serv,sizeof(serv)); 20 21 listen(fd,20); 22 23 struct sockaddr_in client; 24 socklen_t cli_len=sizeof(client); 25 int maxfd=fd; 26 fd_set reads, temp; 27 FD_ZERO(&reads); 28 FD_SET(fd,&reads); 29 while(1) 30 { 31 temp=reads; 32 int ret=select(maxfd+1,&temp,NULL,NULL,NULL); 33 if(-1==ret) 34 { 35 perror("select error"); 36 exit(1); 37 } 38 //客户端发起连接 39 if(FD_ISSET(fd,&temp)) 40 { 41 //接受连接 42 int cfd=accept(fd,(struct sockaddr*)&client,&cli_len); 43 if(cfd==-1) 44 { 45 perror("accept error"); 46 exit(1); 47 } 48 FD_SET(cfd,&reads); 49 //更新最大文件描述符 50 maxfd=maxfd<cfd?cfd:maxfd; 51 52 } 53 for(int i=fd+1;i<=maxfd;++i) 54 { 55 if(FD_ISSET(i,&temp)) 56 { 57 char buf[1024]={0}; 58 int len=recv(i,buf,sizeof(buf),0); 59 if(len==-1) 60 { 61 perror("recv error"); 62 exit(1); 63 64 } 65 else if(len==0) 66 { 67 printf("客户端断开连接\n"); 68 close(i); 69 70 FD_CLR(i,&reads); 71 } 72 else 73 { 74 printf("recv buf: %s\n",buf); 75 send(i,buf,strlen(buf)+1,0); 76 } 77 } 78 } 79 } 80 close(fd); 81 return 0; 82 }
poll:
头文件:#include <poll.h>
函数:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:数组地址。内核检测fds中的文件描述符。
nfds:数组的最大长度,数组中最后有效元素的下标+1。
timeout:超时返回,-1永久阻塞,0不阻塞调用后立即返回,>0等待的时长,单位毫秒。
返回值:成功返回要操作的个数,失败返回-1。
struct pollfd { int fd; /*文件描述符*/ short events; /*等待的事件*/ short revents; /*实际发生的事件,内核给的反馈*/ }
pollfd常用事件:读事件,POLLIN;写事件,POLLOUT;错误事件,POLLERR(不能作为events的值);
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <string.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <ctype.h> 9 #include <poll.h> 10 11 #define SERV_PORT 8989 12 13 int main(int argc, const char* argv[]) 14 { 15 int lfd, cfd; 16 struct sockaddr_in serv_addr, clien_addr; 17 int serv_len, clien_len; 18 19 // 创建套接字 20 lfd = socket(AF_INET, SOCK_STREAM, 0); 21 // 初始化服务器 sockaddr_in 22 memset(&serv_addr, 0, sizeof(serv_addr)); 23 serv_addr.sin_family = AF_INET; // 地址族 24 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP 25 serv_addr.sin_port = htons(SERV_PORT); // 设置端口 26 serv_len = sizeof(serv_addr); 27 // 绑定IP和端口 28 bind(lfd, (struct sockaddr*)&serv_addr, serv_len); 29 30 // 设置同时监听的最大个数 31 listen(lfd, 36); 32 printf("Start accept ......\n"); 33 34 // poll结构体 35 struct pollfd allfd[1024]; 36 int max_index = 0; 37 // init 38 for(int i=0; i<1024; ++i) 39 { 40 allfd[i].fd = -1; 41 } 42 allfd[0].fd = lfd; 43 allfd[0].events = POLLIN; 44 45 while(1) 46 { 47 int i = 0; 48 int ret = poll(allfd, max_index+1, -1); 49 if(ret == -1) 50 { 51 perror("poll error"); 52 exit(1); 53 } 54 55 // 判断是否有连接请求 56 if(allfd[0].revents & POLLIN) 57 { 58 clien_len = sizeof(clien_addr); 59 // 接受连接请求 60 int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len); 61 printf("============\n"); 62 63 // cfd添加到poll数组 64 for(i=0; i<1024; ++i) 65 { 66 if(allfd[i].fd == -1) 67 { 68 allfd[i].fd = cfd; 69 break; 70 } 71 } 72 // 更新最后一个元素的下标 73 max_index = max_index < i ? i : max_index; 74 } 75 76 // 遍历数组 77 for(i=1; i<=max_index; ++i) 78 { 79 int fd = allfd[i].fd; 80 if(fd == -1) 81 { 82 continue; 83 } 84 if(allfd[i].revents & POLLIN) 85 { 86 // 接受数据 87 char buf[1024] = {0}; 88 int len = recv(fd, buf, sizeof(buf), 0); 89 if(len == -1) 90 { 91 perror("recv error"); 92 exit(1); 93 } 94 else if(len == 0) 95 { 96 allfd[i].fd = -1; 97 close(fd); 98 printf("客户端已经主动断开连接。。。\n"); 99 } 100 else 101 { 102 printf("recv buf = %s\n", buf); 103 for(int k=0; k<len; ++k) 104 { 105 buf[k] = toupper(buf[k]); 106 } 107 printf("buf toupper: %s\n", buf); 108 send(fd, buf, strlen(buf)+1, 0); 109 } 110 111 } 112 113 } 114 } 115 116 close(lfd); 117 return 0; 118 }
select和poll虽然没有前面几种方法的缺点,但是select和poll只返回个数,不会告诉进程具体是哪几个描述符要操作, 而且select和poll最多只能检测1024个。select每次调用时,都需要把fd集合从用户态和内核态之间相互拷贝,这在fd很多时会消耗大量资源。
epoll检测的个数没有限制,它在内部构造维护了红黑树,减少了资源的消耗。
epoll:
头文件:#include <sys/epoll.h>
函数:
int epoll_create(int size); 生成epoll专用的文件描述符,size:epoll上能关注的最大描述符个数。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create生成的文件描述符。
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); 等待IO事件发生,可以设置阻塞。
epfd:要检测的句柄。
events:回传待处理的数组。
maxevents:events的大小。
timeout:超时返回。-1永久阻塞;0立即返回;>0超时时间。
1 typedef union epoll_data { 2 void *ptr; 3 int fd; 4 uint32_t u32; 5 uint64_t u64; 6 } epoll_data_t; 7 8 struct epoll_event { 9 uint32_t events; /* Epoll events */ 10 epoll_data_t data; /* User data variable */ 11 };
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <string.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <ctype.h> 9 #include <sys/epoll.h> 10 11 12 int main(int argc, const char* argv[]) 13 { 14 if(argc < 2) 15 { 16 printf("eg: ./a.out port\n"); 17 exit(1); 18 } 19 struct sockaddr_in serv_addr; 20 socklen_t serv_len = sizeof(serv_addr); 21 int port = atoi(argv[1]); 22 23 // 创建套接字 24 int lfd = socket(AF_INET, SOCK_STREAM, 0); 25 // 初始化服务器 sockaddr_in 26 memset(&serv_addr, 0, serv_len); 27 serv_addr.sin_family = AF_INET; // 地址族 28 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP 29 serv_addr.sin_port = htons(port); // 设置端口 30 // 绑定IP和端口 31 bind(lfd, (struct sockaddr*)&serv_addr, serv_len); 32 33 // 设置同时监听的最大个数 34 listen(lfd, 36); 35 printf("Start accept ......\n"); 36 37 struct sockaddr_in client_addr; 38 socklen_t cli_len = sizeof(client_addr); 39 40 // 创建epoll树根节点 41 int epfd = epoll_create(2000); 42 // 初始化epoll树 43 struct epoll_event ev; 44 ev.events = EPOLLIN; 45 ev.data.fd = lfd; 46 epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); 47 48 struct epoll_event all[2000]; 49 while(1) 50 { 51 // 使用epoll通知内核fd 文件IO检测 52 int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1); 53 54 // 遍历all数组中的前ret个元素 55 for(int i=0; i<ret; ++i) 56 { 57 int fd = all[i].data.fd; 58 // 判断是否有新连接 59 if(fd == lfd) 60 { 61 // 接受连接请求 62 int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len); 63 if(cfd == -1) 64 { 65 perror("accept error"); 66 exit(1); 67 } 68 // 将新得到的cfd挂到树上 69 struct epoll_event temp; 70 temp.events = EPOLLIN; 71 temp.data.fd = cfd; 72 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp); 73 74 // 打印客户端信息 75 char ip[64] = {0}; 76 printf("New Client IP: %s, Port: %d\n", 77 inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)), 78 ntohs(client_addr.sin_port)); 79 80 } 81 else 82 { 83 // 处理已经连接的客户端发送过来的数据 84 if(!all[i].events & EPOLLIN) 85 { 86 continue; 87 } 88 89 // 读数据 90 char buf[1024] = {0}; 91 int len = recv(fd, buf, sizeof(buf), 0); 92 if(len == -1) 93 { 94 perror("recv error"); 95 exit(1); 96 } 97 else if(len == 0) 98 { 99 printf("client disconnected ....\n"); 100 // fd从epoll树上删除 101 ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); 102 if(ret == -1) 103 { 104 perror("epoll_ctl - del error"); 105 exit(1); 106 } 107 close(fd); 108 109 } 110 else 111 { 112 printf(" recv buf: %s\n", buf); 113 write(fd, buf, len); 114 } 115 } 116 } 117 } 118 119 close(lfd); 120 return 0; 121 }
epoll三种工作模式:
水平触发:epoll默认工作模式,只要fd对应的缓冲区有数据,epoll_wait就会返回。epoll_wait调用次数越多,系统开销越大。
边沿触发:fd默认是阻塞的,客户端发送一次数据epoll_wait就返回一次,不管数据是否读完。如果要读完数据,可以循环读取,但是recv会阻塞,解决方法是将fd设置为非阻塞。
边沿非阻塞触发:将fd设置为非阻塞(open下设置O_NONBLOCK,或者利用fcntl()函数)。效率最高,可以将缓冲区数据完全读完。