文章目录
- 1、思路
- 2、select接口
- 3、实现
- 1、准备工作
- 2、实现等待多个fd
- 3、辨别连接和简单处理读事件
- 4、简单处理写、读事件
- 4、特点
1、思路
select就是多路转接IO。select能以某种形式,等待多个文件描述符,只要有哪个fd有数据就可以读取并全部返回。就绪的fd,要让用户知道。select等待的多个fd中,一定有少量或者全部都准备好了数据。
2、select接口
nfds输入型参数,表示select等待的多个fd中,fd对应的数 + 1
剩下四个参数都是输入输出型参数,意思是用户传入参数,操作系统通过这些参数把结果交给用户。
除去timeout,中间三个是同一个类型的,分别对应读、写、异常,只是用处不同,但思路相同。timeout的类型是一个结构体,表示select应该以什么方式来轮询检测,一共有3种值。NULL/nullptr表示阻塞等待,等待多个fd的时候就是把文件结构体的指针放到阻塞队列中,如果没有就绪的就不返回;{0,0}表示非阻塞等待,也就是等待多个文件描述符时,如果没有就绪的,就等待0s后出错返回;{n,m}表示n秒以内阻塞等待,超过这个时间就timeout一次,也就是非阻塞一次,也就是出错返回,如果n秒内得到了数据并返回,那么timeout此时就表示剩余时间。
返回值是一个int值,大于0表示有几个fd是就绪的,为0表示超时出错返回了,小于0表示等待失败。
readfd参数的类型是fd_set类型,是一个位图结构
用位图结构来表示多个fd,进行用户和内核之间的信息的互相传递。对于位图结构的操作只能用系统给定的接口。
FD_CLR把指定的描述符从指定的集合中清除;FD_ISSET查看指定fd是否在集合中;FD_SET添加fd到集合中;FD_ZERO将集合清空。
通过这个位图结构,用户告诉内核,用户关心哪些fd对应的文件有数据,内核要告诉用户,哪些fd的读事件就绪了。
假设用户这样设置,fd_set rfds:0110 111,表明文件描述符为4和7的不管,看fd为012356的文件;如果只有3号fd就绪了,内核传回来的rfds则是0000 1000;用户拿到rfds后,扫描位图,哪个位置为1就说明是哪个文件就绪了。
fd_set是OS提供的固定大小的数据类型,比特位数有上限,所以select能管理的fd也有上限。查看多大
#include <iostream>
#include <sys/select.h>
int main()
{
fd_set fd;
std::cout << sizeof(fd) << std::endl;
return 0;
}
字节数就是sizeof(fd) * 8。
3、实现
1、准备工作
makefile
selectserver:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f selectserver
main.cc
#include "SelectServer.hpp"
#include <memory>
int main()
{
//fd_set fd;
//std::cout << sizeof(fd) << std::endl;
std::unique_ptr<SelectServer> svr(new SelectServer(3389));
svr->InitServer();
svr->Start();
return 0;
}
SelectServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
class SelectServer
{
public:
SelectServer(uint16_t port): port_(port)
{}
void InitServer()
{
}
void Start()
{
}
~SelectServer()
{}
private:
uint16_t port_;
};
引入之前的Sock.hpp和log.hpp和err.hpp
Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "err.hpp"
#include "log.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(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
//设置地址是可复用的
int opt = 1;
setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(const uint16_t& port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()
{
if(listen(_sock, gbacklog) < 0)//第二个参数维护了一个队列,发送了连接请求但是服务端没有处理的客户端,服务端开始accept后,就会出现另一个队列,就是服务端接受了请求但还没被accept的客户端
{
logMessage(Fatal, "listen error, code: %d, 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: %d, errstring: %s", errno, strerror(errno));
}
else
{
*clientip = inet_ntoa(temp.sin_addr);//这个函数就可以从结构体中拿出ip地址,转换好后返回
*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;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
return connect(_sock, (struct sockaddr*)&server, sizeof(server));//先不打印消息
}
int Fd()
{
return _sock;
}
void Close()
{
if(_sock != defaultfd) close(_sock);
}
~Sock()
{}
private:
int _sock;
};
err.hpp
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
SETSID_ERR,
OPEN_ERR
};
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
const std::string filename0 = "log/tcpserver.log.Debug";
const std::string filename1 = "log/tcpserver.log.Info";
const std::string filename2 = "log/tcpserver.log.Warning";
const std::string filename3 = "log/tcpserver.log.Error";
const std::string filename4 = "log/tcpserver.log.Fatal";
const std::string filename5 = "log/tcpserver.log.Unknown";
enum
{
Debug = 0,//调试信息
Info,//正常信息
Warning,//告警,不影响运行
Error,//一般错误
Fatal,//严重错误
Unknown
};
static std::string toLevelString(int level, std::string& filename)
{
switch(level)
{
case Debug:
filename = filename0;
return "Debug";
case Info:
filename = filename1;
return "Info";
case Warning:
filename = filename2;
return "Warning";
case Error:
filename = filename3;
return "Error";
case Fatal:
filename = filename4;
return "Fatal";
default:
filename = filename5;
return "Unknown";
}
}
static std::string getTime()
{
time_t curr = time(nullptr);//拿到当前时间
struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量
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);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900
return buffer;
}
//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()以%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{
//写入到两个缓冲区中
char logLeft[1024];//用来显示日志等级,时间,pid
std::string filename;
std::string level_string = toLevelString(level, filename);
std::string curr_time = getTime();
snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
char logRight[1024];//用来显示消息体
va_list p;
va_start(p, format);
//直接用这个接口来对format进行操作,提取信息
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);
//打印
printf("%s%s\n", logLeft, logRight);
//format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数
//保存到文件中
FILE* fp = fopen(filename.c_str(), "a");
if(fp == nullptr) return ;
fprintf(fp, "%s%s\n", logLeft, logRight);
fflush(fp);
fclose(fp);
//va_list p;//char*
//下面是三个宏函数
//int a = va_arg(p, int);//根据类型提取参数
//va_start(p, format);//让p指向可变参数部分的起始地址
//va_end(p);//把p置为空, p = NULL
}
2、实现等待多个fd
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"
class SelectServer
{
public:
SelectServer(uint16_t port): port_(port)
{}
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
void Start()
{
listensock_.Accept();
}
~SelectServer()
{}
private:
uint16_t port_;
Sock listensock_;
};
最一开始,服务器只有一个socket,就是上面这些代码创建出来的。如果没创建,服务器根本就没有socket,那么这时候Accept也没有检测到链接,只能阻塞着。所以不能直接Accept,要先有链接。在网络中,新链接当作读事件来就绪。我们先添加套接字到select中再处理。
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include <cstring>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"
class SelectServer
{
public:
SelectServer(uint16_t port): port_(port)
{}
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
void Start()
{
fd_set rfds;
FD_ZERO(&rfds); // 先清空,即使是新的rfds
FD_SET(listensock_.Fd(), &rfds);
while (true)
{
struct timeval timeout = {2, 0};//timeout是输出型参数,每一次都需要重新设置,不然后续都会变成0而直接打印第一条语句
int n = select(listensock_.Fd() + 1, &rfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
break;
case -1:
logMessage(Warning, "%d: %s", errno, strerror(errno));
break;
default:
logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
break;
}
}
}
~SelectServer()
{
listensock_.Close();
}
private:
uint16_t port_;
Sock listensock_;
};
事件现在已经可以检测到是否就绪了。
void HandlerEvent(fd_set& rfds)
{
if(FD_ISSET(listensock_.Fd(), &rfds))
{
std::cout << "有一个新连接到了" << std::endl;
//进行Accept
}
}
default:
logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
HandlerEvent(rfds);//rfds已经设置好了
break;
然后完善处理
void HandlerEvent(fd_set& rfds)
{
if(FD_ISSET(listensock_.Fd(), &rfds))
{
std::cout << "有一个新连接到了" << std::endl;
//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);//获取连接
if(sock < 0) return ;
//到这里已得到了新连接对应的fd,但不能直接读取数据
//将sock添加到select的rfds中,让select来管理
//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
//select对fd要有持续监听能力,不能有一个就绪其它全部重置
}
}
获取连接代表新增了一个fd,可以用套接字来进行IO服务,但还不知道数据是否就绪,只知道有连接。客户端发送请求,和我们的服务端建立连接后,客户端可以不发数据,这时调用read接口就会阻塞在这里。
获得的sock不能直接添加到rfds,以后越来越多的sock都要处理,时间耗费长,且有个前提,应当处理用户关心的fd。无脑设置进去,当我们关心的fd就绪后,其它都会清0,只有那个fd的位置会是1,所以也无用,所以select对fd要有持续监听能力,不能有一个就绪其它全部清零。
这里的思路其实也不难,要持续监听,我们就维护一个数组保护所有获得连接的sock就行。
const static int gport = 8888;
typedef int type_t;
class SelectServer
{
static const int N = (sizeof(fd_set) * 8);
public:
SelectServer(uint16_t port = gport) : port_(port)
{
}
//...
private:
uint16_t port_;
Sock listensock_;
type_t fdarray_[N];
初始化
static const int defaultfd = -1;//在之前的Sock.hpp中就已经有了这个全局变量,所以SelectServer.hpp中可以不设置这个,不过Sock.hpp中不要加static,这样两个文件都可以用
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
for(int i = 0; i < N; i++) fdarray_[i] = defaultfd;
}
Start
void Start()
{
fdarray_[0] = listensock_.Fd();
while (true)
{
struct timeval timeout = {2, 0};
//rfds是输入输出参数,所以rfds每次都要重置
//服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd
//所以rfds相关操作要在循环里做
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fdarray_[0];//用max_fd来表示fd中的最大值,看select接口那里的说明
for(int i = 0; i < N; i++)
{
if(fdarray_[i] == defaultfd) continue;
//合法fd
FD_SET(fdarray_[i], &rfds);
if(maxfd < fdarray_[i]) maxfd = fdarray_[i];
}
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
break;
case -1:
logMessage(Warning, "%d: %s", errno, strerror(errno));
break;
default:
logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
HandlerEvent(rfds);//rfds已经设置好了
break;
}
}
}
接下来就把sock放到fdarray_中。
void HandlerEvent(fd_set& rfds)
{
if(FD_ISSET(listensock_.Fd(), &rfds))
{
std::cout << "有一个新连接到了" << std::endl;
//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);//获取连接
if(sock < 0) return ;
//到这里已得到了新连接对应的fd,但不能直接读取数据
//将sock添加到select的rfds中,让select来管理
//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
//select对fd要有持续监听能力,不能有一个就绪其它全部重置
//让select来管理,把sock添加到fdarray_[]中
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;
}
//...
default:
logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
HandlerEvent(rfds);//rfds已经设置好了
DebugPrint();
break;
}
}
}
void DebugPrint()
{
std::cout << "fdarray[]: ";
for(int i = 0; i < N; ++i)
{
if(fdarray_[i] == defaultfd) continue;
std::cout << fdarray_[i] << " ";
}
std::cout << "\n";
}
以上的代码就已经能够做到等到多个fd并管理好它们了。
3、辨别连接和简单处理读事件
能够处理很多个连接,也都能把它们一次次的设置进集合里,但我们需要有条件地接收,要有目的地去接收。
先改一下形式
void Accepter()
{
std::cout << "有一个新连接到了" << std::endl;
//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);//获取连接
if(sock < 0) return ;
//到这里已得到了新连接对应的fd,但不能直接读取数据
//将sock添加到select的rfds中,让select来管理
//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
//select对fd要有持续监听能力,不能有一个就绪其它全部重置
// 让select来管理,把sock添加到fdarray_[]中
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)
{
if (FD_ISSET(listensock_.Fd(), &rfds))
{
Accepter();
}
}
HandlerEvent除了放入连接,还得处理一下连接。
void Recver(int index)
{
int fd = fdarray_[index];
// 用户不关心的fd就绪了
char buffer[1024];
// 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的
// 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s - 1] = 0;
std::cout << "client# " << buffer << std::endl;
// 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满
std::string echo = buffer;
echo += " [select server echo]";
send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式
}
else // 读完或者出错
{
if (s == 0)
logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);
else
logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);
// 清空当前这个连接,关闭fd
close(fdarray_[index]);
fdarray_[index] = defaultfd;
}
}
void HandlerEvent(fd_set &rfds)
{
//这个函数并不知道rfds的哪些fd就绪了,所以得循环检测
for(int i = 0; i < N; i++)
{
if(fdarray_[i] == defaultfd) continue;
//就绪的fd属于用户关心的,且它存在与rfds集合中
if ((fdarray_[i] == listensock_.Fd()) && FD_ISSET(listensock_.Fd(), &rfds))
{
Accepter();
}
else if((fdarray_[i] != listensock_.Fd()) &&FD_ISSET(fdarray_[i], &rfds))
{
Recver(i);
}
}
}
4、简单处理写、读事件
为了方便,先建立一个结构体
#define READ_EVENT (0X1)
#define WRITE_EVENT (0X1<<1)
#define EXCEPT_EVENT (0X1<<2)
typedef struct FdEvent
{
int fd;
uint8_t event;
std::string clientip;
uint16_t clientport;
}type_t;
static const int defaultevent = 0;
这样fdarray_这个数组就变成结构体类型的数组了。代码很多地方也要改一下,比如原本fdarray_[i]改成fdarray_[i].fd。
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
for (int i = 0; i < N; i++)//初始化一下
{
fdarray_[i].fd = defaultfd;
fdarray_[i].event = defaultevent;
fdarray_[i].clientport = 0;
}
}
void Start()
{
fdarray_[0].fd = listensock_.Fd();
fdarray_[0].event = READ_EVENT;
while (true)
{
struct timeval timeout = {2, 0};
// rfds是输入输出参数,所以rfds每次都要重置
// 服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd
// 所以rfds相关操作要在循环里做
fd_set rfds;
fd_set wfds;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
int maxfd = fdarray_[0].fd; // 用max_fd来表示fd中的最大值,看select接口那里的说明
for (int i = 0; i < N; i++)
{
if (fdarray_[i].fd == defaultfd)
continue;
// 合法fd
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, &timeout);
switch (n)
{
case 0:
logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
break;
case -1:
logMessage(Warning, "%d: %s", errno, strerror(errno));
break;
default:
logMessage(Debug, "有一个就绪事件发生了"); // 这时候就说明至少有一个fd就绪了
HandlerEvent(rfds, wfds);
DebugPrint();
break;
}
}
}
void DebugPrint()
{
std::cout << "fdarray[]: ";
for (int i = 0; i < N; ++i)
{
if (fdarray_[i].fd == defaultfd)
continue;
std::cout << fdarray_[i].fd << " ";
}
std::cout << "\n";
}
上面加入写事件,虽然我们还是关心读事件,但是写事件也要管理。以及处理用的函数部分
void Accepter()
{
std::cout << "有一个新连接到了" << std::endl;
//进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);//获取连接
if(sock < 0) return ;
//到这里已得到了新连接对应的fd,但不能直接读取数据
//将sock添加到select的rfds中,让select来管理
//打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
//select对fd要有持续监听能力,不能有一个就绪其它全部重置
// 让select来管理,把sock添加到fdarray_[]中
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 | WRITE_EVENT);//读写都关心,如果只关心一个,那就写一个
fdarray_[pos].event = READ_EVENT;
fdarray_[pos].clientip = clientip;
fdarray_[pos].clientport = clientport ;
}
}
void Recver(int index)
{
int fd = fdarray_[index].fd;
// 用户不关心的fd就绪了
char buffer[1024];
// 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的
// 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s - 1] = 0;
std::cout << fdarray_[index].clientip << ":" << fdarray_[index].clientport << "# " <<buffer << std::endl;
// 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满
std::string echo = buffer;
echo += " [select server echo]";
send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式
}
else // 读完或者出错
{
if (s == 0)
logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);
else
logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);
// 清空当前这个连接,关闭fd
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)
{
//这个函数并不知道rfds的哪些fd就绪了,所以得循环检测
for (int i = 0; i < N; i++)
{
if (fdarray_[i].fd == defaultfd) continue;
// 关心读事件且读事件已经就绪
if ((fdarray_[i].event & READ_EVENT) && FD_ISSET(fdarray_[i].fd, &rfds))
{
if (fdarray_[i].fd == listensock_.Fd())
{
Accepter();
}
else if (fdarray_[i].fd != listensock_.Fd())
{
Recver(i);
}
else {}
}
// 关心写事件且读事件已经就绪
else if ((fdarray_[i].event & WRITE_EVENT) && FD_ISSET(fdarray_[i].fd, &wfds))
{}
else {}
}
}
4、特点
可监控,能关心的fd个数有上限;将fd加入select监控集的同时,还需要使用一个数组结构array保存放到select监控集中的fd,用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断,以及select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。fd_set的大小可以调整,不过涉及到内核。
select也有缺点。每次调用select,都需要手动设置fd集合来告知系统用户关心哪些fd,不方便,且要把fd集合从用户态拷贝到内核态,每次都这样,开销不算小。以及在代码中可以发现,操作系统底层不知道fd对应的文件里是否有数据,它就得遍历用户关心的全部fd,而用户层也要遍历,系统在遍历时,如果没有fd就绪且是非阻塞,就直接返回了,如果有timeout,那就过了时间还没有就绪再返回,如果是阻塞的,那么没有就绪的就挂起全部进程,直到有就绪的,就再遍历一次找到这个就绪的,除此之外,我们的代码中也遍历了好多次。select只是相对于之前的IO方式高效,但也并不是好方案。
select能等待的fd数量太少。存储文件描述符的一般是数组,也就是数组下标表示fd,数组有上限,如果做成服务器,这个数组会调最大,所以有上限是必然的。一个进程能等的fd数量有限,但select能等的更少,所以select等的少是select设计时的限制,和进程无关。select上限就不高。
下一篇写poll型服务器,会在select代码的基础上进行更改。
本篇gitee
结束。