博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络编程之IO复用机制(多路IO转接)之select实现IO复用的代码实现03
阅读量:235 次
发布时间:2019-03-01

本文共 12756 字,大约阅读时间需要 42 分钟。

1 思路图

上一篇我们说过了select的代码思路,如果你清楚的话可以不看。但是这个图还是很有必要去了解的,虽然select不建议深入。

  • 1)我们先看图一,图一是我们调用accept去进行通信的最简单的例子。我们知道,accept函数每一次只能与一个客户端进行建立连接,要想与多个客户端进行连接,必须开启多个server,所以这是不符合我们的需求的。
  • 2)所以,伟大的程序员们就想到了IO复用机制即select,poll,epoll去借助内核帮我们实现一个server与多个客户端连接并进行通信。本节只讲select。看图二,我们将accept函数替换成了select,select是一个非阻塞的函数,可以设置超时返回。它的作用是:将预先创建好的lfd监听想要连接的客户端和想要通信的客户端,然后进行对应操作。
  • 3)这里强调一点,select只需要监听读事件集合即可。因为相对于服务器,你客户端不管请求连接还是请求通信(即写内容到套接字的缓冲中),针对于服务器都是读事件,所以服务器只需要读事件集合即可完成想要的读写操作返回给客户端。而写事件其实意义不大,因为针对于服务器,你想写就写,不需要什么条件来满足才能写,这个写事件可以参考libevent库,它对写事件也是实用性不大,所以这个事件只需要传NULL即可,无需了解。而异常事件一般是select借助内核时可能触发的一些异常,这里我们也不需要理,因为发生异常你也搞不来,传NULL即可。

图一:

在这里插入图片描述

图二select:

在这里插入图片描述

2 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 select,poll,epoll的优缺点

3.1 select的优缺点

缺点:
-1)监听上限受文件描述符限制,最大1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。需要自己检查满足条件的描述符,并且allset只有两个描述符,一个是lfd,另一个为最大的cfd即maxfd为1023时,每次都要轮询一千多次,效率非常低下。业务逻辑可提高性不高,并且增加代码难度。

优点:

  • 1)跨平台,win,linux,macos,unix,类unix,mips都能运行。

3.2 poll的优缺点

缺点:
-1)不能跨平台,只能在linux(unix)运行,无法直接定位满足监听事件的文件描述符。

优点:

  • 1)自带数组结构(参1),可以将监听事件集合和返回的事件集合分离,可以超出1024的上限。

3.3 epoll的优缺点

缺点:
-1)不能跨平台,只能在linux(unix)运行。

优点:

  • 1)高效,编码简单。

4 添加数组后的select

关于对select添加数组的总结。首先不建议看下面的代码,作用不大,但个人因为好奇还是实践了一遍。

看下面的总结需要了解:allrset是保存所有文件描述符的集合,rset是每次传出的集合,client是保存所有文件描述符的数组,maxi是保存数组最后元素的下标。maxfd是已有连接文件描述符最大值。

  • 1)该例子是我从某视频中看到的,它说添加数组后对比上面效率会提高。但是个人经过多次猜想,实际并未能增加效率,而且还因增加了数组循环,效率变低了。原因就是:由于文件描述符创建时每次都会创建最小值,就导致不会出现rset返回1023时数组元素下标值为1,该下标必定是指向数组最后元素1023的位置,即1019(不懂往后看),并且除开allrset已有的lfd不说,allrset集合和client数组内的元素个数关系必定是n和n-1(看代码可以看出,两者都是同时增加和减少的),这样就导致maxfd和maxi的关系为maxfd=maxi+4,即遍历maxfd和maxi的情况不会差太大,效率也提高不大。例如有cfd=4,5连进来,那么此时数组元素有4和5,下标maxi=1,maaxfd=5。即使出现这种极端情况:当4-1023所有连接都连进来,那么此时数组元素有4-1023共有1020个,然后4-1022全部断开连接,此时maxi仍是1019指向最后一个元素,maxfd也是1023,遍历的次数并未优化多大。并且因Accept添加了数组的for而导致效率下降。不管怎样,最后的结果是select添加数组并无卵用,废物一个,浪费研究的时间,实在想要使用select使用上面的例子即可,差别不大。
  • 2)但是针对与上面代码,下面的代码在–nready这一步确实是优化了,当它执行完满足的读事件后就会break退出for,进行下一次监听。但注意这一点并非是由于添加数组而优化的,是因为上面没有加上这个语句而已,所以你在上面加上也可以。
  • 3)注意,select的参数1是最大文件描述符+1,并且开头的lfd是3,所以select的参1起始值实际是4。具体这样传有可能是select底层遍历时的循环因为是3开始(跳过系统默认的描述符),需要观察源代码才能确认,但没必要,知道这样传参即可。
  • 4)client数组开头是不需要添加lfd描述符的,因为它的作用只是保存所有的cfd描述符而已。
  • 5)在遍历cfd时,不能使用if ((tmpfd = client[i]) != -1)来判断,若使用数组判断则会使–nready失效,即没办法使用select的返回值判断传出的cfd,变成每次都是循环遍历数组的所有元素了,应该使用FD_ISSET。
  • 6)客户端关闭时maxi不能减1,因为当数组中间某个描述符关闭而不是末尾关闭的话,会导致最后一个下标的描述符失去检索,但实际上仍可能在通信。
  • 7)客户端的cfd通信时,–nready不能写在if外面,否则当有两个描述符时,并且第二个是单独通信时,由于遍历第一个时外部if减减,导致满足条件,所以for跳出导致第二无法Read通信。
  • 8)不建议将–ready == 0写成nready == 1,这样会失去当nready不是等于1时而尽快跳出for的机会。导致效率降低。

上面就是我个人在手写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/

你可能感兴趣的文章