【linux】高级IO,I/O多路转接之select,接口和原理讲解,select版本的TCP服务器逐步完善讲解
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
- 前言
- 一、接口和原理讲解
- 二、select版本的TCP服务器
- Main.cc
- SelectServer.hpp
- 完善一
- 完善二
- 完善三
- 完善四
- 完善五
- 完善六
- 完善七
- 三、select的缺点
- 四、源代码
- Main.cc
- makefile
- SelectServer.hpp
- Log.hpp
- Socket.hpp
- 总结
前言
【linux】高级IO(一)认识五种IO模型,非阻塞IO的实现,fcntl——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】高级IO,I/O多路转接之select,接口和原理讲解,select版本的TCP服务器逐步完善讲解
一、接口和原理讲解
- 多路转接的介绍 第二点中的9到11点讲解的赵六钓鱼的例子,第三点中的多路转接,详情请点击<——,多路转接的方案分为三种,分别是select,poll,epoll,下面我们来学习一下select

- 在之前的文章中,我们已经知道了,IO = 等待 + 拷贝,所以在select这里,select只负责等待,select一次可以等待多个文件描述符fd,如上就是要使用select这个多路转接方案对应的系统调用接口,下面我们先来学习第一个系统调用接口select,要使用这个系统调用接口select需要包含#include
这个头文件

- 那么对于select这个接口一共有5个参数,对于后四个参数都是输入输出型参数,那么对于第一个参数nfds来讲,select要等待多个文件描述符,那么选取多个文件描述符中最大的那一个文件描述符对应的值加1,即macfd + 1就是nfds要传入的值,接下来对于第二个,第三个,第四个参数我们先不讲,先来看第五个参数
- 接下来先讲解第五个参数timeout用于给select设置等待方式,我们可以可以看到timeout这个参数的类型是struct timeval*类型,所以我们来看一下上图中struct timeval这个结构体类型中的成员变量,有两个成员变量,那么第一个是tv_sec是一个时间戳单位是秒,第二个是tv_usec单位是秒,所以通过struct timeval这个结构体对象就可以表示一段精确到微秒的时间

- 那么select的第五个参数timeout既然是用于给select设置等待方式的,所以使用以及含义如下,目前我们关心的文件描述符fd上的事件有三种,分别是读事件(对应第二个参数),写事件(对应第三个参数),异常事件(对应第四个参数)
(一)struct timeval timeout = {5, 0},表示每隔五秒timeout一次,也就是说select会等待5秒,如果在5秒期间如果没有文件描述符上的事件就绪,那么就直接返回,此时5秒已经过了,所以返回的时候timeout = {0, 0},如果在5秒期间,假设第2秒的时候文件描述符上的事件就绪了,所以select就会直接返回,由于已经过了两秒,别忘了第五个参数timeout可是一个输出型参数,所以返回的时候timeout = {3, 0}表示还余下3秒
(二)struct timeval timeout = {0, 0},表示立马返回,非阻塞的一种,也就是和非阻塞IO的效果一样 第四点,非阻塞IO的实现,详情请点击<——
(三)如果设置为NULL,那么表示阻塞式等待,即timeout之后,select会一直等待直到有文件描述符上的事件就绪

- 接下来我们看一下select的返回值,我们可以看到select的返回值n的类型是一个int类型
(一)如果n大于0,那么表示有n个文件描述符fd就绪了
(二)如果n等于0,那么表示此时select等待超时了,没有错误,也没有文件描述符就绪
(三)如果n小于0,那么表示此时select等待出错了 - 我们目前关心的文件描述符fd上的事件有三种,分别为读事件(对应第二个参数),写事件(对应第三个参数),异常事件(对应第四个参数),并且观察上图中select的中的第二个参数,第三个参数,第四个参数的类型都是相同的,都是fd_set类型,并且都是输入输出型参数
- 所以也就意味着只要我们会使用一个,那么其余的两个就都会使用,所以下面小编以读事件readfds为例进行讲解,readfds的类型是*fd_set,即readfds是指针类型,那么如果我们传入空nullptr,那么代表任何一个文件描述符fd的读事件我们都不关心
- fd_set是内核提供的一种数据类型,它是位图 关于位图的讲解,详情请点击<——,别忘了readfds可是一个输入输出型参数,所以如下,我们来具体理解一下输入,输出的含义
(一)输入时,用户告诉内核,我给你传入的一个或多个文件描述符fd,你要帮我关心fd上的读事件,如果读事件就绪了,你内核要告诉我用户就绪了
(二)输出时,内核告诉用户,用户你让我关心的多个fd中,有哪些已经就绪了,用户你赶紧读取吧 - 经过上面我们理解了输入和输出的含义,那么输入和输出应该如何和这个fd_set关联起来呢?例如位图有8位 0000 0000,用户需要内核关心的是0,1,2号文件描述符,已经就绪的文件描述符是2号文件描述符,所以如下
(一) 对于输入来讲,0000 0000 -> 0000 0111,比特位的位置,从左侧低位到右侧高位,表示文件描述符的编号,比特位的内容,0或者1,表示是否需要内核关心,如果为1表示用户需要内核关心,如果为0表示用户不需要内核关心,所以由于用户需要内核关心的是0,1,2号文件描述符,所以低0位置表示文件描述符的编号为0,以此类推低1位置表示文件描述符的编号为1,低2位置表示fd为2,所以我们需要将前低3位设置为1
(二)对于输出来讲,0000 1111 -> 0000 0100,比特位的位置,从左侧低位到右侧高位,表示文件描述符的编号,比特位的内容,0或者1,表示用户关心的哪些文件描述符fd,上面的读事件已经就绪了 - 所以fd_set是一张位图,是用户 -> 内核,传递用户需要内核关心的文件描述符fd有哪些,内核 -> 用户,内核给用户传递哪些文件描述符fd上的事件已经就绪了,那么fd_set是一张位图,那么也就意味着这个位图需要被初始化,需要被设置,需要被读取,所以注定了使用select的时候,一定要进行进行大量的位图操作,那么关于位图操作,系统已经为我们提供了如下

