前言
上文介绍了五种IO模型。本文将介绍五种IO模型之一的多路转接。多路复用的优势在于同一时间可以等待多个文件描述符。提高了IO的效率。在现代计算机中IO效率最慢的就是网络通信。本文将介绍多路转接的初始模型:select。了解select的工作原理,并且编写网络服务器。
seletc函数是用来等待的!并不负责拷贝,拷贝是交由read\send来进行
#include
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
参数解释
返回值
出错时候的错误码
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足
参数timeout的介绍
关于事件位图
fd_set的本质就是一张位图,一般最多能接受sizeof(fd_set)是512字节,也就是8*sizeof(fd_set)4K大小的事件。
fd_set的添加/删除/修改事件都必须通过特定的宏函数
如果关心某个事件,就会把事件添加到fd_set 位图中。比如关心0号文件描述符的读事件,就先将0号fd添加到位图中,然后调用select等待。
timeout和fd_set都是输入输出型参数
所以每一次调用select之前,都必须被fd_set设置。这是一个很麻烦的操作!
所以需要借助第三方容器将要关心的fd事件提前保留下来,等下一次select前添加到fd_set中。
另外select可以关心读事件、写事件、异常事件如果其中有一项不想关心,设为nullptr即可。
什么叫做事件就绪?
比如俩个主机建立TCP通信。主机A给主机B发消息。数据来不及发出去,一直发导致写缓冲区满了 ,那么 写事件就是不就绪。
B主机上的接收缓冲区一旦有数据,就代表建立连接的sockfd上读事件就绪!
编写一个基于TCP通信的select模型
要点:
初始化&&启动服务器
服务器的主体结构
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 fd_nums; // 事件先添加进描述符数组
std::unique_ptr _listensock;
};
这里就不做过多的介绍了。一个读事件如果就绪了,会有俩种:listensock上的新连接到来,
普通套接字上收到数据。对于这俩种情况分别处理。
处理新连接到来:必须添加到fd_set中
维护第三方容器保存关心的事件
不难发现,select编写存在大量的遍历。遍历是相当耗费时间的。
另外需要用户自己维护第三方数组。
优点:
可以一次等待多个文件描述符,IO效率比较高。
缺点:
针对select的这么多缺点,后来也引入许多解决方案。在下文将详细介绍比select更加优秀的poll和epoll