【网络】【Linux】多路转接技术
文章目录
- 1.select
-
- 1.1select系统调用及参数介绍
- 1.2select基本工作流程
- 1.3select技术实现echo服务器
- 1.4select优缺点
- 1.5select的适用场景
- 2.poll(了解)
-
- 2.1poll系统调用及参数介绍
- 2.2poll技术实现echo服务器
- 2.3poll优缺点
- 3.epoll
-
- 3.1epoll系统调用及参数介绍
- 3.2epoll工作原理
-
- 回调机制
- 3.3eventpoll与文件
- 3.4epoll技术实现echo服务器
- 3.5epoll优点
- 3.6epoll工作方式
在之前学习五种IO模型时,我们认识到了IO的本质是等+拷贝,而多路转接技术可以让等的过程重叠,即同时等待多个文件描述符的就绪状态,所以今天我们就来学习下如何等待多个文件描述就绪。
本篇文章会介绍三种实现多路转接的系统调用接口,实际最常用的是epoll,其实一些老的机器上面只兼容select,而poll并不常用。
1.select
select是系统提供的一个多路转接接口。
- select系统调用可以让我们的程序同时监视多个文件描述符的上的事件是否就绪。
- select的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select才会成功返回并将对应文件描述符的就绪事件告知调用者。
1.1select系统调用及参数介绍
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
- nfds:需要监视的文件描述符中,最大的文件描述符值+1(select底层使用for循环遍历实现,该值是为了界定遍历范围)。
- readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
- writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
- exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
- timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。
参数timeout的取值:
- NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
- 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。
返回值说明:
- 如果函数调用成功,则返回有事件就绪的文件描述符个数。
- 如果timeout时间耗尽,则返回0。
- 如果函数调用失败,则返回-1,同时错误码会被设置。
select调用失败时,错误码可能被设置为:
- EBADF:文件描述符为无效的或该文件已关闭。
- EINTR:此调用被信号所中断。
- EINVAL:参数nfds为负值。
- ENOMEM:核心内存不足。
(1)fd_set类型
fd_set类型可以理解为一个位图,每个比特位位置代表是哪个文件描述符,值代表是否就绪。
调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对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的全部位
(2)timeval结构
传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段时间长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒。
调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。
(3)socket就绪条件
读就绪
- socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
- 监听的socket上有新的连接请求。
- socket上有未处理的错误。
写就绪
- socket内核中,发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
- socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。=
- socket使用非阻塞connect连接成功或失败之后。
- socket上有未读取的错误。
1.2select基本工作流程
利用select多路转接实现一个简单的Echo服务器,该服务器要做的就是读取客户端发来的数据并进行打印:
- 先初始化服务器,完成套接字的创建、绑定和监听。
- 定义一个fd_array辅助数组用于保存监听套接字和已经与客户端建立连接的套接字,刚开始时就将监听套接字添加到fd_array数组当中。
- 然后服务器开始循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作。
- 每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将fd_array辅助数组当中保存的文件描述符依次设置进readfds当中表示让select帮我们监视这些文件描述符的读事件是否就绪。
- 当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds当中,此时我们就能通过提取readfds中的信息得知哪些文件描述符已经就绪,并对这些文件描述符进行对应的操作。
- 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fd_array数组当中。
- 如果读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
- 当然,服务器与客户端建立连接的套接字读事件就绪,也可能是因为客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从fd_array辅助数组当中清除,因为下一次不需要再监视该文件描述符的读事件了。
为什么要有辅助数组?
-
在服务器程序中,随着客户端连接的建立和断开,需要监听的文件描述符集合会动态变化。select调用后,只有发生事件的文件描述符会被保留在集合中,未发生事件的文件描述符会被清除。
-
因此,每次调用select之前,都需要重新构建文件描述符集合,确保所有需要监听的文件描述符都被包括在内。辅助数组可以用来存储当前所有需要监听的文件描述符,方便在每次调用select之前重新构建
fd_set
。
说明
-
服务器刚开始运行时,fd_array数组当中只有监听套接字,因此select第一次调用时只需要监视监听套接字的读事件是否就绪,但每次调用accept获取到新连接后,都会将新连接对应的套接字添加到fd_array当中,因此后续select调用时就需要监视监听套接字和若干连接套接字的读事件是否就绪。
-
由于调用select时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历fd_array对readfds进行重新设置时,还需要记录最大文件描述符值。
这其中还有很多细节,下面我们就来实现这样一个select服务器(这里我们只对读取实现多路转接)。
1.3select技术实现echo服务器
(1)构造服务器
对于一个Tcp服务器来说,我们首先需要创建一个listen套接字,然后在该listen套接字上等待获取新连接(普通套接字),而这个等待新连接到来的行为等价于对方给我发送数据,所以我们将获取新连接的行为看作读事件,所以在构造时,创建完listen套接字后,我们还需要将listen套接字仿佛辅助数组,未来由select统一进行等待。
SelectServer(uint16_t port)
: _port(port), _listensock(std::make_unique())
{
InetAddr addr("0", _port);
_listensock->BuildListenSocket(addr);
for (int i = 0; i < N; i++)
{
_fd_array[i] = defaultfd;
}
// listensocket 等待新连接到来,等价于对方给我发送数据!我们作为读事件统一处理
// 新连接到来 等价于 读事件就绪!
// 首先要将listensock添加到select中!
_fd_array[0] = _listensock->SockFd(); // 首先将listen套接字放入辅助数组
}
(2)服务器核心逻辑Loop
服务器首先必须要有一个Loop()方法,是服务器执行的主逻辑,服务器就是一个死循环。
我们需要利用select监视读事件,所以每次循环都需要首先要初始化出来一个fd_set结构,并利用FD_ZERO
将该文件描述符集内容清空,然后将辅助数组中保存的就绪的文件描述符通过FD_SET
函数赋值给文件描述符集,注意更新最大文件描述符的值。(这里我们只考虑读事件,所以写事件和异常事件我们不考虑)。
之后我们就可以填充timeval结构:
- NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
- 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。
然后根据select的返回值打印日志,当select返回值>0时(返回值是有事件就绪的文件描述符个数),证明监视的套接字中有读事件发生,此时对读事件进行处理,这里我们封装一个HandlerEvent
函数,表示对读事件处理。
void Loop()
{
while (true)
{
fd_set rfds;
FD_ZERO(&rfds); // 将文件描述符集清空
int max_fd = defaultfd;
for (int i = 0; i < N; i++)
{
if (_fd_array[i] == defaultfd)
{
continue;
}
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};
// select 同时等待的fd,是有上限的。因为fd_set是具体的数据类型,有自己的大小!
// rfds是一个输入输出型参数,每次调用,都要对rfds进行重新设定!
int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
switch (n)
{
case 0:
LOG(INFO, "timeout, %d.%d
", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error...
");
break;
default:
LOG(DEBUG, "Event Happen. n : %d
", n);
HandlerEvent(rfds);
break;
}
}
}
(3)对读事件进行处理HandlerEvent
调用该函数时,证明此时rfds文件描述符集已经被设定(输入输出型参数),文件描述符集中就是哪些文件描述符发生读事件,需要进行处理,所以我们只需要遍历辅助数组中保存的文件描述符,检测他们在rfds中是否被设定,如果被设定证明在该文件描述符上有事件发生需要进行处理,然后只需要分成两种情况,一种是listen套接字发生读事件,此时需要获取连接,另一种就是