- 那么如上就是系统提供的四个关于fd_set这个位图结构的四个位图操作,下面小编来介绍一下含义
(一)FD_CLR是在位图中移除一个文件描述符,即将指定位置设置为0
(二)FD_ISSET是在位图中判断指定位置的文件描述符是否被设置,即是否为1
(三)FD_SET是在位图中设置一个文件描述符,即将指定位置设置为1
(四)FD_ZERO是用于对位图进行初始化,将比特位上的值全部设置为0

- 那么此时我们对于第二个参数读事件已经较为了解了,那么对于第三个参数写事件,第四个参数出错事件的使用也和第二个参数读事件类似,所以这里小编就不过多介绍了
- 所以此时我们目前对于系统调用select的5个参数以及关于fd_set位图的四个操作函数的使用已经较为了解,那么下面我们不废话,直接写代码验证上面的所有内容
二、select版本的TCP服务器
- 接下来我们要实现一个select版本的TCP服务器,这个服务器让select同时等待多个文件描述符,关心文件描述符上的读事件,如果读事件就绪了之后进行处理即可,我们需要使用到日志,套接字
关于Log.hpp日志的讲解,详情请点击<——
关于Socket.hpp套接字的讲解,在第三点的TCP服务器的Socket.hpp进行的讲解,详情请点击<——

- 那么如上我们的select版本的TCP服务器需要建立以及包含的源文件和头文件概况如上(一)Log.hpp是日志,其中定义了Log lg,所以我们可以直接使用lg打印日志
(二)Main.cc是主函数,包含调用服务器的逻辑
(三)makefile用于编译构建可执行程序
(四)SelectServer.hpp用于对select版本的TCP服务器进行封装
(五)Socket.hpp用于封装套接字的原生接口
Main.cc
- 那么在main函数中包含对服务器的调用逻辑,所以我们使用智能指针unique_ptr管理select版本的TCP服务器,那么服务器要首先调用Init进行初始化,接下来之后再调用Start将服务器启动起来
#include "SelectServer.hpp"
#include
int main()
{
unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
SelectServer.hpp
完善一
- 那么在SelectServer.hpp就是进行封装select版本的TCP服务器,首先定义默认端口号为8080,然后就开始编写SelectServer类,我们来看一下成员对象,对于一款服务器来讲,要有用于监听连接到来的_listensock,所以这里我们使用include "Socket.hpp"中的Sock类定义一个_listensock即可,因为Sock类的成员变量包含sockfd_,并且SelectServer类的成员对象还要包含一个端口号_port用于服务器绑定端口号
- 接下来在构造函数中使用默认端口号8080初始化成员变量_port即可,那么对于析构函数调用Sock类对象_listensock中的Close关闭套接字即可
- 那么紧接着就是Init初始化服务器的编写了,依次需要完成套接字的创建,服务器的绑定,将套接字设置为监听状态,那么我们依次调用Sock类对象_listensock中的成员函数Socket完成套接字的创建,调用Bind完成服务器的绑定,调用Listen将_listensock设置为监听状态
#include
#include
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport)
:_port(port)
{}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Start()
{}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
};
- Start就是要启动服务器,那么首先我们调用Sock类对象_listensock中的Fd接口,将文件描述符sockfd获取上来,临时保存为listensock便于使用,那么服务器一旦启动就要源源不断的接收来自客户端的连接请求,服务器一旦启动几乎都在运行,所以也就意味着服务器的逻辑要基于一个死循环运行,所以这里我们使用for(;;)进行死循环
- 这里值得思考一点,按照我们之前的逻辑,在for循环内上来就要accept获取连接,那么这里能不能直接accept呢?不能,一定不能 ,因为accept是在检测并获取listensock上面的事件,所谓的accept获取的连接,意味着进行了三次握手,对方向我服务器发来AYN,我服务器向对方发去SYN+ACK,对方给我发来ACK,然后将连接对象放到全连接队列中,accept负责将全连接队列中的节点拿上去
- 那么一旦在for循环内直接去等待了,那么如果此时没有客户端来连接,那么服务器accept就阻塞等待连接的到来了,所以此时accept既负责等待又负责将连接对象从全连接对象拿上去获取连接,这还不是最关键的,最关键的是说好的一次等待多个文件描述符,可是这里accept一次只能等待一个文件描述符,select就是用于一次等待多个文件描述符的,所以一次等待多个文件描述符的工作就要交给select,你accept就只负责将连接对象从全连接对象拿上去获取连接即可
void Start()
{
int listensock = _listensock.Fd();
for(;;)
{
}
}
- 那么对于listensock来讲,连接的到来,等于读事件就绪,所以我们在select一次等待多个文件描述符的时候,要等待listensock的读事件就绪,那么在这里虽然select可以一次等待多个文件描述符,但是在最开始的时候,我们手头的文件描述符只有listensock,所以这里在select的时候,我们先硬编码让select等待一个文件描述符listensock,后面在进行完善让select等待多个文件描述符

- 那么既然是硬编码让select等待一个文件描述符listensock,那么select的第一个参数nfds是最大文件描述符的值加1,我们这里仅有一个listensock,所以nfds的值是listensock+1

- 那么第二个参数readfds是读事件位图,所以这里我们使用fd_set定义rfds读文件描述符集,那么使用FD_ZERO将rfds位图初始化为全0,接下来使用FD_SET将listensock设置进rfds位图中,即将比特位位置从右向左的第listensock比特位位置设置为1,表示用户在rfds读文件描述符集中传入给内核的要关心的文件描述符是listensock

