IO多路转接—select,并且实现select版本的TCP服务器

xiaoxiao2021-02-28  54

下面是关于高级IO我总结的一篇文章: https://blog.csdn.net/qq_37941471/article/details/80952057 可以了解一下 五种IO模型 以及 它们之间的关系;当然还有IO多路转接的其他实现方式:poll epoll 以及三者之间的对比

select


一 . IO多路转接的作用—提高效率:

在数据通信过程中,分为两部分: 1. 是等待数据到达内核; 2. 是将数据从内核拷贝到用户区。 而往往在实际应用中,等待的时间往往比拷贝的时间多,所以我们如果想要提高效率; 必然就是要将等的时间减少(在一定的时间内,减少等待的比重) 1. 这个时候,IO多路转接就是解决这个问题的:一次监视多个文件描述符 在IO多路转接中,由于一次等待多个文件描述符, 在单位时内就绪事件发生的概率就越大,所以等的比重就会越小。 2. 而这里我们会有一个问题:监视的文件描述符返回条件是什么? 答: 1. 监视的文件描述符都有自己要关注的事件(读/写/异常事件 2. 返回条件就是:我们所监视(关心)的文件描述符的事件至少一个已经就绪 3. IO多路转接的实现方式: 1. select 2. poll 3. epoll等 当然还有好多,我们也可以说这三个都是:就绪事件通知机制

具体下面我们来讲一下select:

二. select函数:


1. select的函数原型
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds,\ fd_set *exceptfds, struct timeval *timeout);
2. 参数说明:
1. nfds :表示的是等待的文件描述符中的最大的那个+1; 用于限定操作系统遍历的区间(只关心的那部分,其他的就不会去遍历,减少了开销) 2. fd_set:该结构实际是一个位图; 因为文件描述符其实是数组下标,也就是从0开始的整数; 所以对于位图的每一个比特位表示的是一个文件描述符的状态; 如果是1表示关心该文件描述符上的事件,是0,表示不关心该文件描述符上的事件。 而具体关心文件描述符上的什么事件,则由中间三个参数决定。 3. readfds :表示的是需要等待的读事件的文件描述符集; 4. writefds :表示的是需要等待的写事件的文件描述符集; 5. exceptfds :表示的是需要等待的异常事件的文件描述符集; 注意:以上三个位图参数都是输入输出型参数 : 1.作为输入参数:告诉系统我要关心的文件描述符的哪些事件 2.作为输出参数:关心的文件描述符中哪些事件就绪 6. timeout:用于设置select阻塞等待的时间。 取值如下: 1. NULL:表示select阻塞等待,关心的多个文件描述符上没有时间发生时, 进程会一直阻塞在select的函数调用处。 如果至少有一个文件描述符上有事件发生,则select返回。 eg: select(max_fd+1,&rfds,NULL,NULL,NULL) // 表示只监视该文件描述符的读事件 2. 0:表示select非阻塞等待,只是用于检测等待事件的状态。 当调用该函数时,不管有无事件发生,该函数都会立即返回, 进程不会挂起等待事件的发生。 eg : // 设置select()的等待事件 struct timeval timeout = {0,0}; // 开始调用select等待所关心的文件描述符集 select(max_fd+1,&rfds,NULL,NULL,&timeout) // 表示只监视该文件描述符的读事件 3.特定的时间值:表示select只会阻塞等待一定的时间; 在该时间段内,如果有事件发生,则select返回,进程结束阻塞。 如果达到规定的时间,还没有事件发生,此时select将会超时返回。 eg : // 设置select()的等待事件 struct timeval timeout = {5,0}; // 开始调用select等待所关心的文件描述符集 select(max_fd+1,&rfds,NULL,NULL,&timeout) // 表示只监视该文件描述符的读事件 timeval的结构如下: struct timeval { long tv_sec; /* seconds :秒*/ long tv_usec; /* microseconds:微秒 */ };//头文件<sys/time.h>
3. 返回值的含义:
1. > 0 :满足就绪条件的事件个数 2. 0 : 在规定的时间内没有事件发生(超出timeout设置的时间) 3. -1 :错误 原因由errno标识;此时中间三个参数的值变得不可预测。
4. select模型—理解select的执行过程
*** 关键在于理解fd_set 假设fd_set长度为1字节,即8个bit位,一个bit位可以表示一个文件描述符; 则1字节长的fd_set最大可以对应8个文件描述符(fd) 步骤: (1)执行fd_set set; FD_ZERO(&set);set用位表示是0000,0000。 (2)若fd=5,执行FD_SET(fd,&set);set变为0001,0000(第5位置为1) (3)若再加入fd=2,fd=1,则set变为0001,00114)执⾏行select(6,&set,0,0,0)阻塞等待 (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。 注意:没有事件发生的fd=5被清空 1. 所以,在select之前需要将关心的文件描述符集合保存起来。这样便可设置下一次需要关心的 文件描述符集,同时select返回时也可由此判断哪些文件描述符上的事件就绪了。这里,可以 提供一个数组fdArray[]来保存关心的文件描述符。将文件描述符作为数组元素保存在数组中。 也就是说,fdArray[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量 int fdArray[sizeof(fd_set)*8];//第三方数组 sizeof(fd_set)*8 就是最多能够连接的数量 2. fd_set:该结构实际是一个位图; 因为文件描述符其实是数组下标,也就是从0开始的整数; 所以对于位图的每一个比特位表示的是一个文件描述符的状态; 如果是1表示关心该文件描述符上的事件,是0,表示不关心该文件描述符上的事件。
5. socket就绪条件:
一. 读就绪: 1. 当对内核中的数据进行读取时,如果接收缓冲区中的字节数,大于等于低水位 SO_RCVLOWAT,此时就说明读就绪,在对该文件描述符调用read等进行读取时, 不会阻塞,且返回值大于02. 在TCP通信中,如果服务器的socket上有新的连接到达时,客户端会发送SYN数据包 给服务器,说明读就绪。此时在调用accept接收新连接时,不会阻塞。 而且会返回新的文件描述符与客户端进行通信; 3. TCP通信中,如果对端关闭连接,此时会发送FIN数据包。也说明读就绪, 此时在调用read对文件描述符进行读取时,会返回04. 当socket上有未处理的错误时,也说明读就绪, 在对文件描述符进行read读取时,会返回-1; 二. 写就绪: 1. 在socket内核中,如果发送缓冲区中的可用字节数大于等于低水位 标记SO_SNDLOWAT时,说明写就绪,此时调用write进行写操作时, 不会阻塞,且返回值大于02. 当一方要进行写操作(即关心的是写事件),而对端将文件描述符关闭, 此时写就绪。调用write时会触发SIGPIPE信号; 3. 当socket使用非阻塞connect连接成功或失败之后,写就绪; 4. 当socket上有未读取的错误时,写就绪,此时write进行写时,会返回-1; 三. 异常就绪:当socket上收到带外数据时,异常事件就绪。
6. select的优缺点:
优点 : 1. 不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用 也就是没有多进程和多线程的一些缺点 2. 同时等待多个文件描述符,效率相对较高 缺点 : 1. 代码编写复杂,维护起来较麻烦 2. 每次调用select,都需要把fd集合从用户态拷贝到内核态, 在fd很多的情况下,循环次数多;开销大 3. 每次调用select都需要在内核遍历传递进来的所有fd,开销比较大 4. 能够接收的文件描述符有上限 select支持的文件描述符数量过小,默认是1024

实现 select版本的TCP服务器:


Makefile :

.PHONY:select_server clean select_server:select_server.c gcc -o $@ $^ clean: rm -rf select_server

select_server.c :

#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/select.h> #include <netinet/in.h> #include <arpa/inet.h> int fdArray[sizeof(fd_set)*8];//第三方数组 int startup( int port ) { // 1. 创建套接字 int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP if( sock < 0 ) { perror("socket fail...\n"); exit(2); } // 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启 int opt = 1; setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); struct sockaddr_in local; local.sin_family = AF_INET; local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型 local.sin_port = htons(port);// 这里的端口号也可以直接指定8080 // 3. 绑定端口号 if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 ) { perror("bind fail...\n"); exit(3); } // 4. 获得监听套接字 if( listen(sock,5) < 0 ) { perror("listen fail...\n"); exit(4); } return sock; } int main(int argc,char* argv[] ) { if( argc != 2 ) { printf("Usage:%s port\n ",argv[0]); return 1; } // 1. 获得监听套接字 int listen_sock = startup(atoi(argv[1]));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型 // 2. 初始化数组 fdArray[0] = listen_sock ; int num = sizeof(fdArray)/sizeof(fdArray[0]);// num表示最多能描述的文件的描述符 printf("%d\n",num);//我们可以把它的值打印出来看一下,这里就是可以监视的文件描述符的上限 // 另外根据不同的操作系统,这里的值也不同 int i = 1; for( ; i < num; i++ ) { fdArray[i] = -1;//为什么置为1?因为文件描述符的大小是从0开始递增的小整数;所以初始化时,将其全置为-1 } while(1) { //3. 根据数组向文件描述符集中添加文件描述符 fd_set rfds;// 定义一个只读文件描述符集 FD_ZERO(&rfds);//清空文件描述符集 int max_fd = fdArray[0]; int i = 0; for( ; i < num; i++ ) { if( fdArray[i] >= 0 )//遍历数组,遇到一个不是-1的数组元素,将该元素表示的文件描述符添加进文件描述符集中 { FD_SET(fdArray[i],&rfds); if( fdArray[i] > max_fd ) { max_fd = fdArray[i];// 不断的更新,找到所关心的最大的描述符,是为了填写select的第一个参数 } } } // 4. 设置select()的等待事件 struct timeval timeout = {5,0}; //5. 开始调用select等待所关心的文件描述符集 switch( select(max_fd+1,&rfds,NULL,NULL,&timeout) )// 表示只监视该文件描述符的读事件 { case 0:// 表示词状态改变前已经超过了timeout的时间 printf("timeout...\n"); case -1:// 失败了 printf("select fail...\n"); default: // 成功了 { // 6. 根据数组中记录的所关心的文件描述符集先判断哪个文件描述符就绪 // 如果是监听文件描述符,则调用accept接受新连接 // 如果是普通文件描述符,则调用read读取数据 int i = 0; for( ;i < num; i++ ) { if( fdArray[i] == -1 ) { continue; } if( fdArray[i] == listen_sock && FD_ISSET( fdArray[i],&rfds ) ) { // 1. 如果监听套接字上读就绪,此时提供接受连接服务 struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(listen_sock,(struct sockaddr *)&client,&len); if(new_sock < 0) { perror("accept fail...\n "); continue; } //获得新的文件描述符之后,将该文件描述符添加进数组中,以供下一次关心该文件描述符 int i = 0; for( ; i < num; i++ ) { if( fdArray[i] == -1 )//放到数组中第一个值为-1的位置 break; } if( i < num ) { fdArray[i] = new_sock; } else { close(new_sock); } printf("get a new link!,[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port)); continue; } //2. 此时关心的是普通文件描述符 // 此时提供读取数据的服务 if( FD_ISSET( fdArray[i],&rfds ) ) { char buf[1024]; ssize_t s = read(fdArray[i],buf,sizeof(buf)-1); if( s < 0 ) { printf("read fail...\n"); close(fdArray[i]); fdArray[i] = -1; } else if( s == 0 ) { printf("client quit...\n"); close(fdArray[i]); fdArray[i] = -1; } else { buf[s] = 0; printf("client# %s\n",buf); } } } } break; } } return 0; }

测试代码 :


这里我们用telnet客户端程序来测试,同样的我们也可以写一个自己的客户端程序,同TCP客户端程序;下面是我的TCP程序代码,可以参考: https://blog.csdn.net/qq_37941471/article/details/80738319 另外如果telnet还没有安装,没有用过的可以看一下这篇文章: 终端下telnet的安装及其应用:https://blog.csdn.net/qq_37941471/article/details/80787368

1. 参数timeout 的值为特定的时间值:

// 4. 设置select()的等待事件 struct timeval timeout = {5,0}; //5. 开始调用select等待所关心的文件描述符集 switch( select(max_fd+1,&rfds,NULL,NULL,&timeout) ) // 表示只监视该文件描述符的读事件

这段代码也就是上面的代码,另外: 特定的时间值 : 如果在指定的时间内没有事件产生,select将超时返回。 并且这里的select的返回值为0时,表示词状态改变前已经超过了timeout的时间 输出:timeout…

1. 首先我们先运行服务器端的代码,如果先运行客户端,会发生连接失败:

原因很简单:这是一个TCP程序,TCP面向连接

先运行客户端:

运行客户端:

2. 运行客户端:

2. 参数timeout 的值为0:

// 4. 设置select()的等待事件 struct timeval timeout = {0,0}; //5. 开始调用select等待所关心的文件描述符集 switch( select(max_fd+1,&rfds,NULL,NULL,&timeout) ) // 表示只监视该文件描述符的读事件

将上面的代码修改如上:

0:表示select非阻塞等待,只是用于检测等待事件的状态。当调用该函数时,不管有无事件发生,该函数都会立即返回,进程不会挂起等待事件的发生。

服务器端运行结果:

3. 参数timeout 的值为NULL:

// 4. 设置select()的等待事件 // struct timeval timeout = {0,0}; //5. 开始调用select等待所关心的文件描述符集 switch( select(max_fd+1,&rfds,NULL,NULL,NULL) ) // 表示只监视该文件描述符的读事件

将上面的代码修改如上:直接在select()函数中把timeout参数置为NULL

NULL:表示select阻塞等待,关心的多个文件描述符上没有时间发生时,进程会一直阻塞在select的函数调用处。如果至少有一个文件描述符上有事件发生,则select返回。

2. 运行服务器端:

一直阻塞:

2. 运行客户端:


转载请注明原文地址: https://www.6miu.com/read-2630977.html

最新回复(0)