1、select介绍
select
模型是一种用于处理 I/O 多路复用的编程模型,广泛应用于网络编程和系统编程中。它允许一个进程监视多个文件描述符,以便在其中一个或多个文件描述符变为可读、可写或发生异常时进行响应。
2、select接口
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
: 监视的文件描述符的最大值加一。readfds
: 监视可读事件的文件描述符集合。writefds
: 监视可写事件的文件描述符集合。exceptfds
: 监视异常事件的文件描述符集合。timeout
: 指定等待的时间,如果为 NULL,则无限期等待。
相关函数接口:
FD_ZERO(fd_set* set); //清空文件描述符集合
FD_SET(int fd, fd_set *set) //将文件描述符 fd 添加到集合 set 中
FD_CLR(int fd, fd_set *set) //从集合 set 中移除文件描述符 fd
FD_ISSET(int fd, fd_set *set) //检查文件描述符 fd 是否在集合 set 中
3、selectServer
简单的select服务器搭建:
select.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/select.h>
#include <string>
#include "sock.hpp"
#include "err.hpp"
#include "log.hpp"
#define READ_EVENT (0x1)
#define WRITE_EVENT (0x1<<1)
#define EXCEPT_EVENT (0x1<<2)
const static uint16_t gport = 8888;
typedef struct Fd
{
int fd;
uint8_t event;
std::string clientIp;
uint16_t clientPort;
} type_t;
//static const defaultfd = -1;
class selectServer
{
static const int N = (sizeof(fd_set)*8);
static const uint8_t defaultevent = 0;
public:
selectServer(uint16_t port = gport) : _port(port)
{
}
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
for(int i = 0; i < N; i++)
{
fdarry_[i].fd = defaultfd;
fdarry_[i].event = defaultevent;
fdarry_[i].clientPort = 0;
}
}
void Recver(int index)
{
// ServiceIO
// BUG
char buffer[1024];
ssize_t s = recv(fdarry_[index].fd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s - 1] = 0;
std::cout << "client# " << buffer << std::endl;
// 发送消息也要被select管理的,TODO
std::string echo = buffer;
echo += "[select server echo]";
send(fdarry_[index].fd, echo.c_str(), echo.size(), 0);
}
else
{
if (s == 0)
{
logMessage(Info, "client quit ... ,fdarry_[i] -> defaultfd: %d->%d", fdarry_[index].fd, defaultfd);
}
else
{
logMessage(Warning, "recv error ... , fdarry_[i] -> defaultfd: %d->%d", fdarry_[index].fd, defaultfd);
}
close(fdarry_[index].fd);
fdarry_[index].fd = defaultfd;
fdarry_[index].event = 0;
fdarry_[index].clientPort = 0;
fdarry_[index].clientIp.resize(0);
}
}
void accepter()
{
std::cout << "有一个新连接来了!\n";
// Accept,这里进行Accept会不会阻塞呢?不会!
std::string clientIp;
uint16_t clientPort;
int sock = _listensock.Accept(&clientIp, &clientPort);
if (sock < 0)
return;
// 得到对应的sock我们能不能直接进行recv/send? 不能
// 你怎么直到sock上有数据就绪了呢?不知道,我们需要把sock交给select,让select进行管理
logMessage(Debug, "[%s : %d], sock : %d", clientIp.c_str(), clientPort, sock);
// 要让select帮我们管理,只要把sock添加到fdarry_[]里面即可
int pos = 1;
for (; pos < N; pos++)
{
if (fdarry_[pos].fd == defaultfd)
break;
}
if (pos >= N)
{
// 说明数组满了
close(sock);
logMessage(Warning, "fdarry_ is full!");
}
else
{
fdarry_[pos].fd = sock;
//fdarry_[pos].event = READ_EVENT | WRITE_EVENT;
fdarry_[pos].event = READ_EVENT;
fdarry_[pos].clientPort = clientPort;
fdarry_[pos].clientIp = clientIp;
}
}
void HandlerEvent(fd_set& rfds, fd_set& wfds)
{
for (int i = 0; i < N; i++)
{
if(fdarry_[i].fd == defaultfd) continue;
if ((fdarry_[i].event & READ_EVENT) && FD_ISSET(_listensock.Fd(), &rfds))
{
if (fdarry_[i].fd == _listensock.Fd())
{
accepter();
}
else if (fdarry_[i].fd != _listensock.Fd())
{
Recver(i);
}
}
else if((fdarry_[i].event & WRITE_EVENT) && FD_ISSET(_listensock.Fd(), &wfds))
{
//TODO
}
else
{
}
}
}
void start()
{
fdarry_[0].fd = _listensock.Fd();
while (true)
{
//struct timeval timeout = {2, 0};
//因为rfds是输入输出型参数,注定了每次都要对rfds重置,必定要知道我历史上有哪些fd,fdarry[N]
//因为select服务器在运行中,sockfd是一直变化的,所以maxfd也一定一直变化,maxfd也要一直更新,fdarry[]
fd_set rfds;
fd_set wfds;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
int maxfd = fdarry_[0].fd;
for(int i = 0; i < N; i++)
{
if(fdarry_[i].fd == defaultfd) continue;
//合法fd
if(fdarry_[i].event & READ_EVENT)
FD_SET(fdarry_[i].fd, &rfds);
if(fdarry_[i].event & WRITE_EVENT)
FD_SET(fdarry_[i].fd, &wfds);
if(maxfd < fdarry_[i].fd) maxfd = fdarry_[i].fd;
}
int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr);
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, "有一个就绪事件发生了");
HandlerEvent(rfds, wfds);
DebugPrint();
break;
}
sleep(1);
}
}
void DebugPrint()
{
std::cout << "fdarry_[] : ";
for(int i = 0; i < N; i++)
{
if(fdarry_[i].fd == defaultfd) continue;
std::cout << fdarry_[i].fd << " ";
}
std::cout << std::endl;
}
~selectServer()
{
_listensock.Close();
}
private:
uint16_t _port;
sock _listensock;
type_t fdarry_[N];
};
sock.hpp : 简单的socket封装
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "log.hpp"
#include "err.hpp"
static const int gbacklog = 32;
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);
}
}
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)
{
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);
*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()
{
close(_sock);
}
~sock()
{
if(_sock != defaultfd) close(_sock);
}
private:
int _sock;
};
4、总结
优点
- 高效: 能够同时处理多个 I/O 操作,避免了阻塞和频繁的上下文切换。
- 简单: API 相对简单,易于使用。
缺点
- 文件描述符限制:
select
对文件描述符数量有限制(通常为 1024),在高并发场景下可能不够用。 - 性能下降: 当监视的文件描述符数量很大时,性能可能会下降,因为每次调用
select
都需要遍历整个文件描述符集合。