- 所以第二个参数我们就传入rfds位图的地址,select的第三个参数表示读事件位图,第四个参数表示出错事件位图,由于这里我们只关心listensock上的读事件就绪,并不关心listensock上的写事件和出错事件就绪,所以对于select的第三个参数和第四个参数我们传入nullptr表示不关心任何文件描述符的写事件和出错事件就绪
- 那么对于select第五个参数是设置等待时间,所以这里我们就使用struct timeval结构体类型定义对象timeout并初始化为{2, 0},表示等待2秒,然后取地址timeout传入给select的第五个参数,所以此时我们就完成了对select的参数传入
- 那么对于select的返回值n,这里我们使用switch-case语句进行判断,如果返回值为0表示等待超时,既没有出错,也没有文件描述符上的事件就绪,所以这里我们打印信息,将超时时间也打印出来看一下,由于是超时,并且select的第五个参数是输入输出型参数,所以select的第五个参数输入是2秒,2秒过后没有任何文件描述符上的时间就绪,此时超时返回,余下的时间为0秒,所以按照我们的预期是打印0.0表示余下0秒
- 如果返回值为-1小于0表示等待出错,那么这里我们简单的打印出错信息即可,如果那么如果返回值即不为0,也不为-1,那么default的情况是n大于0,即表示此时用户所关心的文件描述符上的事件有就绪了的,这里具体是指listensock文件描述符上的读事件就绪了
- 那么在listensock上的读事件就绪表示此时连接已经到来,所以此时我们可以从listensock上使用accept将连接对象从全连接队列拿上来获取到一个连接,但是此时我们先不调用accept获取连接,而是仅仅使用cout打印一条连接消息即可

- 其实这里还有细节,由于select的后4个参数都是输入输出型参数,并且select的使用要嵌入到死循环中,所以也就意味着select的所有参数在select由于等待超时或者有文件描述符上的事件就绪或者等待出错返回之后,仍旧要继续使用,别忘了对于第二个,第三个,第四个参数是用户要操作系统关心的文件描述符上的事件
- 所以传入的时候,假设用户让内核关心的是0号文件描述符上的读事件(listensock也是如此),那么timeval的时间过去之后,此时0号文件描述符上的读事件仍旧没有就绪,那么内核返回给用户的rfds上的从右向左的第0位的比特位上的值就会被设置0,表示0号文件描述符上的读事件没有就绪
- 关键的来了,如果此时用户在下次调用select没有及时向rfds上添加让内核关心0号文件描述符上的读事件,别忘了select的后4个参数都是输入输出型参数,那么此时传入的rfds就是曾经select第一次由于等待超时返回给用户的,即rfds上的从右向左的第0位的比特位上的值仍旧为0
- 所以此时内核就无法继续关心用户让其最初关心的0号文件描述符上的读事件了,所以对于select的第二个输入输出型参数rfds每次调用select之前都要重新进行设置,所以小编在下面编写for循环的时候,在调用select之前每次都会对rfds进行重新设置,同样的道理,如果用户要关心写事件,也要关心出错事件,那么此时对于第三个参数,第四个参数也要进行类似于第二个参数的调用select之前每次对位图进行重新设置
- 同样的,别忘了第五个参数也是输入输出型参数,所以上一次返回的值也要被下一次调用select使用,如果用户期望每次等待的时间是2秒钟,那么如果不进行重新设置,那么第一次返回之后,此时timeval为0.0秒,所以如果在下一次传入的时候不进行重置为2秒,那么timeval仍旧为0.0秒,所以此时就变成了非阻塞等待了,就不符合用户的期望等待时间每次都是2秒钟了,所以细心的读者友友也可以观察到,小编对于timeval,每次调用select之前都对timeval进行了重置为2.0秒

- 并且如果此时等待的是多个文件描述符,那么如果最大的文件描述符的值有变动,所以此时对于select的第一个参数nfds也要及时更新为新的最大的文件描述符的值加1,只不过在目前的场景中,小编所使用的文件描述符listensock是硬编码的一个文件描述符,不需要nfds进行更新,所以在select一次等待多个文件描述符的场景中,我们得出对于select的每次调用,都要对select的五个参数进行周期性的重复设置
void Start()
{
int listensock = _listensock.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
struct timeval timeout = {2, 0};
// struct timeval timeout = {0, 0};
int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
break;
}
}
}
运行结果如下
- 此时等待时间是2秒,所以每隔2秒,select都要等待超时一次,返回的timeval为0.0符合我们的预期,并且由于小编在每次调用select之前都对第二个参数用户关心的文件描述符以及第五个参数timeval进行了设置,所以每次调用select之后,操作系统都可以得知,噢噢,你让我关心listensock上的读事件,并且等待时间是2秒,如果2秒期间listensock上的读事件就绪了,那么我就立即返回,如果没有那么等待超时返回即可
- 所以前10秒,其中的每次的2秒都是等待超时返回的timeval的值为0.0,那么接下来小编使用telnet通过127.0.0.1进行本地环回测试,连接左侧select版本的TCP服务器的8080端口,所以此时左侧用户让内核关心的文件描述符listensock上的读事件已经就绪了
- 那么此时select就会立即返回将rfds中的从低位向高位上的第listensock上的比特位设置为1,表示你用户让我关心的listensock上的读事件已经就绪了,你用户赶紧来读取吧,所以此时select的返回值n就是大于0,所以就会cout打印连接消息,可是问题来了,为什么左侧演示的现象中,显示器上会快速的一直打印get a new link呢?
- 因为这是select的机制,如果用户让内核关心的文件描述符上的事件就绪,上层应用层用户不进行处理,那么select会一直通知你进行处理,表现是什么呢?即调用select不会阻塞等待2秒了而是会直接返回,那么由于此时是1个文件描述符上的读事件就绪了,所以此时n会设置为1,如果n为1在我们的代码逻辑中就会打印get a new link,那么select整体上是在一个for循环内,所以显示器上才会快速的一直打印get a new link
- 并且我们也要知道,select告诉我们文件描述符listensock上的读事件就绪了,那么在接下来的一次读取,在读取fd的时候,不会阻塞等待,即此时调用accept不会进行阻塞等待连接的到来,而是accept可以直接将连接对象从tcp的全连接队列中拿上来,当然值得注意的是listensock上我们只关心读事件就绪,读事件就绪就是代表底层进行了三次握手连接到来,即监听套接字只关心连接的到来
完善二
- 上面我们已经验证了timeval为2.0秒的情况,下面我们将timeval设置为0.0秒,验证是否可以进行非阻塞IO,即将timeval设置为0.0秒,那么此时select调用之后内核查看完用户让关心的文件描述符之后会直接返回,不会阻塞
- 并且由于我们在代码中没有添加任何sleep休眠的代码,所以如果最初的时候,小编仅仅是启动服务器,此时进行的是非阻塞IO,那么在右侧如果小编不使用telnet充当客户端连接服务器,那么此时我们应该观察到的现象是一直快速的重复打印time out, timeout: 0.0,因为此时文件描述符listensock上的读事件并没有就绪,并且timeval每次都被设置为0.0进行非阻塞IO,所以一进入select之后,就会立即返回,将n设置为0,告诉用户没有任何文件描述符就绪,然后以此重复
- 那么当小编在右侧使用telnet充当客户端连接服务器之后,那么此时文件描述符listensock上的读事件已经就绪了,所以select等待成功,只有一个事件就绪,即n = 1,告诉用户,你让我关心的fd上的事件已经就绪了,赶快来读取吧,此时我们并没有处理连接,仅仅是打印消息,所以一进入select之后就会立即返回,select会一直通知我,赶快读取fd上的事件,所以此时按照我们的预期应该是会快速的一直打印get a new link
void Start()
{
int listensock = _listensock.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
break;
}
}
}
运行结果如下,无误
完善三
- 那么如果我们给select的第五个参数传参nullptr,那么此时select的等待方式就是阻塞等待,即阻塞等待用户要内核关心的文件描述符上的事件就绪,阻塞直到文件描述符上的事件就绪才返回
- 所以如果我们给select的第五个参数传参nullptr,那么由于最开始启动客户端之后小编并没有使用telnet连接服务器,所以此时会什么都不打印,接下来小编使用telnet连接服务器,此时文件描述符上的读事件已经就绪了,所以此时select会直接返回,由于我们并没有对连接进行处理,所以此时select会不断的告诉上层去处理连接,所以在屏幕上会快速的一直打印get a new link
void Start()
{
int listensock = _listensock.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
break;
}
}
}
运行结果如下,无误
- 那么当用户让内核关心的文件描述符listensock上的读事件就绪的时候,我们要对文件描述符上就绪的事件进行处理,所以此时我们封装一个函数HandlerEvent,那么已经就绪的文件描述符在哪里?在调用select之后返回的rfds中,所以此时我们要将rfds传参给处理事件函数HandlerEvent
- 目前我们仅仅让操作系统关心了文件描述符listensock上的读事件就绪,一旦listensock上的读,也就意味着listensock收到了连接,小编在上面仅仅是打印了一条消息get a new link,并没有将accept连接获取上来,所以接下来我们要将连接获取上来,所以我们该如何去做呢?
- 所以此时就要在Start函数中的switch-case语句的default进行调用HandlerEvent传参rfds读文件描述符集,紧接着我们就要开始编写HandlerEvent了,首先读文件描述符集rfds中包含的是读事件已经就绪的文件描述符的集合,所以我们要确定这个rfds中已经就绪的文件描述符是否包含我们所关心的listensock

