本文共 12756 字,大约阅读时间需要 42 分钟。
上一篇我们说过了select的代码思路,如果你清楚的话可以不看。但是这个图还是很有必要去了解的,虽然select不建议深入。
图一:
图二select:
我们简单写一下select的实战,服务器的作用是,当客户端连接时,将客户端写过来的内容转成大写发回给客户端。
注意:下面开头部分的函数是封装好出错处理的函数,方便观察逻辑。#include#include #include #include #include #include #include #include #include #define SERV_PORT 6666void perr_exit(const char *s){ perror(s); exit(-1);}int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){ int n;again: if ((n = accept(fd, sa, salenptr)) < 0) { if ((errno == ECONNABORTED) || (errno == EINTR)) goto again; else perr_exit("accept error"); } return n;}int Bind(int fd, const struct sockaddr *sa, socklen_t salen){ int n; if ((n = bind(fd, sa, salen)) < 0) perr_exit("bind error"); return n;}int Connect(int fd, const struct sockaddr *sa, socklen_t salen){ int n; if ((n = connect(fd, sa, salen)) < 0) perr_exit("connect error"); return n;}int Listen(int fd, int backlog){ int n; if ((n = listen(fd, backlog)) < 0) perr_exit("listen error"); return n;}int Socket(int family, int type, int protocol){ int n; if ((n = socket(family, type, protocol)) < 0) perr_exit("socket error"); return n;}ssize_t Read(int fd, void *ptr, size_t nbytes){ ssize_t n;again: if ( (n = read(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n;}ssize_t Write(int fd, const void *ptr, size_t nbytes){ ssize_t n;again: if ( (n = write(fd, ptr, nbytes)) == -1) { if (errno == EINTR) goto again; else return -1; } return n;}int Close(int fd){ int n; if ((n = close(fd)) == -1) perr_exit("close error"); return n;}/*参三: 应该读取的字节数*/ssize_t Readn(int fd, void *vptr, size_t n){ size_t nleft; //usigned int 剩余未读取的字节数 ssize_t nread; //int 实际读到的字节数 char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return n - nleft;}ssize_t Writen(int fd, const void *vptr, size_t n){ size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; ptr += nwritten; } return n;}static ssize_t my_read(int fd, char *ptr){ static int read_cnt; static char *read_ptr; static char read_buf[100]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) goto again; return -1; } else if (read_cnt == 0) return 0; read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1;}ssize_t Readline(int fd, void *vptr, size_t maxlen){ ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n - 1; } else return -1; } *ptr = 0; return n;}int main(){ int nready, i, j, n; int maxfd, lfd, cfd; socklen_t clie_addr_len; char buf[BUFSIZ]; fd_set rset, allset;/* allset为保存所有读事件文件描述符集合,rset只是用来暂存每次调用select后,满足读事件的描述符 */ lfd = socket(AF_INET, SOCK_STREAM, 0);/*参1为协议族,参2为套接字类型,参数默认0即可*/ if(lfd == -1){ perr_exit("socket failed."); } /*sockaddr_in以0填充8个字节,变成14字节的结构体,方便与下面的sockaddr结果转换, 两者实际是一样的,只不过我们使用sockaddr_in时有ip和端口具体成员,而sockaddr没有,方便我们赋值*/ struct sockaddr_in clie_addr, serv_addr; bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family= AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port= htons(SERV_PORT); int ret = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));/*参2为sockaddr类型的服务器地址*/ if(ret == -1){ perr_exit("bind failed."); } if(listen(lfd, 128) == -1){ /*允许连接的最大数,并非字面意思监听,监听实际上是下面的accept监听*/ perr_exit("listen failed."); } /*先将lfd添加到allset读事件集合,这样才能监听到客户端的连接请求和通信请求*/ FD_ZERO(&allset); FD_SET(lfd, &allset); maxfd = lfd; /* while里完完全全就是维护allset和maxfd,并且while内只处理lfd和cfd请求的事情 */ while (1) { rset = allset; /* 满足读事件的使用rset,allset只保存所有文件描述符集合,注意select与open时不一样,open默认会占3个描述符,所以只有1021可用,而select有1024 */ nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (nready < 0){ /* select的返回一般只用于判断出错 */ perr_exit("select error."); } //处理客户端请求连接的事情 if (FD_ISSET(lfd, &rset)) { clie_addr_len = sizeof(clie_addr); cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */ FD_SET(cfd, &allset); /* 向保存所有文件描述符集合的allset添加新的文件描述符cfd,它像open文件描述符时一样,默认插入(open为分配)到下标最小的位图位置,以至于当第一次超过1023(因为0-1023)后,cfd不会一直被插入到allset的末尾而覆盖正在通信的描述符 */ if (cfd > maxfd){ /* 更新maxfd,防止下一次select后遍历不完整导致出错,并且select第一个参数需要 */ maxfd = cfd; } if (nready == 1){ /* 只有lfd不需要执行下面的通信cfd的事情 */ continue; } /* 至此这里处理完lfd连接请求的事情 */ } /* 处理客户端请求通信的事情 */ for (i = lfd + 1; i <= maxfd; i++) { /* 遍历所有文件描述符,判断是否在传出rset请求的集合中 */ if (FD_ISSET(i, &rset)) { if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */ Close(i); FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */ } else if (n > 0) { for (j = 0; j < n; j++){ buf[j] = toupper(buf[j]); /* 转大写 */ } Write(i, buf, n); /* 写回给客户端并接着在下面打印到终端STDOUT_FILENO */ Write(STDOUT_FILENO, buf, n); } } } } Close(lfd); return 0;}
上面程序结果可以看到,我们使用nc模拟多个客户端连接同一个服务器,是可以同时通信的,能够将发送过去的小写转成了大写,解决了我们开头所讲一个accept只能与一个客户端连接的弊端,这就是selectIO复用。
3.1 select的优缺点
缺点: -1)监听上限受文件描述符限制,最大1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。需要自己检查满足条件的描述符,并且allset只有两个描述符,一个是lfd,另一个为最大的cfd即maxfd为1023时,每次都要轮询一千多次,效率非常低下。业务逻辑可提高性不高,并且增加代码难度。优点:
3.2 poll的优缺点
缺点: -1)不能跨平台,只能在linux(unix)运行,无法直接定位满足监听事件的文件描述符。优点:
3.3 epoll的优缺点
缺点: -1)不能跨平台,只能在linux(unix)运行。优点:
关于对select添加数组的总结。首先不建议看下面的代码,作用不大,但个人因为好奇还是实践了一遍。
看下面的总结需要了解:allrset是保存所有文件描述符的集合,rset是每次传出的集合,client是保存所有文件描述符的数组,maxi是保存数组最后元素的下标。maxfd是已有连接文件描述符最大值。上面就是我个人在手写select数组时遇到的问题和总结,再次强调一遍,这个select添加数组例子完全可以不用看,浪费脑力和精力,并且毛用没有。直接用上面不加数组的例子,自己添加一个–nready的判断比下面的代码更加好。
/* 部分函数和宏在上面的例子,由于一样就不复制了,避免篇幅过长。 */int main(){ int nready, i, j, n; int maxfd, lfd, cfd,tmpfd; int maxi; //用于保存数组最后元素的下标,即数组最后的文件描述符下标 socklen_t clie_addr_len; char buf[FD_SETSIZE], client[FD_SETSIZE], str[INET_ADDRSTRLEN]={ 0}; /* FD_SETSIZE为系统默认的1024大小 */ fd_set rset, allset;/* allset为保存所有读事件文件描述符集合,rset只是用来暂存每次调用select后,满足读事件的描述符 */ lfd = socket(AF_INET, SOCK_STREAM, 0);/*参1为协议族,参2为套接字类型,参数默认0即可*/ if(lfd == -1){ perr_exit("socket failed."); } int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));/* 端口复用 */ /*sockaddr_in以0填充8个字节,变成14字节的结构体,方便与下面的sockaddr结果转换, 两者实际是一样的,只不过我们使用sockaddr_in时有ip和端口具体成员,而sockaddr没有,方便我们赋值*/ struct sockaddr_in clie_addr, serv_addr; bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family= AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port= htons(SERV_PORT); int ret = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));/*参2为sockaddr类型的服务器地址*/ if(ret == -1){ perr_exit("bind failed."); } if(listen(lfd, 128) == -1){ /*允许连接的最大数,并非字面意思监听,监听实际上是下面的accept监听*/ perr_exit("listen failed."); } /*先将lfd添加到allset读事件集合,这样才能监听到客户端的连接请求和通信请求*/ FD_ZERO(&allset); FD_SET(lfd, &allset); maxfd = lfd; printf("lfd===========%d\n", lfd);//3 //初始化数组 for(i = 0;i < FD_SETSIZE; i++){ client[i] = -1;//默认-1,你也可以是其它,但不能是0-1023 } maxi = -1;//数组最后元素下标默认值 /* while里完完全全就是维护allset和maxfd,并且while内只处理lfd和cfd请求的事情 */ while (1) { rset = allset; /* 满足读事件的使用rset,allset只保存所有文件描述符集合,注意select与open时不一样,open默认会占3个描述符,所以只有1021可用,而select有1024 */ nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (nready < 0){ /* select的返回一般只用于判断出错 */ perr_exit("select error."); } //处理客户端请求连接的事情 if (FD_ISSET(lfd, &rset)) { clie_addr_len = sizeof(clie_addr); cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */ printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port)); FD_SET(cfd, &allset);/* 向保存所有文件描述符集合的allset添加新的文件描述符cfd,它像open文件描述符时一样,默认插入(open为分配)到下标最小的位图位置,以至于当第一次超过1023(因为0-1023)后,cfd不会一直被插入到allset的末尾而覆盖正在通信的描述符 */ if (cfd > maxfd){ /* 更新maxfd,防止下一次select后遍历不完整导致出错,并且select第一个参数需要 */ maxfd = cfd; } for(i = 0; i < FD_SETSIZE; i++){ /*往数组添加文件描述符*/ if(client[i] == -1){ client[i] = cfd; break; } } if(i > maxi){ /*更新数组最后元素的下标*/ maxi = i; } if(i >= FD_SETSIZE){ /* 连接超过1024程序退出 */ perr_exit("connect too much,exit."); } if (--nready == 0){ /* 只有lfd不需要执行下面的通信cfd的事情 */ continue; } /* 至此这里处理完lfd连接请求的事情 */ } /* 处理客户端请求通信的事情 */ //下面是没加数组的例子,方便对比 // for (i = lfd + 1; i <= maxfd; i++) { /* 遍历所有文件描述符,判断是否在传出rset请求的集合中 */ // if (FD_ISSET(i, &rset)) { // if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */ // Close(i); // FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */ // } else if (n > 0) { // for (j = 0; j < n; j++){ // buf[j] = toupper(buf[j]); /* 转大写 */ // } // Write(i, buf, n); /* 写回给客户端并接着在下面打印到终端STDOUT_FILENO */ // Write(STDOUT_FILENO, buf, n); // } // } // } /* 使用数组下标代替最大文件描述符的值遍历 */ for(i = 0; i <= maxi; i++){ if((tmpfd = client[i]) == -1){ /*这一步实际上可以不要,因为FD_ISSET(i, &rset)也判断了,作用是一样的,但是我们需要获取数组的描述符用于判断是否在集合才加上,你也可以单独获取*/ continue; } //if ((tmpfd = client[i]) != -1) {/* 注意不能使用数组判断,若使用数组判断则会使--nready失效,即没办法使用select的返回值判断传出的cfd,变成每次都是循环遍历数组的所有元素了,应该使用FD_ISSET */ if(FD_ISSET(tmpfd, &rset)){ if ((n = Read(tmpfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */ Close(tmpfd); FD_CLR(tmpfd, &allset); /* 解除select对此文件描述符的监控 */ client[i] = -1; //maxi-=1;//不能减1,因为当数组中间某个描述符关闭而不是末尾关闭的话,会导致最后一个下标的描述符失去检索,但实际上仍可能在通信 } else if (n > 0) { for (j = 0; j < n; j++){ buf[j] = toupper(buf[j]); /* 转大写 */ } Write(tmpfd, buf, n); /* 写回给客户端并接着在下面打印到终端STDOUT_FILENO */ Write(STDOUT_FILENO, buf, n); } /* 不能写在if外面,否则当有两个描述符时,并且第二个是单独通信时,由于遍历第一个时外部if减减,导致满足条件,所以for跳出导致第二无法Read通信 */ if(--nready == 0){ break;//只有一个cfd则跳出本次for,注意不是while } } } } Close(lfd); return 0;}
结果,我开了五个客户端,每个客户端都能正常通信,然后将中间3,4关闭连接,连接通信也是正常的,说明代码没问题。
转载地址:http://hpfv.baihongyu.com/