目录
2.3.2 利用select非阻塞receive客户端的“网络请求”
2.3.3 利用数组记录已链接的文件描述符(告诉内核遍历的文字描述符表范围)
一、非阻塞 Fcntl (修改文件描述符参数)
文件描述符默认文件读取为阻塞式读取,用标准输入这个文件描述符为例子,利用read函数读取文件描述符,当我们不输入任何字符,它会一直在终端等待我们输入,一直等待。
我们可以将该文件描述符设置为非阻塞,这样我们不必一直等待,如果没有数据就进行其他活动。
#pragma once
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
#include<stdio.h>
void setNonBlock(int fd)
{
//1.获得文件描述符flag
int f1 =fcntl(fd,F_GETFL);
if(f1<0)
{
std::cerr<<"fcntl"<<strerror(errno)<<std::endl;
return;
}
//2.添加非阻塞flag
fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}
void Printlog()
{
std::cout<<"this is a log task"<<std::endl;
}
void download()
{
std::cout<<"this is a download task"<<std::endl;
}
#include<vector>
#include<functional>
using fun_t = std::function<void()>;
//C式宏函数操作
#define INIT(v) do{\
v.push_back(Printlog);\
}while(0)
#define EXEC_OTHER(cbs) do{\
for(auto const & cb : cbs) cb();\
}while(0)
int main()
{
std::vector<fun_t> callbacks;
INIT(callbacks);
char buffer[1024];
setNonBlock(0);
while(true)
{
// std::cout<<">>>";
// fflush(stdout);
ssize_t n = read(0,buffer,sizeof(buffer)-1);//IO读取
if(n > 0)
{
buffer[n-1] = 0;
std::cout<<"echo# "<<buffer<<std::endl;
}
else if(n == 0)
{
std::cout<<"read end"<<std::endl;
break;
}
else
{
//数据没准备好
if(errno == EAGAIN)
{
std::cout<<"我没错,只是没有数据"<<std::endl;
EXEC_OTHER(callbacks);
}
//信号阻止
else if(errno == EINTR)
{
continue;
}
//真的出问题了!
else{
std::cout<<"result n ="<< n <<" errno:"<<strerror(errno)<<std::endl;
}
}
sleep(5);
}
return 0;
}
二、多路转接select
2.1 select函数功能
通过让程序监视多个文件描述符的状态变化来实现的。当至少有一个被监视的文件描述符的读写时间就绪时,select会通知上层进行读取或者写入(本质是进行拷贝)。
2.2 函数参数接口
- nfds:这是一个整数,表示需要监视的文件描述符集合中的最大文件描述符值加一。例如,如果需要监视3个文件描述符,分别为3、4和7,那么传给nfds的值应该是8。
- readfds、writefds和exceptfds:这些参数都是指向fd_set类型数据的指针,分别用于指定需要监视的可读文件描述符集合、可写文件描述符集合和异常文件描述符集合。fd_set是一个位图,每一位代表一个文件描述符,位的状态(0或1)表示该文件描述符是否被select监听。设置这个函数和指针,可以控制select关注的是哪种事件还是多种事件!
对fd_set进行操作通过函数调用完成,图片中下面的四个函数:
1.不关心某个文件描述符
2.是否该文件描述符设为了关心状态
3.设置某个文件描述符为关心状态
4.清空该集合
3. timeout:这是一个指向struct timeval类型的指针,用于设置select的等待时间。struct timeval包含两个成员,分别是秒(tv_sec)和微秒(tv_usec)。如果timeout参数为nullptr,select会阻塞等待,直到有一个或多个文件描述符就绪;如果timeout参数为{0, 0},select会使用非阻塞等待模式,需要程序员编写轮询检测代码;如果timeout参数设置了具体值,如{5, 0},select会在5秒内阻塞等待,如果在5秒内有文件描述符就绪,则通知上层进程;如果没有则超时返回。
select函数返回值有三种情况:
- 返回值大于0表示有相应个文件描述符就绪。
- 返回值等于0表示没有文件描述符就绪,超时返回。
- 返回值小于0表示select调用失败。
2.3 利用多路转接结合网络IO场景代码
2.3.1 利用select非阻塞处理链接请求Accept
创建完套接字以后服务端通过监听链接请求,调用Accept函数获取链接(本质是约定两端IO的文件描述符),listen套接字本身也是文件描述符,通过accept函数获取链接请求,如果没有链接请求,就会采取阻塞式等待。现在我们利用select,让该文件描述符的等待让select完成,一旦有链接请求,则再accept获取链接的文件描述符。
2.3.2 利用select非阻塞receive客户端的“网络请求”
同样,当链接建立成功,就可以进行网络通信,客户端发送请求,服务端提供网络服务。服务端通过receive函数调用获取请求,同样是阻塞式的获取请求,我们可以让select非阻塞等待网络请求。
2.3.3 利用数组记录已链接的文件描述符(告诉内核遍历的文字描述符表范围)
将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd ,一是用于再 select 返回后, array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd逐一加入 (FD_ZERO 最先 ) ,扫描 array 的同时取得 fd 最大值 maxfd ,用于 select 的第一个参数。结合select第一个参数让其关心maxfd+1,每次获取新的链接我们都要更新这个maxfd。
我们设置一个整形数组,数组存放文件描述符。数组大小是位图所能管理的最大文件描述符个数。设置默认下标值为-1,不为默认值的下标值代表某个链接,我们遍历整个数组,与用当前数组的下标值比较,用更大的文件描述符就更新,这样我们就可以让select帮我们等待更多的请求。
断开链接,对应的文件描述符就close,数组下标恢复默认值。
2.3.4 实现代码
#pragma once
#include <iostream>
#include<functional>
#include<cstdio>
#include "sock.hpp"
static const int defaultport = 8080;
static const int fdnum = sizeof(fd_set) * 8; //最大位图表示的文字描述符
static const int defaultfd = -1;
namespace select_ns
{
using func_t = function<string&(string&)>;
class SelectServer
{
public:
SelectServer(func_t cb,int port = defaultport):_f(cb), _port(port), _listensock(-1),fd_array(nullptr)
{}
void Print()
{
std::cout<<"fd list: ";
for(int i = 0;i<fdnum;i++)
{
if(fd_array[i] != defaultfd)
std::cout<<fd_array[i]<<" ";
}
std::cout<<std::endl;
}
void initServer()
{
_listensock = sock::GetSocket(_port); //获取监听套接字
sock::Bind(_listensock, _port); //绑定套接字
sock::Listen(_listensock); //监听
fd_array = new int[fdnum]; //第三方数组辅助
for(int i = 0;i<fdnum;i++) fd_array[i] = -1;
fd_array[0] = _listensock; //0号下标设置为监听文字描述符
}
//建立链接服务
void Accepter()
{
std::string clientip;
uint16_t clientport;
int sock = sock::Accept(_listensock, &clientip, &clientport); //建立链接!!!
if (sock < 0)
return;
logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
int i = 0;
for (; i < fdnum; i++)
{
if (fd_array[i] != defaultfd)
continue;
else
break;
}
if (i == fdnum)
{
logMessage(WARNING, "server is full ,please wait");
close(sock);
}
else
{
fd_array[i] = sock; //数组添加新的链接文件描述符
}
Print();
}
//请求处理服务
void Recver(int fd,int index)
{
char buffer[1024];
while(true) //网络TCP/IP协议以字节流传输,所以我们为了读到完整报文,需要循环读取
{
int n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout<<"client echo# "<<buffer<<endl;
}
else if (n == 0)
{
close(fd);
fd_array[index] = defaultfd;
logMessage(NORMAL, "client quit!");
return;
}
else
{
close(fd);
fd_array[index] = defaultfd;
logMessage(ERROR, "recv error, code: %d, error string: %s", errno, strerror(errno));
return;
}
string request = buffer;
string resp = _f(request);
resp = "server response# " + resp;
write(fd, resp.c_str(), resp.size());
}
}
//处理IO
void HandlerEvent(fd_set &rfds)
{
for(int i=0;i<fdnum;i++)
{
if(fd_array[i] == defaultfd) continue;
if(fd_array[i] == _listensock && FD_ISSET(_listensock,&rfds))
Accepter(); //建立链接
else if(FD_ISSET(fd_array[i],&rfds))
Recver(fd_array[i],i); //处理IO请求
}
}
//启动服务端服务器
void start()
{
while (true)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0]; //maxfd ->让select遍历到哪里?
for(int i = 0;i<fdnum;i++)
{
if(fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i],&rfds);
if(maxfd < fd_array[i]) maxfd = fd_array[i];
}
struct timeval timeout = {3,0}; //设置等待时间为3s
// select存在的问题
//1.位图结构 处理的请求文件描述符有上限
//2.必须借助第三方数组
//3.调用的参数都是输入输出类型的参数,所以每次都要更新fd_set
//4.需要确定遍历范围
//5.select采用位图,用户->内核 内核->用户 会来回拷贝,拷贝成本大
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
switch(n)
{
case 0:
logMessage(NORMAL, "timeout...");
break;
case -1:
logMessage(WARNING, "select error, code: %d,error string describes: %s", errno, strerror(errno));
break;
default:
logMessage(NORMAL,"get a new link");
HandlerEvent(rfds);
break;
}
}
}
~SelectServer()
{
if(_listensock < 0)
close(_listensock);
}
private:
func_t _f; //处理的回调函数
int _port;
int _listensock;
int* fd_array;
};
}
三、多路转接之poll
3.1 poll函数功能
// pollfd 结构struct pollfd {int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */};fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.nfds表示fds数组的长度.timeout表示poll函数的超时时间, 单位是毫秒(ms)返回值:返回值小于0, 表示出错;返回值等于0, 表示poll函数等待超时;返回值大于0, 表示poll由于监听的文件描述符就绪而返回
3.2 poll的优缺点
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。1. pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比 select更方便。2. poll并没有最大数量限制 (但是数量过大后性能也是会下降)。
3.3 在select模式基础上修改成poll模式
#pragma once
#include <iostream>
#include<functional>
#include<cstdio>
#include<poll.h>
#include "sock.hpp"
static const int defaultport = 8080;
static const int defaultfd = -1;
static const int num = 10; //默认最大关心数 也可以改为动态的
namespace poll_ns
{
using func_t = function<string&(string&)>;
class PollServer
{
public:
PollServer(func_t cb,int port = defaultport):_f(cb), _port(port), _listensock(-1),_rfds(nullptr)
{}
void Print()
{
std::cout<<"fd list: ";
for(int i = 0;i<num;i++)
{
if(_rfds[i].fd != defaultfd)
std::cout<<_rfds[i].fd<<" ";
}
std::cout<<std::endl;
}
void initServer()
{
_listensock = sock::GetSocket(_port);
sock::Bind(_listensock, _port);
sock::Listen(_listensock);
_rfds = new struct pollfd[num];
for(int i =0; i<num;i++)
{
_rfds[i].fd = defaultfd;
_rfds[i].events = 0;
_rfds[i].revents = 0;
}
_rfds[0].fd = _listensock;
_rfds[0].events = POLLIN;
}
void Accepter()
{
std::string clientip;
uint16_t clientport;
int sock = sock::Accept(_listensock, &clientip, &clientport);
if (sock < 0)
return;
logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
int i = 0;
for (; i < num; i++)
{
if (_rfds[i].fd != defaultfd)
continue;
else
break;
}
if (i == num)
{
logMessage(WARNING, "server is full ,please wait");
close(sock);
}
else
{
_rfds[i].fd = sock;
_rfds[i].events = POLLIN;
}
Print();
}
void Recver(int fd,int index)
{
char buffer[1024];
while(true)
{
int n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout<<"client echo# "<<buffer<<endl;
}
else if (n == 0)
{
close(fd);
_rfds[index].fd = defaultfd;
_rfds[index].events = 0;
logMessage(NORMAL, "client quit!");
return;
}
else
{
close(fd);
_rfds[index].fd = defaultfd;
_rfds[index].events = 0;
logMessage(ERROR, "recv error, code: %d, error string: %s", errno, strerror(errno));
return;
}
string request = buffer;
string resp = _f(request);
resp = "server response# " + resp;
write(fd, resp.c_str(), resp.size());
}
}
void HandlerEvent()
{
for(int i=0;i<num;i++)
{
if(_rfds[i].fd == defaultfd) continue;
if(!(_rfds[i].events & POLLIN)) continue;
if(_rfds[i].fd == _listensock && (_rfds[i].revents & POLLIN))
Accepter();
else if(_rfds[i].revents == POLLIN)
Recver(_rfds[i].fd,i);
}
}
void start()
{
while (true)
{
int timeout = 1000;
int n = poll(_rfds,num,timeout);
switch(n)
{
case 0:
logMessage(NORMAL, "timeout...");
break;
case -1:
logMessage(WARNING, "poll error, code: %d,error string describes: %s", errno, strerror(errno));
break;
default:
logMessage(NORMAL,"get a new link");
HandlerEvent();
break;
}
}
}
~PollServer()
{
if(_listensock < 0)
close(_listensock);
}
private:
func_t _f;
int _port;
int _listensock;
struct pollfd * _rfds; //关心的事件
};
}