- 那么如何确定select返回的rfds中是否包含读事件已经就绪的文件描述符listensock呢?其实一个文件描述符就绪了在rfds中表现为它所对应的位图的位置为1,所以此时我们可以使用FD_ISSET进行判断,那么依次传参listensock,以及rfds的地址,如果listensock在rfds位图中被设置,那么返回true,否则为false
- 所以如果为true代表此时我们关心的listensock上的读事件已经就绪了,即此时连接已经到来了,所以我们就可以调用accept将连接从底层tcp的全连接队列中拿上来,这里我们使用#include "Socket.hpp"中接口,即Sock对象_listensock中的函数Accept来做,此时我们就可以将连接获取上来了
- Accept的返回值为获取到连接对应的文件描述符sock,如果sock小于0代表获取失败,那么我们直接返回即可,走到下一步代表此时的文件描述符sock获取成功,即连接获取成功,那么我们使用日志对象lg打印一下获取上来的新连接对应的文件描述符sock,客户端的IP地址,端口号即可
void HandlerEvent(fd_set& rfds)
{
if(FD_ISSET(_listensock.Fd(), &rfds))
{
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0)
return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
}
}
void Start()
{
int listensock = _listensock.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
HandlerEvent(rfds);
break;
}
}
}
运行结果如下
- 那么结果如上,首先我们的代码中的select的第五个参数是nullptr,即阻塞等待直到文件描述符上的事件就绪,所以在最开始小编启动左侧服务器的时候,并没有客户端过来连接,所以服务器没有收到来自客户端的连接,别忘了listensock文件描述符上的读事件就绪表示连接的到来,此时没有连接到来,那么也就表示此时listensock文件描述符上的读事件并没有就绪,所以我们才会观察到左侧显示器上并不进行任何输出,好像卡住了一样
- 那么接下来,小编在右侧的第一个会话中使用telnet连接服务器,所以此时左侧服务器有连接到来,listensock上的读事件就绪,那么select的阻塞等待就会结束,然后由于此时有一个文件描述符上的事假已经就绪,所以会返回n等于1,那么经过switch-case之后就会打印get a new link,然后进入HandlerEvent函数
- 那么在HandlerEvent函数中,调用Accept将连接获取上来,然后使用日志进行打印客户端信息,所以此时左侧会观察到第一次打印get a new link以及客户端信息,接下来打印完成,返回Start函数,继续进行for循环select又去阻塞等待文件描述符就绪了,同样的道理,那么小编在右侧的第二个会话中使用telnet连接服务器,也是会观察到第二次打印get a new link以及客户端信息
完善四
- 所以此时连接获取上来了,并且我们也拿到了连接对应的sock文件描述符,所以此时问题来了,我们可不可以直接read读取数据呢?不可以
- 一定不可以,这个sock是服务器和客户端的连接,如果客户端连接后不给服务器发送报文数据,那么此时sock上的tcp的接收缓冲区中就没有数据,所以如果服务器此时直接调用read向sock中读取数据,那么此时read就会由于底层sock上没有数据而不会返回,即此时服务器进程就阻塞式等待sock上的数据,即等待sock上的读事件了
- 那么服务器可不可以直接在这里由于调用read由于底层sock上没有数据而直接阻塞住呢?不可以一旦服务器阻塞住了,那么此时又有客户端来连接服务器明明还有能力提供服务,服务器的负载还没有达到上限,此时你服务器告诉我客户端我等待着数据呢,你们客户端别来连接我
- 那么如果服务器永远等待不到数据,此时服务器无法连接其它的客户端了,服务器的目的就是为了给众多的客户端提供服务,所以这样的让服务器直接read的方式是不可取的,那么别忘了IO = 等待 + 拷贝,服务器不能直接调用read等待读取数据,但是一旦涉及到等待,此时我们就想起了select
- select可以一次等待多个文件描述符呀,但是此时这个sock文件描述符是处于HandlerEvent这个成员函数中的,而select是处于Start这个成员函数中的,那么我们该如何将sock传递给select呢?仔细想一下,首先HandlerEvent和Start这两个都是成员函数
- 既然是成员函数,那么也就意味着同样都处于一个类内,既然是同样都处于同一个类内,那么也就意味着可以看到类的成员变量,所以此时我们就有了一个想法,将服务器连接的客户端对应的文件描述符sock放到成员变量中,既然是服务器,那么服务器是要同时连接多个客户端的
- 所以也就意味着服务器同时连接的多个客户端都有连接对应的文件描述符sock的,既然是多个文件描述符sock,我们该如何将其放到成员变量中呢?先描述,再组织,那么关于描述工作,我们已经做完了,因为文件描述符就是一个整数,使用一个int类型进行描述即可,所以接下来摆在我们面前的是再组织,所以如何组织呢?
- 那么此时我们想一下,我们需要一个数据结构来保存这些文件描述符sock,目的是为了等待这些文件描述符sock上的数据,即等待这些文件描述符sock读事件就绪,等待工作是要交给select去等待的,如何交给select?

