什么是多路转接?什么是select? 多路转接就是一次等待多个文件描述符; 简单来说,select只做一件事,那就是等,等至少一个文件描述符的读写时间就绪。
具体来说,系统提供select来实现多路复用输入/输出模型。 select系统调用可以让程序监听多个文件描述符的状态变化。 程序会在select在这里等待,直到被监视的文件描述符至少有一个发生了状态改变。
select函数声明
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);参数解释:
nfds:需要监视的最大文件描述符的值+1readfds:只关心写事件,可读文件描述符的集合writefds:只关心写事件,可写文件描述符的集合exceptfds:只关心异常事件,异常文件描述符的集合参数timeout为结构timeval,用来设置select()的等待时间fd_set的结构 fd_set就是一个整形数组,更严格的说,一个“位图”,位图中的位置代表对应的文件描述符,用0或1来控制。 但是,我们不能直接操作fd_set,而是要调用函数;
void FD_ZERO(fd_set& set); //清楚set的全部位 void FD_SET(int fd, fd_set& set) //把fd设置进set的相关位 void FD_CLR(int fd, fd_set& set) //把fd在set的相关位清楚 int FD_ISSET(int fd, fd_set& set); //判断fd是否被设置相关位fd_set作为输入输出型参数:
输入时:用户想告诉内核,让OS帮用户关心那些文件描述符输出时:内核告诉用户,那些文件描述符已就绪timeout:用来设置等待时间,取值为: NULL:表示select一直阻塞,知道某个文件描述符发生了时间; 0:非阻塞,不等待外部事件发生。 特定时间:如果在指定时间内没有时间发生,将立刻超时返回。
timeval的结构:
struct timeval { _time_t tv_sec;//秒数 _suseconds_t tv_usec;//毫秒 };返回值解释: 执行成功则返回已经改变状态的文件描述符的个数; 返回0则表示在描述符状态改变前已超过timeout超时,没有返回; 返回-1表示发生了错误,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测;
错误值可能为:
EBADF 文件描述符无效或该文件描述符已关闭EINTR 此调用被信号中断EINVAL 参数n为负值ENOMEM 核心内存不足socket就绪条件
读就绪sochet内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RECVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0 socket TCP通信中,对端关闭连接,此时对该socket读,则返回0 监听的socket上有新的连接请求时 socket上有未处理的错误时
写就绪socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记,SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0 socket的写操作被关闭(close或者shutdown),对于一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号 socket使用非阻塞connect连接成功或失败之后 socket上有未读取的错误
异常就绪socket上收到带外数据(关于带外数据,和TCP紧急模式相关,在TCP报头中有一个紧急指针的字段)
简述select的执行过程 为了方便说明,假设fd_set的长度为1字节,则一字节最大可以对应8个文件描述符。 1、fd_set set; FD_ZERO(&set); 此时set为0000 0000; 2、若fd=5,执行FD_SET(&set);此时set变为0001 0000 (第5位置为1); 3、在加上 fd=1 和 fd=2 文件描述符,则set变为0001 0011; 4、执行select(6, &set, 0, 0,0) 阻塞等待; 5、若 fd=1 和 fd=2 发生了可读事件,则select返回,此时set变为0000 0011。注意:因为 fd=5 没有发生事件,所以对应位置被清空;
编写select代码 一、检测标准输入:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/time.h> 4 #include<sys/types.h> 5 int main() 6 { 7 //将0(标准输入)设置进read_fds 8 fd_set read_fds; 9 FD_ZERO(&read_fds); 10 FD_SET(0, &read_fds); 11 12 while(1){ 13 printf("> "); 14 fflush(stdout); 15 16 //最大文件描述符为0,只关心读事件,阻塞等待 17 int ret = select(1, &read_fds, NULL, NULL, NULL); 18 if(ret < 0){ 19 perror("select error\n"); 20 continue; 21 } 22 if(FD_ISSET(0, &read_fds)){//若标准输入就绪 23 char buf[1024] = {0}; 24 read(0, buf, sizeof(buf)-1); 25 printf("input:%s\n", buf); 26 } 27 else{ 28 continue; 29 } 30 FD_ZERO(&read_fds); 31 FD_SET(0, &read_fds); 32 } 33 return 0; 34 }此时的运行的效果为,找到在标准输入上输入之前,select都会一直阻塞等待。 二、模拟实现select服务器 服务器
#include<stdio.h> 2 #include<sys/socket.h> 3 #include<unistd.h> 4 #include<sys/time.h> 5 #include<sys/types.h> 6 #include<stdlib.h> 7 #include<arpa/inet.h> 8 #include<netinet/in.h> 9 10 int main(int argc, char* argv[]) 11 { 12 //建立监听套接字 13 if(argc != 3){ 14 printf("./server [ip] [port]\n"); 15 return 1; 16 } 17 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); 18 if(listen_sock < 0){ 19 perror("socker error\n"); 20 return 2; 21 } 22 struct sockaddr_in local; 23 local.sin_family = AF_INET; 24 local.sin_addr.s_addr = inet_addr(argv[1]); 25 local.sin_port = htons(atoi(argv[2])); 26 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ 27 perror("bind error\n"); 28 return 3; 29 } 30 if(listen(listen_sock, 5) < 0){ 31 perror("listen error\n"); 32 return 4; 33 } 35 //数组的大小就是fd_set这个位图能够存多少个fd,因为需要用数组记录 36 int fdArray[sizeof(fd_set)*8]; 37 38 //把listen_sock给数组的第一个元素 39 fdArray[0] = listen_sock; 40 41 int num = sizeof(fdArray)/sizeof(fdArray[0]);//最多存在1024个fd 42 43 //然后初始化数组为-1 44 for(int i=0; i<num; i++){ 45 fdArray[i] = -1; 46 } 47 48 while(1){ 49 fd_set rfds;//建立关心读位图 50 FD_ZERO(&rfds);//初始化位图 51 int maxfd = fdArray[0];//当前的最大fd为listen_sock 52 53 for(int i=0; i<num; i++){ 54 if(fdArray[i] >= 0){ 55 FD_SET(fdArray[i], &rfds); 56 //更新最大的fd 57 if(maxfd < fdArray[i]) 58 maxfd = fdArray[i]; 59 } 60 } 61 switch(select(maxfd+1, &rfds, NULL, NULL, NULL)){ 62 case 0: 63 printf("超时!\n"); 64 break; 65 case -1: 66 printf("出错!\n"); 67 break; 68 default: 69 { 70 for(int i=0; i<num; i++){ 71 if(fdArray[i] == -1) 72 continue; 73 //若fd已经就绪 74 if(FD_ISSET(fdArray[i], &rfds)){ 75 //listen_sock就绪 76 if(FD_ISSET(fdArray[i], &rfds) && i==0){ 77 //建立连接 78 struct sockaddr_in client; 79 socklen_t len = sizeof(client); 80 int new_sock = accept(listen_sock, (struct s 81 if(new_sock < 0){ 82 perror("accept error!\n"); 83 return 5; 84 } 85 //遇见-1就说明处理完了 86 for(int i=0; i<num; i++){ 87 if(fdArray[i] == -1) 88 break; 89 } 90 //new_sock必须在范围之内 91 if(i < num) 92 fdArray[i] = new_sock; 93 else 94 continue; 95 } 96 } 97 //普通fd就绪 98 if(FD_ISSET(fdArray[i], &rfds)){ 99 char buf[1024]; 100 ssize_t s = read(fdArray[i], buf, sizeof(buf 101 if(s > 0){ 102 buf[s] = 0; 103 printf("client#%s\n", buf); 104 } 105 else if(s == 0){ 106 printf("client quit!\n"); 107 close(fdArray[i]); 108 fdArray[i] = -1; 109 } 110 else{ 111 break; 112 } 113 } 114 } 115 } 116 } 117 } 118 return 0; 119 }客户端:
#include<sys/types.h> 5 #include<stdlib.h> 6 #include<arpa/inet.h> 7 #include<netinet/in.h> 8 #include<string.h> 9 10 int main(int argc, char* argv[]) 11 { 12 if(argc != 3){ 13 printf("./client [ip] [port]\n"); 14 return 1; 15 } 16 struct sockaddr_in server; 17 server.sin_family = AF_INET; 18 server.sin_addr.s_addr = inet_addr(argv[1]); 19 server.sin_port = htons(atoi(argv[2])); 20 21 int fd = socket(AF_INET, SOCK_STREAM, 0); 22 if(fd < 0){ 23 perror("socket error\n"); 24 return 2; 25 } 26 27 int ret = connect(fd, (struct sockaddr*)&server, sizeof(server)); 28 if(ret < 0){ 29 perror("connect error\n"); 30 return 3; 31 } 32 33 while(1){ 34 printf("> "); 35 fflush(stdout); 36 char buf[1024] = {0}; 37 read(0, buf, sizeof(buf)-1); 38 39 ssize_t s = write(fd, buf, strlen(buf)); 40 if(s < 0){ 41 perror("write error\n"); 42 return 4; 43 } 44 } 45 close(fd); 46 return 0; 47 }select的特点
可监控的文件描述符个数取决于sizeof(fd_set)的值,在我的服务器上sizeof(fd_set) = 128,每bit表示一个文件描述符,则我的服务器上支持的最大文件描述符个数是128*8 = 1024将fd加入select监控集的同时,还需要在使用一个数据结构array保存放到select监控集中的fd一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断 二是select返回后会把以前加入的但并没有事件发生的fd清空,则每次开始 select 前都要重新从array中取得fd逐一加入,扫描array的同时取得fd的最大值maxfd,用于select的第一个参数
select的缺点
每次调用select,都需要手动设置fd集合,从接口使用来说非常不方便每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大select支持的文件描述符有上限