一、I/O多路转接之select
1.1select介绍
(1)系统提供select函数来实现多路复用输入/输出模型
(2)select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
(3)程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
1.2select函数原型及使用:

1.2.1参数解释:
(1)参数nfds是需要监视的最大的文件描述符值+1。
(2)rdset、wrset、exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合
及异常文件描述符的集合。
(3)参数timeout为结构timeval,用来设置selecto的等待时间。
1.2.2函数返回值:
(1)大于0,执行成功则返回文件描述词状态已改变的个数。
(2)等于0,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回,超时代表没有文
件描述符就绪,也没有错误。
(3)小于0,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
1.2.3timeval结构体:

1.2.3 fd_set结构

1.2.4 fd_set结构体位图的最大个数大小和错误码

1.3socket就绪条件
1.3.1读就绪
(1)sOcket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以
无阻塞的读该文件描述符,并且返回值大于0。
(2)socketTCP通信中,对端关闭连接,此时对该socket读,则返回0。
(3)监听的socket上有新的连接请求。
(4)socket上有未处理的错误。
1.3.2写就绪
(1)socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水
位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
(2)socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写作,
会触发SIGPIPE信号。
(3)socket使用非阻塞connect连接成功或失败之后。
(4)socket上有未读取的错误。
二、select的使用------使用select实现服务器:
2.1SelectServer.hpp文件:
cpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Sock.hpp"
static const uint16_t defaultport = 8080;
static const int fd_num_max = sizeof(fd_set) * 8; // 获取fd_set中最大表示的文件描述符个数
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport)
: _port(port)
{
// 初始化辅助数组
for (size_t i = 0; i < fd_num_max; i++)
{
_fd_array[i] = defaultfd;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void PrintFd()
{
std::cout << "online fd list:";
for (int i = 0; i < fd_num_max; i++)
{
if (_fd_array[i] == defaultfd)
{
continue;
}
else
{
std::cout << _fd_array[i] << "->";
}
}
std::cout << std::endl;
}
void Accepter()
{
// 连接事件就绪
uint16_t clientport;
std::string clientip;
// 在此处不会在阻塞了,因为selec已经告诉我事件已经就绪
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0)
{
return;
}
else
{
lg.logmessage(Info, "accept success! %s, %d", clientip.c_str(), clientport);
// 这里不能直接读,因为读写事件不一定就绪,所以要直接把这里获取的sock交给select
// 这里吧sock交给辅助数组就可以把sock交给select来监听了
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (_fd_array[pos] != defaultfd)
{
continue;
}
else
{
break;
}
}
// 这里跳出循环有两种情况,一是找到了没有设置的数组下标,二是数组满了
if (pos == fd_num_max)
{
// 整个数组已经满载了
lg.logmessage(Warning, "sorry, server is full, sockfd[%d] will close", sock);
close(sock);
return;
}
else
{
// 找到未被设置的下标了
_fd_array[pos] = sock;
PrintFd();
}
}
}
void Recvr(int fd, int pos)
{
// 其他的文件描述符就绪,进行读取操作
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a message:" << buffer << std::endl;
}
else if (n == 0)
{
lg.logmessage(Info, "client quit, close fd is:%d", fd);
_fd_array[pos] = defaultfd; // 从select中移除了文件描述符
close(fd);
}
else
{
lg.logmessage(Warning, "read error, fd is:%d", fd);
_fd_array[pos] = defaultfd; // 从select中移除了文件描述符
close(fd);
}
}
void Dispacter(fd_set &rfds)
{
// 便利所有的文件描述符数组,因为我不知道哪一个就绪了
for (int i = 0; i < fd_num_max; i++)
{
int fd = _fd_array[i];
if (fd == defaultfd)
{
// 不是合法的文件描述符
continue;
}
// 判断listensock是否在rfds中,判断连接是否就绪
if (FD_ISSET(fd, &rfds))
{
if (_listensock.Fd() == fd)
{
// 连接管理器
Accepter();
}
else
{
Recvr(fd, i);
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
_fd_array[0] = listensock;
for (;;)
{
// 这里不能直接accept,这里必须要检测并获取listensock上面的时间,新链接到来,等价于读事件的就绪
fd_set rfds;
// 创建的位图可能有乱码,所以进行清空处理
FD_ZERO(&rfds);
// 找最大的文件描述符
int maxfd = _fd_array[0];
// 便利辅助数组,将合法的文件描述符添加进rfds
for (int i = 0; i < fd_num_max; i++)
{
if (_fd_array[i] == defaultfd)
{
continue;
}
else
{
// 将合法文件描述符设置进位图
FD_SET(_fd_array[i], &rfds);
if (maxfd < _fd_array[i])
{
maxfd = _fd_array[i];
lg.logmessage(Info, "maxfd update, maxfd is %d", maxfd);
}
}
}
// 每次重新设置每个5秒返回一次(要针对周期的重复设置)
struct timeval timeout = {5, 0};
// 如果时间就绪了,上层不对数据进行处理,select就会一直处理
// select告诉就绪了,接下来的一次读取,读取fd的时候,不会发生阻塞
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
std::cout << "time out, timeout:" << timeout.tv_sec << "." << timeout.tv_usec << std::endl;
break;
case -1:
std::cout << "select fail" << std::endl;
default:
// 有事件就绪
std::cout << "get a link" << std::endl;
// 处理事件
Dispacter(rfds);
break;
}
}
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int _fd_array[fd_num_max]; // 使用辅助数组进行select和文件描述符之间的传递
};
2.2 Sock.hpp文件
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"
const int backlog = 10;
extern log lg;
enum
{
SocketError = 2,
BindError,
ListenError
};
class Sock
{
public:
Sock()
: _sockfd(-1)
{
}
~Sock()
{
if (_sockfd >= 0)
{
close(_sockfd);
}
}
// 创建套接字
void Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
lg.logmessage(Fatal, "socket fail, %s, %d", errno, strerror(errno));
exit(SocketError);
}
int opt = -1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &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(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg.logmessage(Fatal, "bind fail, %s, %d", errno, strerror(errno));
exit(BindError);
}
}
void Listen()
{
if (listen(_sockfd, backlog) < 0)
{
lg.logmessage(Fatal, "listen fail, %s, %d", errno, strerror(errno));
exit(ListenError);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (newfd < 0)
{
lg.logmessage(Warning, "accept fail, %s, %d", errno, strerror(errno));
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
// 获取远端主机的信息
*clientport = ntohs(peer.sin_port);
*clientip = ipstr;
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);
int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
{
std::cerr << "connect to" << ip << ":" << port << std::endl;
return false;
}
return true;
}
void Close()
{
close(_sockfd);
}
int Fd()
{
return _sockfd;
}
private:
int _sockfd;
};
2.3 main.cc文件
cpp
#include "SelectServer.hpp"
#include <memory>
int main()
{
//std::cout << "fd_set bit max:" << sizeof(fd_set)* 8 << std::endl;
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
2.4 Log.hpp文件
cpp
#pragma once
#include <iostream>
#include <stdarg.h>
#include <time.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>
#define SIZE 1024
// 设置日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define logFile "log.txt"
class log
{
public:
log()
{
PrintMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
PrintMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
default:
return "None";
}
}
void logmessage(int level, const char *format, ...) // 后面的省略号表示可变参数
{
char leftbuffer[SIZE];
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
snprintf(leftbuffer, sizeof(leftbuffer), "[%s],[%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
char rightbuffer[SIZE];
va_list s;
va_start(s, format);
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 3];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour,ctime->tm_min,ctime->tm_sec);
//printf("%s", logtxt);
PrintLog(level,logtxt);
// 格式:默认部分+自定义部分(可变参数部分)
}
void PrintLog(int level, const std::string& logtxt)
{
switch(PrintMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(logFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd < 0)
{
return ;
}
write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = logFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~log()
{
}
private:
int PrintMethod;
std::string path;
};
log lg;
三、Select的缺点
从上面我们可以看出select的缺点:
(1)每次调用select,都需要手动重新设置fd集合,从接口使用角度来说也非常不便。
(2)由于select的输入输出型参数比较多,每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
(3)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd也很大。
(4)select支持的文件描述符数量太小,也就是等待的fd有上限。