IO多路转接之select

编程入门 行业动态 更新时间:2024-10-14 06:24:14

IO<a href=https://www.elefans.com/category/jswz/34/1768459.html style=多路转接之select"/>

IO多路转接之select

本文分享的是IO多路转接中的select,其中包括select函数如何去使用,以及使用相关代码实现客户端向服务端发送消息的服务,从而更好地理解多路转接的select。

多路转接

多路转接是IO模型的一种,这种IO模型通过select函数进行IO等待,并且select函数能够同时等待多个文件描述符的就绪状态,单个文件描述符的等待与阻塞IO类似。

select

系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

通俗的来讲,select函数,就是负责等待,得到文件描述符就绪后,通知上层进行读取或写入。select没有读取或写入数据的功能,并且select能够同时等待多个文件描述符。

select函数原型

select的函数原型:

#include <sys/select.h>int select(int nfds,  fd_set  *readfds,  fd_set  *writefds, fd_set  *exceptfds,  struct timeval *timeout);

参数解释:

①参数nfds:需要监视的最大的文件描述符值+1。

要解释readfds、writefds和exceptfds前,先解释它们的类型fd_set类型。

fd_set类型

fd_set是一个整数数组, 更严格的说, 是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。

在fd_set位图结构中,使用比特位的“位置”来表示某一个sock

而对于比特位的“内容”,首先我们需要知道的是,readfds、writefds和exceptfds三个参数都是输入输出型参数。

以readfds读为例:

用户在使用该参数进行输入时,实质上是用户告诉内核,内核你要帮我关心一下哪些文件描述符上的读事件就绪。

内核进行输出时,实质上是告诉用户,用户你所关心的那些文件描述符上的读事件已经就绪。

于是,对于比特位的“内容”,首先是输入时,是用户想要内核帮忙关心的文件描述符的合集。在输出时,是内核要告诉用户已经就绪的文件描述符的合集

比如,输入时,我们规定用户想要关心的文件描述,在位图结构中,其比特位的位置位1,3,5,于是在输入时,将其内容置为1,表示我们需要让select帮我们关心1,3,5文件描述符。那么在输出时,假设这些文件描述符1,5都已经就绪,输出回来时,这个合集中的1,5比特位的位置上的内容为1,而3由于没有就绪,就为0。需要注意的是,输入输出的都是同一个位图,是同一个!

提供了一组操作fd_set的接口, 来比较方便的操作位图:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位。
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真。
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位。
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位。

②readfds、writefds和exceptfds三个参数:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。

在解释参数timeout前,我们先来解释struct timeval结构。

timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

 函数返回值:

执行成功则返回文件描述词状态已改变的个数。
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

EBADF 文件描述词为无效的或该文件已关闭。
EINTR 此调用被信号所中断。
EINVAL 参数n 为负值。
ENOMEM 核心内存不足。

③参数timeou:参数timeout为结构timeval,用来设置select()的等待时间。一般timeou参数的取值有三种:

NULL:填入nullptr或者NULL时,表示阻塞。即表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件,即只要不就绪,就不返回。
0:当struct timeval timeout={0,0},即为0时,表示非阻塞。仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,即只要不就绪,立马返回。
特定的时间值:当struct timeval timeout={5,0}。表示,在5秒内阻塞,5秒后非阻塞。如果在指定的时间段里没有事件发生, select将超时返回。

④select函数返回值

当返回值ret>0:表示已有几个fd已经就绪。比如ret = 2,就有2个fd就绪。

当返回值ret==0,表示超时返回

当返回值ret<0,select调用失败

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则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,0011。

*(4)执行select(6,&set,0,0,0)阻塞等待。

