read/recv 等 文件接口只有一个文件描述符
想要 让一个接口等待多个文件描述符,而read等接口是不具备这个能力的
操作系统就设计一个接口 select,用于多路复用
select 作用
1.等待多个文件描述符
2.只负责等(没有数据拷贝的能力)
输入 man select
由于select只负责等待,不负责拷贝,所以没有缓冲区
第一个参数 nfds,是一个输入型参数 ,表示 select等待的多个文件描述符(fd)数字层面 最大的+1
(文件描述符的本质为 数组下标,多个文件描述符中 数值最大的文件描述符值+1 即nfds )
用户把数据交给操作系统,同样操作系统也要 通过这些输出型参数 把结果 交给用户
为了让 用户 和 操作系统之间进行信息传递,就把参数设置为 输入 输出型参数
timeout 是一个 输入 输出型参数
timeout的数据类型 为struct timeval
可以一个时间结构体,tv_sec 表示 秒, tv_usec 表示 微秒
对于 struct timeval的对象 可设置三种值
第一种 对象被设为 NULL ,对于select来说 表示 阻塞等待
(多个文件描述符任何一个都不就绪,select就一直不返回)
第二种 struct timeval对象定义出来,并将其中变量都设为0
对于select来说 表示 非阻塞等待
(多个文件描述符任何一个都不就绪,select就会立马出错 并返回)
第三种 struct timeval对象定义出来,并将其中变量设为 5 和 0
表示 5s以内 阻塞等待,否则 就 timeout(非阻塞等待) 一次
若在第3s时 有一个文件描述符就绪,则select就会返回 其中参数 timeout 表示 剩余的时间 2s(5-3=2)
readfds writefds exceptfds 这三个参数 是同质的
readfds 表示 读事件
writefds 表示 写事件
excepttfds表示 异常事件
三者类型都为 fd_set
fd_set是一个位图结构,用其表示多个文件描述符
通过比特位的位置, 就代表文件描述符数值是谁
位图结构想要使用 按位与、按位或 这些操作,必须使用操作系统提供的接口
FD_CLR :将指定的文件描述符从 指定的集合中清除
FD_ISSET:判断文件描述符是否在该集合中被添加
FD_SET: 将一个文件描述符添加到对应的set集合中
FD_ZERO:将文件描述符整体清空
以readfds 读事件为例
若放入 readfds 集合中,用户告诉内核 ,那些文件描述符对应的读事件需要由 内核 来关心
返回时,内核要告诉用户,那些文件描述符的读事件已经就绪
假设想让操作系统去关心八个文件描述符对应的事件
用户想告诉内核时,用户需 定义 fd_set 对象 rfds ,其中八个比特位设置为1
比特位的位置表示几号文件描述符
比特位被置1,则操作系统就需要关心 对应的几号文件描述符
如:需要关心 1-8号文件描述符,即查看是否就绪
当select返回时, 内核会告诉用户,rfds重置,并将 就绪的文件描述符 对应 的 比特位位置 置1
如: 3号和5号就绪,则对应比特位 位置 置1 ,表示3号和5号文件描述符 对应的内容就绪
select的返回值 同样也有三种情况
第一种 大于0
表示有几个文件描述符 是就绪的
第二种 等于0
进入timeout状态 ,即 5s以内没有任何一个文件描述符 就绪
第三种 小于0
等待失败 返回-1
如:想要等待下标为1 和2的文件描述符,但是下标为2的文件描述符根本不存在,就会等待失败
本次实现分为V1版本和V2版本
V1版本 只能为 读事件
而V1版本 除了有读事件 还可以处理 写事件和异常事件
同时两者只有selectserver.hpp有区别,其他都是一样的
首先设置一个rfds集合,用来表示读集合
在使用 FD_ZERO 将读集合清空
将listensock套接字 添加到 读集合rfds中
由于当前只看读集合,所以写 和异常集合都为空,同时将timeout设置为阻塞等待,即输入才返回
slect的返回值 用n接收 ,使用switch case 来区分 返回值
创建 HandlerEvent函数,来处理就绪事件
因为只有n大于0时,才有文件描述符就绪,所以将其放入default中
但是这样是存在问题的,当不断会有listensock套接字就绪时,文件描述符会变多 即将套接字 放入 rfds集合中
当select返回时,rfds大部分位图可能会被清空,就没办法保证 对之前的文件描述符有持续的监控能力
select服务器,在使用的时候,需要程序员自己维护一个第三方数组,来进行已经获得的sock进行管理
先通过typedef 将int类型定义 为type_t
定义出一个整形数组 fdarray,并设置数组大小为N (位图大小)
将listensock套接字的文件描述符 作为fdarray 数组的第一个元素
由于select函数的第一个函数 是所有文件描述符 最大值+1
所以通过maxfd 记录最大值
每次都从数组下标为0的位置处开始 向后寻找
而初始化时,将数组的所有元素都设为-1
所以当寻找到不为-1的数时,就将对应的文件描述符 添加到rfds读集合中
这样就可以保证在查询时,可以寻找到历史的文件描述符
继续遍历fdarray数组,跳过数组元素为-1的
若为合法文件描述符 则有两种情况:
listensock套接字 或者 普通文件描述符
是listensock套接字 并且在rfds集合中
为了方便观看,所以写了一个Accepter函数用于获取新连接的动作
此时就可以直接使用accept函数了,定义一个客户端IP和客户端端口号
用于获取客户端IP和端口号
定义sock 用于接收返回值,当返回值sock大于0时,即获取连接成功
因为下标为0处,已经被listensock套接字占用了,所以从下标为1位置开始
找到数组元素为-1(表示该位置没有被使用)
将返回值sock赋值给对应的数组元素
不是listensock套接字 但在rfds集合中 即普通文件描述符
使用recv函数 ,将文件描述符fd中的数据 发送到 buffer中
若返回值s大于0,则表示返回成功
在buffer数据的基础上,添加 select server echo ,通过 send 函数 重新发送给文件描述符fd中,被select管理
返回值的其他情况,都是打印信息
最后关闭文件
//SelectServer V1版本
#include
#include
#include
#include
#include"Sock.hpp"
#include"Log.hpp"
#include"Err.hpp"
using namespace std;
const static int gport=8888;
typedef int type_t;//定义一个整形数组
class SelectServer
{
static const int N=sizeof(fd_set)*8;//N对应位图大小
public:
SelectServer(uint16_t port=gport)
:port_(port)
{}
void InitServer()//初始化
{
listensock_.Socket();//创建套接字
listensock_.Bind(port_);//绑定
listensock_.Listen();//设置监听状态
//对fdarray数组进行初始化
for(int i=0;i<N;i++)
{
fdarray_[i]= defaultfd;
}
}
void Accepter()//获取新连接的动作
{
//这里再使用accept 就不会阻塞了
//listen套接字底层一定有就绪的事件 即连接已经到来了
string clientip;
uint16_t clientport;
int sock=listensock_.Accept(&clientip,&clientport);//获取客户端IP和端口号
if(sock<0)
{
return;
}
//当得到对应新连接的sock套接字,是不能进行read/recv
//并不知道sock上的数据是否就绪的
//所以需要将sock交给select,由select进行管理
logMessage(Debug,"[%s:%d],sock:%d",clientip.c_str(),clientport,sock );
//只需把新获取的sock 添加到 数组中
int pos=1;
for(;pos<N;pos++)
{
if(fdarray_[pos]==defaultfd)//说明没有被占用
{
break;
}
}
if(pos>=N)//整个数组中的位置全被占用了
{
close(sock);
logMessage(Warning,"sockfd[] array full");
}
else //找到了对应的位置
{
fdarray_[pos]=sock;
}
}
void HandlerEvent(fd_set &rfds)//处理就绪事件
{
for(int i=0;i<N;i++)
{
if(fdarray_[i]==defaultfd)
{
continue;
}
//合法fd
//若套接字为listensock套接字
if(fdarray_[i]==listensock_.Fd() &&FD_ISSET(listensock_.Fd(),&rfds))
{
Accepter();
}
//若套接字不是listensock套接字,但在rfds集合中
else if ((fdarray_[i] != listensock_.Fd()) && FD_ISSET(fdarray_[i], &rfds))
{
//普通文件描述符就绪
int fd=fdarray_[i];
char buffer[1024];
ssize_t s=recv(fd,buffer,sizeof(buffer)-1,0);
//读取不会被阻塞
if(s>0)//读取成功
{
buffer[s-1]=0;
cout<<"client# "<<buffer<<endl;
//发送回去 也要被select管理
string echo=buffer ;
echo+= "[select server echo ]";
send(fd,echo.c_str(),echo.size(),0);//发送消息 将echo内的数据 交给fd
}
else
{
if(s==0)//读到文件结尾
{
logMessage(Info,"client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd);
}
else //读取失败
{
logMessage(Warning,"recv error,client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd);
}
close(fdarray_[i]);
fdarray_[i]=defaultfd;
}
}
}
}
void DebugPrint()
{
cout<<"fdarray_[]:"<<endl;
for(int i=0;i<N;i++)
{
if(fdarray_[i]==defaultfd)
{
continue;
}
cout<<fdarray_[i]<<" ";
}
cout<<"\n";
}
void Start() //启动
{
//在网络中,新连接到来被当作 读事件就绪
//对应不同的事件就绪,做出不同的动作
fdarray_[0]=listensock_.Fd();//先将listensock套接字添加到数组中
while(true)
{
//因为rfds是输入 输出型参数,就注定了每次都要对rfds进行重置
//重置 就通过 fdarray数组知道 历史上有那些fd
//因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一直在变化
//maxfd也要动态更新
fd_set rfds;//作为读文件描述符集合
FD_ZERO(&rfds);//将集合清空
int maxfd=fdarray_[0];
for(int i=0;i<N;i++)
{
if(fdarray_[i]==defaultfd)
{
continue;
}
//将合法fd添加到rfds集合中
FD_SET(fdarray_[i],&rfds);
if( maxfd<fdarray_[i])
{
maxfd=fdarray_[i];
}
}
int n=select( maxfd+1,&rfds,nullptr,nullptr,nullptr);//将listensock套接字添加到读文件描述符集合中
//timeout 设为nullptr后,全部为阻塞等待
switch(n)
{
case 0: //表示没有任何一个文件描述符就绪
logMessage(Debug,"timeout,%d: %s",errno,strerror(errno));
break;
case -1: //等待失败 返回-1
logMessage(Warning,"%d: %s",errno,strerror(errno));
break;
default: //大于0 ,则表示成功 返回有多少文件描述符就绪
logMessage(Debug,"有一个就绪事件发生了:%d",n);
HandlerEvent(rfds);//处理就绪事件
DebugPrint();//打印数组内容
break;
}
}
}
~SelectServer()
{
listensock_.Close();
}
private:
uint16_t port_;//端口号
Sock listensock_;//创建Sock对象
type_t fdarray_[N];//自己定义一个数组,与位图大小相同,来进行已经获得的sock进行管理
};
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,//2
BIND_ERR,//3
LISTEN_ERR,//4
SETSID_ERR,//5
OPEN_ERR//6
};
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
const std::string filename="tecpserver.log";
//日志等级
enum{
Debug=0, // 用于调试
Info , //1 常规
Warning, //2 告警
Error , //3 一般错误
Tatal , //4 致命错误
Uknown//未知错误
};
static std::string tolevelstring(int level)//将数字转化为字符串
{
switch(level)
{
case Debug : return "Debug";
case Info : return "Info";
case Warning : return "Warning";
case Error : return "Error";
case Tatal : return "Tatal";
default: return "Uknown";
}
}
std::string gettime()//获取时间
{
time_t curr=time(nullptr);//获取time_t
struct tm *tmp=localtime(&curr);//将time_t 转换为 struct tm结构体
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
void logMessage(int level, const char*format,...)
{
//日志左边部分的实现
char logLeft[1024];
std::string level_string=tolevelstring(level);
std::string curr_time=gettime();
snprintf(logLeft,sizeof(logLeft),"%s %s %d",level_string.c_str(),curr_time.c_str());
//日志右边部分的实现
char logRight[1024];
va_list p;//p可以看作是1字节的指针
va_start(p,format);//将p指向最开始
vsnprintf(logRight,sizeof(logRight),format,p);
va_end(p);//将指针置空
//打印日志
printf("%s%s\n",logLeft,logRight);
//保存到文件中
FILE*fp=fopen( filename.c_str(),"a");//以追加的方式 将filename文件打开
//fopen打开失败 返回空指针
if(fp==nullptr)
{
return;
}
fprintf(fp,"%s%s\n",logLeft,logRight);//将对应的信息格式化到流中
fflush(fp);//刷新缓冲区
fclose(fp);
}
#include"SelectServer.hpp"
#include
int main()
{
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->InitServer();//初始化
svr->Start();//启动
return 0;
}
SelectServer:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f SelectServer
#include
#include
#include
#include
#include
#include
#include
#include"Log.hpp"
#include"Err.hpp"
static const int gbacklog=32;
static const int defaultfd=-1;
class Sock
{
public:
Sock() //构造
:_sock(defaultfd)
{
}
void Socket()//创建套接字
{
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)//套接字创建失败
{
logMessage( Tatal,"socket error,code:%s,errstring:%s",errno,strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(uint16_t port)//绑定
{
struct sockaddr_in local;
memset(&local,0,sizeof(local));//清空
local.sin_family=AF_INET;//16位地址类型
local.sin_port= htons(port); //端口号
local.sin_addr.s_addr= INADDR_ANY;//IP地址
//若小于0,则绑定失败
if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage( Tatal,"bind error,code:%s,errstring:%s",errno,strerror(errno));
exit(BIND_ERR);
}
}
void Listen()//将套接字设置为监听状态
{
//小于0则监听失败
if(listen(_sock,gbacklog)<0)
{
logMessage( Tatal,"listen error,code:%s,errstring:%s",errno,strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string *clientip,uint16_t * clientport)//获取连接
{
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
int sock=accept(_sock,(struct sockaddr*)&temp,&len);
if(sock<0)
{
logMessage(Warning,"accept error,code:%s,errstring:%s",errno,strerror(errno));
}
else
{
//inet_ntoa 4字节风格IP转化为字符串风格IP
*clientip = inet_ntoa(temp.sin_addr) ; //客户端IP地址
//ntohs 网络序列转主机序列
*clientport= ntohs(temp.sin_port);//客户端的端口号
}
return sock;//返回新获取的套接字
}
int Connect(const std::string&serverip,const uint16_t &serverport )//发起链接
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));//清空
server.sin_family=AF_INET;//16位地址类型
server.sin_port=htons(serverport);//端口号
//inet_addr 字符串风格IP转化为4字节风格IP
server.sin_addr.s_addr=inet_addr(serverip.c_str());//IP地址
//成功返回0,失败返回-1
return connect(_sock, (struct sockaddr*)&server,sizeof(server));
}
int Fd()
{
return _sock;
}
void Close()
{
if(_sock!=defaultfd)
{
close(_sock);
}
}
~Sock()//析构
{
}
private:
int _sock;
};
相比于V1版本,V2版本的 type_t 类型从 int整形 变为 结构体
由于数组的每一个元素都是结构体,所以就需要都每一个成员进行初始化
同时分别通过第一个、第二个、第三个比特位 来表示读事件、写事件、异常事件
由于数组的类型是一个结构体,对应的下标为0位置处的元素 也是要说明 结构体对应的成员
将下标为0位置处的元素 (lisetnsock套接字) 设置为读事件
在循环中,设置rfds(读集合)、wfds(写集合)
分别将两者清空
同样与V1相同,需要设置maxfd,用于slect的第一个参数
还需判断,当前的数组元素 是读集合还是写集合
最终将两者添加到select函数中
由于没有异常集合,所以设置为nullptr
相比于V1,多了写事件,所以参数也应该多传一个
由于有读事件和写事件,所以 if 判断 和 else if 判断 区分
若为读事件,并且数组的元素 在rfds读集合中
则进入 if 判断
进行V1操作 即判断是 listensock套接字 还是 普通文件描述符
这里为了方便观看,所以把 普通文件描述符代码放入 Recver中
若为写事件,并且数组的元素在wfds写集合中
则进入else if 判断
#include
#include
#include
#include
#include "Sock.hpp"
#include "Log.hpp"
#include "Err.hpp"
using namespace std;
const static int gport = 8888;
const static int defaultevent=0;
// 通过第一个 第二个 第三个比特位 来表示 读 写 异常
#define READ_EVENT (0x1) // 读事件
#define WRITE_EVENT (0x1 << 1) // 写事件
#define EXCEPT_EVENT (0x1 << 2) // 异常事件
typedef struct FdEvent
{
int fd;
uint8_t event; // 表示关心这个文件描述符上的什么事件
string clientip; // 该文件描述符是那个客户端ip地址
uint16_t clientport; 该文件描述符是那个客户端的端口号
} type_t;
class SelectServer
{
static const int N = sizeof(fd_set) * 8; // N对应位图大小
public:
SelectServer(uint16_t port = gport)
: port_(port)
{
}
void InitServer() // 初始化
{
listensock_.Socket(); // 创建套接字
listensock_.Bind(port_); // 绑定
listensock_.Listen(); // 设置监听状态
// 对fdarray数组进行初始化
for (int i = 0; i < N; i++)
{
fdarray_[i].fd = defaultfd;
fdarray_[i].event = defaultevent; // 默认为0 表示一个事件都不关心
fdarray_[i].clientport = 0;
}
}
void Accepter() // 获取新连接的动作
{
// 这里再使用accept 就不会阻塞了
// listen套接字底层一定有就绪的事件 即连接已经到来了
string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport); // 获取客户端IP和端口号
if (sock < 0)
{
return;
}
// 当得到对应新连接的sock套接字,是不能进行read/recv
// 并不知道sock上的数据是否就绪的
// 所以需要将sock交给select,由select进行管理
logMessage(Debug, "[%s:%d],sock:%d", clientip.c_str(), clientport, sock);
// 只需把新获取的sock 添加到 数组中
int pos = 1;
for (; pos < N; pos++)
{
if (fdarray_[pos].fd == defaultfd) // 说明没有被占用
{
break;
}
}
if (pos >= N) // 整个数组中的位置全被占用了
{
close(sock);
logMessage(Warning, "sockfd[] array full");
}
else // 找到了对应的位置
{
fdarray_[pos].fd = sock;
fdarray_[pos].event= READ_EVENT;
fdarray_[pos].clientip=clientip;
fdarray_[pos].clientport=clientport;
}
}
void Recver(int index)
{
// 普通文件描述符就绪
int fd = fdarray_[index].fd;
char buffer[1024];
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
// 读取不会被阻塞
if (s > 0) // 读取成功
{
buffer[s - 1] = 0;
cout <<fdarray_[index].clientip <<":" <<fdarray_[index].clientport<< buffer << endl;
// 发送回去 也要被select管理
string echo = buffer;
echo += "[select server echo ]";
send(fd, echo.c_str(), echo.size(), 0); // 发送消息 将echo内的数据 交给fd
}
else
{
if (s == 0) // 读到文件结尾
{
logMessage(Info, "client quit...,fdarray_[i] -> defaultfd:%d->%d", fd, defaultfd);
}
else // 读取失败
{
logMessage(Warning, "recv error,client quit...,fdarray_[i] -> defaultfd:%d->%d", fd, defaultfd);
}
close(fdarray_[index].fd);
fdarray_[index].fd = defaultfd;
fdarray_[index].event=defaultevent;
fdarray_[index].clientip.resize(0);
fdarray_[index].clientport=0;
}
}
void HandlerEvent(fd_set &rfds, fd_set &wfds) // 处理就绪事件
{
for (int i = 0; i < N; i++)
{
if (fdarray_[i].fd == defaultfd)
{
continue;
}
// 合法fd
// 若为读事件,并且读事件已经在rfds集合中
if ((fdarray_[i].event & READ_EVENT) && FD_ISSET(fdarray_[i].fd, &rfds))
{
// 处理读取
// 1.accept 2.recv
// 若套接字为listensock套接字
if (fdarray_[i].fd == listensock_.Fd())
{
Accepter();
}
// 若套接字不是listensock套接字,但在rfds集合中
else if ((fdarray_[i].fd != listensock_.Fd()) )
{
Recver(i);
}
else
{}
}
//若为写事件,并且写事件已经在rfds集合中
else if ((fdarray_[i].event & WRITE_EVENT) && FD_ISSET(fdarray_[i].fd, &wfds))
{
}
else
{
//异常事件
}
}
}
void DebugPrint()
{
cout << "fdarray_[]:" << endl;
for (int i = 0; i < N; i++)
{
if (fdarray_[i].fd == defaultfd)
{
continue;
}
cout << fdarray_[i].fd << " ";
}
cout << "\n";
}
void Start() // 启动
{
// 在网络中,新连接到来被当作 读事件就绪
// 对应不同的事件就绪,做出不同的动作
fdarray_[0].fd = listensock_.Fd(); // 先将listensock套接字添加到数组中
fdarray_[0].event = READ_EVENT; // 设置为 读事件
while (true)
{
// 因为rfds是输入 输出型参数,就注定了每次都要对rfds进行重置
// 重置 就通过 fdarray数组知道 历史上有那些fd
// 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一直在变化
// maxfd也要动态更新
fd_set rfds; // 作为读文件描述符集合
fd_set wfds; // 作为写文件描述符集合
FD_ZERO(&rfds); // 将读集合清空
FD_ZERO(&wfds); // 将写集合清空
int maxfd = fdarray_[0].fd;
for (int i = 0; i < N; i++)
{
if (fdarray_[i].fd == defaultfd)
{
continue;
}
// 将合法fd添加到rfds集合中
if (fdarray_[i].event & READ_EVENT) // 若当前为读,则添加到读集合中
{
FD_SET(fdarray_[i].fd, &rfds);
}
if (fdarray_[i].event & WRITE_EVENT) // 若当前为写,则添加到写集合中
{
FD_SET(fdarray_[i].fd, &wfds);
}
if (maxfd < fdarray_[i].fd)
{
maxfd = fdarray_[i].fd;
}
}
int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr); // 将listensock套接字添加到读文件描述符集合中
// timeout 设为nullptr后,全部为阻塞等待
switch (n)
{
case 0: // 表示没有任何一个文件描述符就绪
logMessage(Debug, "timeout,%d: %s", errno, strerror(errno));
break;
case -1: // 等待失败 返回-1
logMessage(Warning, "%d: %s", errno, strerror(errno));
break;
default: // 大于0 ,则表示成功 返回有多少文件描述符就绪
logMessage(Debug, "有一个就绪事件发生了:%d", n);
HandlerEvent(rfds, wfds); // 处理就绪事件
DebugPrint(); // 打印数组内容
break;
}
}
}
private:
uint16_t port_; // 端口号
Sock listensock_; // 创建Sock对象
type_t fdarray_[N]; // 自己定义一个数组,与位图大小相同,来进行已经获得的sock进行管理
};
文件描述符是有上限的,云服务器是1024
需要使用一个array数组保存slect中的文件描述符 (start最终版本有详细说)