- 那么就是通过select的第二个参数rfds,即将数据结构内的文件描述符sock逐一遍历,然后通过FD_SET设置进本地的rfds,然后rfds取地址传参交给select的第二个参数,所以用于阻塞的数据结构要做的工作仅仅是保存然后可以进行遍历即可
- 所以既然涉及到遍历,那么我们使用最简单的数组作为数据结构将多个文件描述符sock组织起来即可,所以说此时我们就在类内定义一个成员变量int fd_array[],但是数组要定义多大呢?这又是一个问题,有的读者友友说,使用STL中的vector定义成空数组自动扩容即可
- 这里小编为大家采用原生数组,更便于帮助我们理解select,所以fd_array数组要定义多大呢?那么数组要定义多大,核心在于fd_array数组要存储多少文件描述符sock,fd_array数组中存放的多个文件描述符sock是要给select的第二个参数rfds使用的,这里就很巧妙了,rfds既然是参数,那么就有类型fd_set*,所以我们来看fd_set这个数据类型
- fd_set这个类型是内核针对于select设计出的数据类型,那么fd_set既然是类型,所以也就意味着这个类型就要有确定的大小,所以我们来看一下fd_set类型的大小,那么我们实际要看的是fd_set究竟可以放多少个文件描述符,所以fd_set的大小的单位是字节,一个字节有8个比特位,所以fd_set的大小乘以8就可以算出fd_set比特位的数目,即算出fd_set究竟可以放多少个文件描述符
int main()
{
cout << "fd_set bits num: " << sizeof(fd_set) * 8 << endl;
return 0;
}
运行结果如下
- 所以我们可以看出fd_set对应的比特位的数目是1024个,即以用户让内核关心文件描述符的读事件为例,用户最多使用fd_set让内核关心1024个文件描述符上的读事件,多余的无法传递只能丢弃,所以我们的fd_array的可以放的文件描述符的大小应该和这个fd_set可以传递的文件描述符的数目相同,即数组的可以存放的数目设置为fd_num_max = 1024,即我们应该这样定义成员变量int fd_array[fd_num_max]
- 并且在类的构造函数时候,遍历文件描述符数组将里面的值设置为default即默认的default的值为-1,我们在使用数组的时候,如果里面的值为default我们就认为当前的位置可覆盖,当前位置的数无效,因为文件描述符的值本质是数组的下标,数组下标是从0开始的
- 所以此时我们有了文件描述符数组fd_array之后,可以在HandlerEvent函数中将listensock获取上来的连接对应的文件描述符sock放到文件描述符数组fd_array中,那么应该如何放呢?遍历fd_array找到pos位置为defaultfd的值,将sock放到对应位置上即可
- 可是如果遍历了一遍没有找到值为defaultfd的位置pos,说明此时数组中的1024个位置全部都被放置了服务器和客户端的连接对应的文件描述符sock,所以即使有sock也无法放到数组fd_array中了,而fd_set中可以传入的文件描述符的数目也就只有1024个,所以此时新到来的这个客户端和服务器的连接对应的sock,作为服务器已经无法进行处理了
- 因为select可等待的文件描述符已经上限了,即此时select版本的TCP服务器已经满载了,那么此时我们就打印日志,丢弃这个sock,如何丢弃?sock是一个文件描述符,那么我们使用close关闭文件描述符sock,释放sock底层对应的文件描述对象即可
- 如果在遍历的过程中,找到了值为defaultfd的位置pos,所以此时就将sock放到fd_array数组的pos位置即可,接下来我们使用PrintFd打印一下fd_array当前在线的文件描述符,如何打印?遍历找到值不为defaultfd的位置,打印即可,那么当PrintFd打印完成,接下来还可以去做其它的事情,这里小编就不举例了,可以根据应用场景去做事请
- 那么在select版本的TCP服务器中,必不可少的是listensock,所以我们可以在Start启动服务器的时候,就将listensock添加到fd_array数组中,因为listensock的读事件的就绪是必须要被用户关心的,只有关心listensock的读事件才可以使用select等待listensock读事件就绪,进而我们才能去Accept去获取到来自客户端的连接sock
#include
#include
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport)
:_port(port)
{
for(int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void HandlerEvent(fd_set& rfds)
{
if(FD_ISSET(_listensock.Fd(), &rfds))
{
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0)
return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < fd_num_max; pos++)
{
if(fd_array[pos] == defaultfd)
break;
}
if(pos == fd_num_max)
{
lg(Warning, "server is full, close %d now", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
HandlerEvent(rfds);
break;
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for(int i = 0; i < fd_num_max; i++)
{
if(fd_array[i] != defaultfd)
{
cout << fd_array[i] << ' ';
}
}
cout << endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max];
};
运行结果如下
- 所以运行结果如上,左侧小编将服务器运行起来,然后右侧的第一个会话作为客户端使用telnet连接服务器,那么左侧服务器成功的获取到了连接,并且将sock添加到了fd_array数组中,那么在第一个连接到来的时候,小编解析一下左侧打印的当前在线的文件描述符,3代表是listensock,而这个listensock在调用Start的时候就被小编设置进了fd_array的0号下标中,4代表的是第一个会话作为客户端使用telnet连接服务器对应的文件描述符
- 为什么文件描述符从3开始,因为一个进程默认会打开3个文件描述符,分别是标准输入0,标准输出1,标准错误2,并且由于文件描述符本质上是数组下标,是连续的,所以说,fd_array中放的第一个文件描述符才是从3开始的
- 那么与第一个会话作为客户端使用telnet连接服务器同样的道理,右侧第二个会话,第三个会话也进行了添加sock到fd_array数组中,然后打印文件描述符数组fd_array对应的在线文件描述符
完善五
- 所以此时,虽然我们已经可以做到等待listensock的读事件就绪,listensock的读事件就绪代表此时连接到来了,所以我们使用HandlerEvent函数处理读事件,使用Accept将连接获取上来得到文件描述符sock,并且我们将文件描述符sock添加到了文件描述符数组fd_array中了
- 我们要关心的是文件描述符数组fd_array的读事件就绪的,此时我们是否通过select告诉内核要关心fd_array中文件描述符的读事件就绪了吗?没有,int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);我们的select仍然是硬编码只关心一个文件描述符listensock

- 此时fd_array数组中有多个文件描述符需要我们关心读事件就绪了,所以此时我们就不能像之前一样给select传参了,那么第一个参数是nfds就是要等待的多个文件描述符中最大的那一个的值加1,即nfds = maxfd + 1,接下来第二个参数rfds是要传入我们用户要内核关心的读事件位图
- 此时我们就要对这个rfds进行设置了,设置的需要关心的读事件就绪对应文件描述符都在fd_array中,当然如果我们还要关心写事件就绪对应的文件描述符那么可以再维护一个wfds,当然这里掌握了读事件的就绪对应的文件描述符再触类旁通掌握写事件的文件描述符就绪不成问题,这里我们就不添加写事件位图wfds了,而是以读事件位图rfds对应的文件描述符数组fd_array为例进行讲解
- 此时rfds仍旧是只设置了关心listensock这一个文件描述符,此时对应rfds,我们目前要关心的读事件的文件描述符是有多个被存储在了fd_array中,所以此时我们就要遍历fd_array,将位置上的值不为defaultfd的统统使用FD_SET设置进rfds中即可
- 同样此时有了多个文件描述符,所以文件描述符的最大值maxfd也有可能会进行变化,所以我们也要在遍历数组寻找文件描述符的最大值,当然由于此时我们可以确定listensok是一定要被我们用户关心读事件的,我们可以使用listensock对maxfd进行初始化
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for(int i = 0; i < fd_num_max; i++)
{
if(fd_array[i] != defaultfd)
{
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
}
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
// int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
HandlerEvent(rfds);
break;
}
}
}
运行结果如下
- 运行结果如上,那么左侧服务器最开始没有客户端来连接,所以此时select就阻塞等待文件描述符的事件就绪了,接下来,右侧的第一个会话作为客户端使用telnet连接服务器,那么左侧服务器成功的获取到了连接,并且将sock添加到了fd_array数组中,同样的更新了最大的文件描述符maxfd,同样的,右侧第二个会话,第三个会话也是如此
完善六
- 此时貌似很完美了,但是别忘了我右侧会话的客户端仅仅是将连接服务器,那么客户端此时并没有向服务器发送任何数据报文,并且服务器的HandlerEvent函数中仅仅是对文件描述符listensock的读事件就绪进行处理,其余的文件描述符sock我们并没有进行处理,即此时如果客户端向我发送数据报文,我服务器目前并没有处理的逻辑,所以在HandlerEvent函数我们就要处理一下
- 那么如何处理呢?例如右侧作为客户端的telnet连接服务器后,那么输入消息,期望服务器将从sock文件描述符从拿到数据进行打印,即一旦右侧客户端输入消息发送给服务器,此时服务器中对应连接的文件描述符sock中就有数据了,即此时连接对应的文件描述符sock的读事件就已经就绪了
- 所以我们在HandlerEvent要对所有已经读事件就绪的文件描述符fd进行处理,这些读事件已经就绪的文件描述符分为两类,一类是listensock获取来自客户端的连接,一类是sock获取来自客户端拿到数据并打印
- 所以既然有两类,那么我们就要进行判断,如果是listensock那么就调用Accept获取连接,将连接放到fd_array数组中,如果是sock那么就定义一个用户级缓冲区buffer,使用read将数据从tcp的接收缓冲区中读取上来,那么为什么在这里小编敢直接使用read读取数据呢?
- 因为内核已经告诉我,你用户要我内核关心的sock上的读事件已经就绪了,那么什么是sock上的读事件已经就绪了?即此时文件描述符sock底层的tcp的接收缓冲区已经收到了来自客户端的数据了,所以此时我们直接调用read不会阻塞,直接可以将内核的tcp接收缓冲区的数据拷贝到用户层缓冲区buffer中