*(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。

*   注意:没有事件发生的fd=5被清空。

需要注意的是,因为select使用输入输出型参数标识不同的含义,因此每一此都会被清空,这意味着,每一次都需要对fd_set进行重新设置!并且,因为需要重新设置,我们需要通过第三方数组来对这些文件描述符进行保存!

代码简单实现多路转换

使用select实现一个简单服务器,客户端可以向服务端发送消息,服务端读取数据。

代码思路:代码分五步:

①创建监听套接字,端口号,绑定,进入监听状态一系列动作。进入监听状态后,不能马上进行accept,因为accept便是阻塞状态,监听套接字本身就可以看作是读事件就绪了。

②准备好一个数组,用于存放套接字。

③select等待前的准备:创建fd_ser类型的变量,并设置相关参数。

④使用select进行等待。在等待后,需要分情况,其返回值是如何。

⑤如果select成功返回读事件已经就绪的文件描述符个数,那么开始进行读取。当然,到达这一步,就证明现在的文件描述符是合法的,然而需要查看在数组中,哪些文件描述符是就绪的了。

找到已经就绪的文件描述符后,还不能马上进行读取,因为有可能该文件描述符是监听套接字,需要进行accept。

确定是用于通信的套接字后,就可以进行读取了。

#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"//一、创建监听套接字,端口号,绑定,进入监听状态一系列动作!//NUM为数组的大小,含义是能够包含NUM个fd,一个fd一个bit
#define NUM (sizeof(fd_set) * 8)//fd_set类型大小为128字节int fd_array[NUM]; //内容>=0,合法的fd,如果是-1,该位置没有fdstatic void Usage(std::string proc)
{std::cout << "Usage: " << proc << " port" << std::endl;
}//需要输入格式: ./select_server 8080
int main(int argc, char *argv[])
{//不符合格式if (argc != 2){Usage(argv[0]);exit(1);}//符合格式uint16_t port = (uint16_t)atoi(argv[1]);//端口号int listen_sock = Sock::Socket();//创建监听套接字Sock::Bind(listen_sock, port);//绑定端口号Sock::Listen(listen_sock);//服务器进入监听状态//二、准备好存放fd的数组//先将存放fd的数组,全部置为-1。-1表示不合法for (int i = 0; i < NUM; i++){fd_array[i] = -1;}// 不会在这里进行accept,accept的本质叫做通过listen_sock获取新链接//accept是阻塞式等待//站在多路转接的视角,我们认为,链接到来,对于listen_sock,就是读事件就绪!!!//对于所有的服务器,最开始的时候,只有listen_sock//三、select等待前的准备:创建fd_ser类型的变量,并设置相关参数//事件循环//创建fd_set结构的位图:使用位图中对应的位来表示要监视的文件描述符fd_set rfds;//将fd数组中的第一个元素,存放为监听套接字fd_array[0] = listen_sock;//进入循环for (;;){//用来清除描述词组set的全部位:将位图全部置0,全部清除。FD_ZERO(&rfds);//创建最大的文件描述符,用于后续select中的第一个参数的设置int max_fd = fd_array[0];for (int i = 0; i < NUM; i++){//不合法,继续if (fd_array[i] == -1)continue;//下面的都是合法的fd//FD_SET:用来设置描述词组set中相关fd的位FD_SET(fd_array[i], &rfds); //所有要关心读事件的fd,添加到rfds中if (max_fd < fd_array[i]){max_fd = fd_array[i]; //更新最大fd}}struct timeval timeout = {0, 0}; // 5s// 我们的服务器上的所有的fd(包括listen_sock),都要交给select进行检测!!// recv,read,write,send,accept : 只负责自己最核心的工作:真正的读写(listen_sock:accept)//四、使用select进行等待int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //暂时阻塞switch (n){case -1: //错误发生时则返回-1std::cerr << "select error" << std::endl;break;case 0:   //返回0代表在描述词状态改变前已超过timeout时间,没有返回std::cout << "select timeout" << std::endl;break;default:  //执行成功则返回文件描述词状态已改变的个数std::cout << "有fd对应的事件就绪啦!" << std::endl;//五、成功返回个数,开始进行//5.1查看是否是就绪fdfor (int i = 0; i < NUM; i++){if (fd_array[i] == -1)continue;//下面的fd都是合法的fd,合法的fd不一定是就绪的fd//FD_ISSET:用来测试描述词组set中相关fd 的位是否为真if (FD_ISSET(fd_array[i], &rfds)){std::cout << "sock: " << fd_array[i] << " 上面有了读事件,可以读取了" << std::endl;// 一定是读事件就绪了!!!// 就绪的fd就在fd_array[i]保存!// read, recv时,一定不会被阻塞!// 读事件就绪,就一定是可以recv,read吗??不一定!!//看看数组中的文件描述符,是属于监听套接字还是普通套接字。//如果是监听套接字,那就需要acceptif (fd_array[i] == listen_sock)//{std::cout << "listen_sock: " << listen_sock << " 有了新的链接到来" << std::endl;// acceptint sock = Sock::Accept(listen_sock);if (sock >= 0){std::cout << "listen_sock: " << listen_sock << " 获取新的链接成功" << std::endl;// 获取成功// recv,read了呢?绝对不能!// 新链接到来,不意味着有数据到来!!直接读的话被阻塞!什么时候数据到来呢?不知道// 可是,谁可以最清楚的知道那些fd,上面可以读取了?select!// 无法直接将fd设置进select,但是,好在我们有fd_array[]!int pos = 1;for (; pos < NUM; pos++){if (fd_array[pos] == -1)break;}// 1. 找到了一个位置没有被使用if (pos < NUM){std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;fd_array[pos] = sock;}else{// 2. 找完了所有的fd_array[],都没有找到没有被使用位置// 说明服务器已经满载,没法处理新的请求了std::cout << "服务器已经满载了,关闭新的链接" << std::endl;close(sock);}}}else  //用于通信的套接字,可以读了{// 普通的sock,读事件就绪啦!// 可以进行读取啦,recv,read// 可是,本次读取就一定能读完吗?读完,就一定没有所谓的数据包粘包问题吗?// 但是,我们今天没法解决!我们今天没有场景!仅仅用来测试std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;char recv_buffer[1024] = {0};ssize_t s = recv(fd_array[i], recv_buffer, sizeof(recv_buffer) - 1, 0);if (s > 0){recv_buffer[s] = '\0';std::cout << "client[ " << fd_array[i] << "]# " << recv_buffer << std::endl;}else if (s == 0){std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;//对端关闭了链接close(fd_array[i]);std::cout << "已经在数组下标fd_array[" << i << "]"<< "中,去掉了sock: " << fd_array[i] << std::endl;fd_array[i] = -1;}else{//读取失败close(fd_array[i]);std::cout << "已经在数组下标fd_array[" << i << "]"<< "中,去掉了sock: " << fd_array[i] << std::endl;fd_array[i] = -1;}}}}break;}}return 0;
}

封装套接字相关接口:

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using namespace std;class Sock
{
public:static int Socket(){int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "socket error" << endl;exit(2);}int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));return sock;}static void Bind(int sock, uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){cerr << "bind error!" << endl;exit(3);}}static void Listen(int sock){if (listen(sock, 5) < 0){cerr << "listen error !" << endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = accept(sock, (struct sockaddr *)&peer, &len);if(fd >= 0){return fd;}return -1;}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0){cout << "Connect Success!" << endl;}else{cout << "Connect Failed!" << endl;exit(5);}}
};

select的特点

可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)= 512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后, array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select缺点

每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
select支持的文件描述符数量太小。

更多推荐

IO多路转接之select

本文发布于:2024-03-23 22:43:30,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1743704.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:多路   IO   select

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!