前言
上文介绍了五种IO模型。本文将介绍五种IO模型之一的多路转接。多路复用的优势在于同一时间可以等待多个文件描述符。提高了IO的效率。在现代计算机中IO效率最慢的就是网络通信。本文将介绍多路转接的初始模型:select。了解select的工作原理,并且编写网络服务器。
认识select
参数介绍
seletc函数是用来等待的!并不负责拷贝,拷贝是交由read\send来进行
#include <sys/select.h>
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
参数解释
- nfds:监视的最大文件描述符+1
- readfds\writefds\execeptfds:分别是读事件、写事件、异常事件的集合
- timeout :等待的时间
返回值
- n>0:表示就绪的事件数目
- n==0:等待超时,没有事件就绪
- n==-1:出错(关于出错,常常利用错误码判断)
出错时候的错误码
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足
参数timeout的介绍
- nullptr:表示阻塞式等待
- 0:非阻塞
- 特点的时间值:比如{5, 0 }表示5秒阻塞等待,之后非阻塞一次
关于事件位图
fd_set的本质就是一张位图,一般最多能接受sizeof(fd_set)是512字节,也就是8*sizeof(fd_set)4K大小的事件。
fd_set的添加/删除/修改事件都必须通过特定的宏函数
如果关心某个事件,就会把事件添加到fd_set 位图中。比如关心0号文件描述符的读事件,就先将0号fd添加到位图中,然后调用select等待。
简单的执行逻辑
理解select的执行过程
timeout和fd_set都是输入输出型参数
- timeout参数:如果timeout设置{5,0}等待了2秒后有事件就绪,timeout就会被重置为 3秒
- fd_set:如果我们等待前设置的文件描述符有0 、5 、8 、10而在一次timeout时间内都没有文件描述符就绪,fd_set的每一位都会被置为 0 。如果timeout时间内只有8号文件描述符就绪,那么只有第8位会保留 1 ,其余位都是0
所以每一次调用select之前,都必须被fd_set设置。这是一个很麻烦的操作!
所以需要借助第三方容器将要关心的fd事件提前保留下来,等下一次select前添加到fd_set中。
另外select可以关心读事件、写事件、异常事件如果其中有一项不想关心,设为nullptr即可。
什么叫做事件就绪?
比如俩个主机建立TCP通信。主机A给主机B发消息。数据来不及发出去,一直发导致写缓冲区满了 ,那么 写事件就是不就绪。
B主机上的接收缓冲区一旦有数据,就代表建立连接的sockfd上读事件就绪!
select服务器
编写一个基于TCP通信的select模型
要点:
- 必须利用第三方数组保存要关心的事件。
- listensock不能直接accept连接,必须先将accept添加到fd_set中。
- 当一个连接被获取上来时,不能直接读取和发送,必须先把连接添加到fd_set中。
- 基于tcp协议,存在粘包问题,这里暂时不做处理
- 对于发送数据,是默认直接发,因为写缓冲区被写满的可能性很小,暂时不做处理
初始化&&启动服务器
服务器的主体结构
using namespace Net_Work;
const int gbacklog = 5;
const int num = sizeof(fd_set) * 8;
class SelectServer
{
public:
SelectServer(int port) : _port(port), _listensock(new TcpSocket()), _stop(false), fd_nums(num, nullptr)
{
}
void Init()
{
// 创建
_listensock->BuildListenSocketMethod(_port, gbacklog);
// 初始化
fd_nums[0] = _listensock.get();
}
void Loop()
{
while (!_stop)
{
// 不能直接监听,把交给select
fd_set rfds;
FD_ZERO(&rfds);
// 将listen添加到集合中
// FD_SET(_listensock->GetSockfd(), &rfds);
// select 等待
// 将fd_nums集合填充进fd_set
int maxfd = _listensock->GetSockfd();
for (auto &sock : fd_nums)
{
if (sock)
{
maxfd = std::max(maxfd, sock->GetSockfd());
FD_SET(sock->GetSockfd(), &rfds);
}
}
struct timeval tv
{
5, 0
};
// int n = select(_listensock->GetSockfd() + 1, &rfds, nullptr, nullptr, &tv);
PrintSet();
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &tv);
switch (n)
{
case 0:
ILOG("事件未就绪...,last time%u.%u", tv.tv_sec, tv.tv_usec);
break;
case -1:
DLOG("select error");
default:
ILOG("事件就绪,last time%u.%u", tv.tv_sec, tv.tv_usec);
HandlerEvent(rfds);
break;
}
sleep(1);
}
_stop = true;
}
~SelectServer()
{
_stop = true;
_listensock->CloseSockFd();
}
private:
void HandlerEvent(fd_set &rfds)
{
// 遍历rfds
for (int i = 0; i < num; i++)
{
if (fd_nums[i])
{
int fd = fd_nums[i]->GetSockfd();
if (FD_ISSET(fd, &rfds))
{
// 一个连接就绪的可能:1.listen 2.read
if (fd == _listensock->GetSockfd())
{
HandlerAccept();
}
// 普通sock //简单的读写
else
{
HandlerRead(i);
}
}
}
}
}
void HandlerAccept()
{
ILOG("获取一个新连接!");
std::string clientip;
uint16_t clientport;
Socket *sock = _listensock->AcceptConnection(&clientip, &clientport);
if (!sock)
{
DLOG("获取连接失败!");
return;
}
ILOG("获取连接成功!ip:%s port:%d", clientip.c_str(), clientport);
// 添加到fd_nums
int i = 0;
for (; i < num; i++)
{
if (!fd_nums[i])
{
fd_nums[i] = sock;
break;
}
}
// 满了!!
if (i == num)
{
WLOG("accept error!link full!!");
sock->CloseSockFd();
delete sock;
}
}
void HandlerRead(int i)
{
std::string buffer;
bool ret = fd_nums[i]->Recv(&buffer, 1024);
if (ret > 0)
{
std::cout << "client say#" << buffer << std::endl;
// 发回消息
std::string tmp = "你好client,我是server:" + buffer;
fd_nums[i]->Send(tmp);
}
// 异常或者直接关闭
else
{
ILOG("link break!!! maybe client quit or error");
// 关闭描述符
// 将数组的值置为空
fd_nums[i]->CloseSockFd();
delete fd_nums[i];
fd_nums[i] = nullptr;
}
}
void PrintSet()
{
std::cout << "fd_nums:";
for (auto &sock : fd_nums)
{
if (sock)
std::cout << sock->GetSockfd() << " ";
}
std::cout << std::endl;
}
private:
int _port;
bool _stop;
std::vector<Socket *> fd_nums; // 事件先添加进描述符数组
std::unique_ptr<Socket> _listensock;
};
这里就不做过多的介绍了。一个读事件如果就绪了,会有俩种:listensock上的新连接到来,
普通套接字上收到数据。对于这俩种情况分别处理。
处理新连接到来:必须添加到fd_set中
编写select多路转接的步骤
维护第三方容器保存关心的事件
- 将事件添加进fd_set
- 调用select等待
- 如果返回值>0继续处理
- 遍历第三方容器,比对关心的事件是否在fd_set的输出参数中
- 事件处理
不难发现,select编写存在大量的遍历。遍历是相当耗费时间的。
另外需要用户自己维护第三方数组。
select的特点
优点:
可以一次等待多个文件描述符,IO效率比较高。
缺点:
- 由于fd_set输入输出型参数的原因,每次都需要手动设置fd_set。需要利用第三方数组保存关心的fd。
- 调用select时候,会把集合从用户态拷贝到内核态,耗费时间。同样select返回时,也要将fd_set从内核态拷贝回用户态。
- 底层存在遍历事件,寻找就绪的事件。
- fd_set关心的事件有上限。我这里是4K。
针对select的这么多缺点,后来也引入许多解决方案。在下文将详细介绍比select更加优秀的poll和epoll