- 那么我们要判断文件描述符的读事件在是否已经在rfds位图中就绪,需要使用FD_ISSET进行判断,所以要进行判断文件描述符从哪里来?从fd_array数组中来,别忘了当初的rfds读事件位图中的文件描述符都是从fd_array中进行设置的,即用户告诉内核,在rfds位图中的文件描述符你要帮助我关心哪些文件描述符已经就绪了
- 而在rfds中被设置的全部文件描述符中如果有一个或多个文件描述符的读事件就绪了,那么从内核返回的rfds中对应的一个或多个文件描述符在位图中对应的位置就会被设置为1,表示这一个或多个文件描述符已经就绪了,用户赶快读取文件描述符吧
- 所以我们要将fd_array数组中的文件描述符进行遍历判断文件描述符读事件是否就绪,如果此时文件描述符是defaultfd那么我们continue,否则就判断当前文件描述符是否就绪,如果就绪了,那么再判断如果是listensock那么就调用Accept获取连接,将连接放到fd_array数组中,如果是sock那么就定义一个用户级缓冲区buffer,使用read将数据从tcp的接收缓冲区中读取上来
- 值得注意的是read的返回值n
(一) 如果大于0,那么说明有数据,那么将缓冲区的第n个位置的值设置为0,0即对应字符串的结尾’ ’,然后将缓冲区buffer的数据当做字符串打印即可,但是这里会一并将用户最后按下输入的换行也读取进来,所以这里我们让缓冲区的第n - 1个位置置为0去除换行即可
(二)如果n等于0,说明客户端已经将连接关闭了,客户端将写端关闭,不会再给我发送消息了,所以我服务端也close关闭连接,将这个文件描述符对应fd_array的位置置为defaultfd,所以在文件描述符数组fd_array中,一旦将关闭连接对应位置的值置为defaultfd,那么就相当于是从select的下次的rfds中移除,因为每次进行select都要从fd_array中重新添加关心的读事件的文件描述符,fd_array中没有了关闭连接的文件描述符那么也就自然不会添加到读位图rfds中了,所以也就相当于将关闭连接的文件描述符从select中移除
(三)如果n小于0,那么说明读取出错,那么打印日志,同样close关闭连接,将出错的文件描述符对应在文件描述符数组fd_array的位置置为default,也就是将出错的文件描述符从select中移除,下次select等待读事件文件描述符就绪不等待这个出错的文件描述符的读事件就绪了
void HandlerEvent(fd_set& rfds)
{
for(int i = 0; i < fd_num_max; i++)
{
int fd = fd_array[i];
if(fd == defaultfd)
continue;
if(FD_ISSET(fd, &rfds))
{
if(fd == _listensock.Fd())
{
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0)
return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < fd_num_max; pos++)
{
if(fd_array[pos] == defaultfd)
break;
}
if(pos == fd_num_max)
{
lg(Warning, "server is full, close %d now", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
}
}
else
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << "get a message: " << buffer << endl;
}
else if(n == 0)
{
lg(Info, "client quit, me too, close fd: %d", fd);
close(fd);
fd_array[i] = defaultfd;
}
else
{
lg(Warning, "recv error, fd: %d", fd);
close(fd);
fd_array[i] = defaultfd;
}
}
}
}
}
运行结果如下
- 运行结果如上,所以此时左侧的select版本的TCP服务器,可以同时等待多个文件描述符,不论是来自客户端的连接,还是来自客户端的数据,左侧服务器都可以把握的住
完善七
- 但是此时我们看一下上面的HandlerEvent的代码有点太长了,所以由于文件描述符分为两类,如果是listensock那么就调用Accept获取连接,将连接放到fd_array数组中,如果是sock那么就定义一个用户级缓冲区buffer,使用read将数据从tcp的接收缓冲区中读取上来
- 所以此时我们可以将listensock的代码处理封装为Accepter连接管理器,将sock的代码处理封装为Recver数据读取器,那么此时我们的HandlerEvent不就相当于一个派发器,将已经读事件已经就绪的不同文件描述符派发到不同的函数中进行处理,那么派发器的英文为dispatcher,所以我们将HandlerEvent修改为Dispatcher,并且在Start调用HandlerEvent的地方也一并修改为Dispatcher
void Accepter()
{
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0)
return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < fd_num_max; pos++)
{
if(fd_array[pos] == defaultfd)
break;
}
if(pos == fd_num_max)
{
lg(Warning, "server is full, close %d now", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
}
}
void Recver(int fd, int pos)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << "get a message: " << buffer << endl;
}
else if(n == 0)
{
lg(Info, "client quit, me too, close fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
else
{
lg(Warning, "recv error, fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
void Dispatcher(fd_set& rfds)
{
for(int i = 0; i < fd_num_max; i++)
{
int fd = fd_array[i];
if(fd == defaultfd)
continue;
if(FD_ISSET(fd, &rfds))
{
if(fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else
{
Recver(fd, i); // 数据读取器
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for(int i = 0; i < fd_num_max; i++)
{
if(fd_array[i] != defaultfd)
{
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
}
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
// int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
Dispatcher(rfds);
break;
}
}
}
运行结果如下,无误
- 运行结果如上,所以此时左侧的select版本的TCP服务器,可以同时等待多个文件描述符,不论是来自客户端的连接,还是来自客户端的数据,左侧服务器都可以把握的住,同样的如果客户端telnet退出连接,那么同样的,退出连接也是sock的读事件就绪,所以即使是客户端的telnet退出结束连接
- 我们的服务器中的select也可以等待到sock的读事件就绪,然后通过经过分派器Dispatcher分派给数据读取器Recver,然后从调用read的返回值中得知n等于0,即客户端断开了连接,所以此时服务器也断开连接,所以我们的服务器对于客户端telnet将连接断开的情况也把握得住
- 此时我们就完成了select版本的TCP服务器,仔细回看一下,从最初的认识接口,我们心中可能会想这个select版本的TCP服务器一定很难写吧,但是此时我们再回头一看,好像也就那样,从完善一到完善七,逐步跟着小编的逻辑思路去理解,把思路捋清楚,select版本的TCP服务器并不难
- 这其中一旦我们将思路捋清楚之后,就可以感觉到select版本的TCP服务器自始至终从完善一到完善七就好像有一根逻辑的线一样,可以助力我们将select版本的TCP服务器编写完成,所以遇到代码的编写不要畏惧,仔细的去捋清楚逻辑思路之后,代码的编写实现将不再成为我们的难点
三、select的缺点
- 所以此时我们来看一下select的缺点

- 第一点:select等待的fd是有限的,这个select的接口本身有问题,fd_set位图中只能容纳有限的1024个fd,不支持扩容或者传递更多的fd
- 第二点:输入输出型参数比较多(select的后四个参数),数据拷贝的频率比较高,每次调用select的时候,从用户到内核,需要对用户关心的fd进行拷贝修改,从内核到用户,需要对已经就绪的fd进行拷贝修改,所以如果频繁的调用select,那么也就会进行频繁的数据拷贝
- 第三点:输入输出型参数比较多,每次都要对关心的fd进行事件重置
- 第四点:在用户层,要使用第三方数组fd_array管理用户关心的fd,并且用户层需要进行多次的遍历,例如在调用select之前用户重置rfds中用户关心的fd需要遍历数组,在Dispatcher判断fd是否在rfds中也需要进行遍历数组,在连接管理器Accepter中寻找合适的位置放入连接对应的文件描述符sock的时候同样需要遍历数组,同样的在内核检测fd就绪也需要遍历底层的文件描述符表进而判断对应的fd上的事件是否就绪
- 所以虽然select是一个多路转接的方案,可以同时等待多个文件描述符,理想情况下,select等待的文件描述符越多,那么单位时间内等待的时间就越少,所以IO效率越高
- 但是在实际中,随着select等待的文件描述符越来越多,上面的四点被触发的概率就越多,即带来的是数据拷贝较多,并且重置增多,遍历增多,所以在实际使用select的时候,随着select一次等待的文件描述符越来越多,select进行IO的效率可能到达某个峰值之后,可能会增长缓慢
- 所以即使select在其它IO模型的对比中,IO效率较高,但是对于我们来讲还不够,并且对于上面的第一点和第三点是select的硬伤,于是我们有了第二种多路转接方案poll,关于poll请各位读者友友期待小编在下一篇文章中的讲解

- 这里我们在着重研究一下select,select是一个系统调用,也就意味着当用户调用select的时候,会从用户态切换成内核态,那么select的作用是一次等待多个文件描述符,那么由于select是系统调用,所以也就是内核如何得知用户通过传参rfds让内核关心的多个文件描述符上的事件是否就绪了呢?
- 本质通过遍历进程对应的文件描述符表,别忘了文件描述符表是一个数组,这个数组的下标有很多,那么内核如何得知需要遍历到什么程度呢?那么实际上就是通过用户给select传入的第一个参数nfds得知,nfds是我们传入的多个要关心的文件描述符中的文件描述符最大的那一个的值加1,这本质上就是要遍历数组要遍历的最大范围
- 所以通过select的第一个参数内核才能得知要遍历进程对应的文件描述符的最大范围,所以在遍历的时候,一旦在文件描述符表中遍历到的文件描述符在用户传入的要关心的rfds位图中,并且一旦有连接到来了或者文件描述符上的缓冲区中有数据了,那么代表这个文件描述符的读事件已经就绪了
- 那么此时select就会将rfds位图中对应已经就绪的文件描述符位置的从低位到高位的比特位的位置设置为1,其余位置为0(当然也有可能会出现多个文件描述符上的事件同时就绪的情况,不怕设置对应比特位位置为1,其余位置为0即可)
- 然后select返回事件已经就绪的文件描述符的个数,告诉用户rfds位图上的文件描述符的读事件已经就绪了,所以上层就可以通过select返回值n获悉,在rfds中有文件描述符的读事件就绪了,那么接下来调用处理文件描述符事件就绪的函数即可
四、源代码
Main.cc
#include "SelectServer.hpp"
#include
int main()
{
unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
// int main()
// {
// cout << "fd_set bits num: " << sizeof(fd_set) * 8 << endl;
// return 0;
// }
makefile
selectserver:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f selectserver
SelectServer.hpp
#include
#include
#include
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport)
:_port(port)
{
for(int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0)
return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < fd_num_max; pos++)
{
if(fd_array[pos] == defaultfd)
break;
}
if(pos == fd_num_max)
{
lg(Warning, "server is full, close %d now", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
}
}
void Recver(int fd, int pos)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << "get a message: " << buffer << endl;
}
else if(n == 0)
{
lg(Info, "client quit, me too, close fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
else
{
lg(Warning, "recv error, fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
void Dispatcher(fd_set& rfds)
{
for(int i = 0; i < fd_num_max; i++)
{
int fd = fd_array[i];
if(fd == defaultfd)
continue;
if(FD_ISSET(fd, &rfds))
{
if(fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else
{
Recver(fd, i); // 数据读取器
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for(int i = 0; i < fd_num_max; i++)
{
if(fd_array[i] != defaultfd)
{
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
}
// struct timeval timeout = {2, 0};
struct timeval timeout = {0, 0};
// int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
// int n = select(listensock + 1, &rfds, nullptr, nullptr, nullptr);
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch(n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cout << "select error" << endl;
break;
default:
cout << "get a new link" << endl;
Dispatcher(rfds);
break;
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for(int i = 0; i < fd_num_max; i++)
{
if(fd_array[i] != defaultfd)
{
cout << fd_array[i] << ' ';
}
}
cout << endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max];
};
Log.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1 //输出到屏幕上
#define Onefile 2 //输出到一个文件中
#define Classfile 3 //根据事件等级输出到不同的文件中
#define LogFile "log.txt" //日志名称
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method) //改变日志打印方式
{
printMethod = method;
}
~Log()
{}
std::string levelToString(int level)
{
switch(level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "";
}
}
void operator()(int level, const char* format, ...)
{
//默认部分 = 日志等级 + 日志时间
time_t t = time(nullptr);
struct tm* ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[2 * SIZE];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
void printLog(int level, const std::string& logtxt)
{
switch(printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string& logname, const std::string& logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string& logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
Socket.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
const int backlog = 10;
enum{
SocketErr = 1,
BindErr,
ListenErr,
};
class Sock
{
public:
Sock()
{}
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(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;
socklen_t len = sizeof(local);
if(bind(sockfd_, (struct sockaddr*)&local, len) < 0)
{
lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s : %d", strerror(errno), errno);
return -1;
}
char ipstr[128];
inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& serverip, uint16_t serverport)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
socklen_t len = sizeof(peer);
int n = connect(sockfd_, (struct sockaddr*)&peer, len);
if(n == -1)
{
std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
return false;
}
return true;
}
void Close()
{
if(sockfd_ > 0)
{
close(sockfd_);
}
}
int Fd()
{
return sockfd_;
}
~Sock()
{}
private:
int sockfd_;
};
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!


















