前言
IO多路复用是一种通过单一的线程或进程同时监视多个输入/输出流的机制。它使得程序能够有效地处理多个客户端连接或多个文件描述符,而无需为每个连接创建一个新的线程或进程。这种技术通常用于网络编程和文件系统操作,以提高系统的性能和资源利用率。
Linux系统提供给用户的接口有三个,select、poll、epoll。
select只负责系统IO的等待工作,不拷贝数据,拷贝需要调用read、write接口来完成。
select其实对于IO多路复用而言并不是一个非常重要的接口,epoll功能更好,使用更方便,因此这里只对select进行简单介绍。
接口介绍
select早期需要包含三个头文件,现在只需要包含<sys/select.h>就好了。
参数介绍
第一个int类型的参数nfds,代表select要监视的多个fd中的最大的那个fd+1。除了第一个参数其他参数均为输入输出型参数。
第五个struct timeval结构体指针类型的参数timeout,先看一下结构体timeval的定义。
两个长整形参数,一个代表秒,一个代表毫秒。当传入的参数为空指针null时,说明select为阻塞式调用。当传入的参数为{0 ,0}时,说明select为非阻塞式调用。当传入的参数不为为{0 ,0},比如{3,0}时,说明select在3秒内为阻塞式调用,超过3秒非阻塞返回一次。
剩下的三个参数代表select未来关心的三个事件readfds(读)、writefds(写)、exceptfds(异常)。首先,关于fd_set,fd_set本质上是一个位图结构,代表文件描述符的集合。
以读事件为例,当有位图被输入到readfds参数时,表示用户需要系统关心哪些fd上的读事件,bit位的位置代表fd的值,bit位的值代表是否关心。当select返回时,会修改被传入readfds的位图,bit位的位置代表fd的值,bit位的值代表哪些fd上的读事件准备就绪。
写事件、异常事件同理。
ps:虽然fd_set是一个位图结构,但在实际使用中不推荐对其直接修改,最好使用系统提供的位图操作函数。
返回值
select返回一个int类型的整数,下面以ret代表返回值。
当ret大于0时,ret代表有几个fd准备就绪。当ret等于0时,说明select超时返回了。当ret小于0时,说明select调用失败,错误码被设置。
相关函数
FD_CLR,将一个文件描述符从位图中清除。
FD_ISSRT,判断一个文件描述符在位图中是否被设置。
FD_SET,设置一个文件描述符。
FD_ZERO,将位图全部置零。
使用注意
由于select需要监听多个fd,因此select需要我们在服务器内部维护一个数组,保存所有合法fd,每关心一个事件就需要维护一个数组。
fd_set的大小是固定的,sizeof(fd_set)*8的值代表着它最多能监听的fd个数。
select缺陷
1. select能监视的fd有上限,且除非修改内核不然无法解决。
2. 需要使用第三方数组维护合法fd,增加代码难度。
3. select大量参数为输入输出型参数,每次调用select前需要重新设置fd数组,调用完成后需要检查fd数组,增加代码难度,带来大量遍历成本。
4. 位图从用户拷贝到内核从内核拷贝到用户,带来拷贝成本。
使用范例
以下是一个简单的tcp服务器,对客户端发来的消息原样返回,作为select的一个使用参考。
考虑到代码复杂性以及select的重要程度,以下代码实际使用会有bug,但可以进行测试。
没有写客户端,可以用telnet命令进行测试。
环境:centos7,g++7.3.1
err.hpp
#pragma once
#include<iostream>
enum
{
USAGE_ERR = 1,
SOCKRT_ERR,
BIND_ERR,
LISTEN_ERR
};
log.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<string>
#include<ctime>
#include<cstdarg>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char* be_level_str(int level)
{
switch (level)
{
case DEBUG : return "DEBUG";
case NORMAL : return "NORMAL";
case WARNING : return "WARNING";
case ERROR : return "ERROR";
case FATAL : return "FATAL";
}
}
void log_msg(int level, const char* format, ...)
{
#define NUM 1024
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]", be_level_str(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
}
sock.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<string>
#include"log.hpp"
#include"err.hpp"
class Sock
{
const static int backlog = 32;
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
log_msg(FATAL, "create socket failed");
exit(SOCKRT_ERR);
}
log_msg(NORMAL, "create socket success: %d", sock);
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return sock;
}
static void Bind(int fd, int 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(fd, (struct sockaddr*)&local, sizeof(local)) < 0){
log_msg(FATAL, "bind socket failed");
exit(BIND_ERR);
}
log_msg(NORMAL, "bind socket success");
}
static void Listen(int fd)
{
if(listen(fd, backlog) < 0){
log_msg(FATAL, "listen socket failed");
exit(LISTEN_ERR);
}
log_msg(NORMAL, "listen socket success");
}
static int Accept(int fd, std::string& client_ip, uint16_t& client_port)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(fd, (struct sockaddr*)&peer, &len);
if(sock < 0){
log_msg(ERROR, "accept failed");
}
else{
log_msg(NORMAL, "accept a new link success, get new sock: %d", sock);
client_port = ntohs(peer.sin_port);
client_ip = inet_ntoa(peer.sin_addr);
}
return sock;
}
};
selectServer.hpp
#pragma once
#include<iostream>
#include<functional>
#include"sock.hpp"
namespace Des1re
{
static const int default_port = 8080;
static const int max_fd_num = 1024;
static const int default_fd_num = -1;
using func_t = std::function<std::string(const char*)>;
class selectServer
{
public:
selectServer( func_t f, int port = default_port): _port(port), _listen_sock(-1), _func(f)
{}
~selectServer()
{
if(_listen_sock < 0){
close(_listen_sock);
}
if(_fd_arr != nullptr){
delete[] _fd_arr;
}
}
void init()
{
_listen_sock = Sock::Socket();
Sock::Bind(_listen_sock, _port);
Sock::Listen(_listen_sock);
_fd_arr = new int[max_fd_num];
for(int i = 0; i < max_fd_num; i++){
_fd_arr[i] = default_fd_num;
}
_fd_arr[0] = _listen_sock;
}
void start()
{
while(true){
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = _fd_arr[0];
for(int i = 0; i < max_fd_num; i++){
if(_fd_arr[i] == default_fd_num) continue;
FD_SET(_fd_arr[i], &rfds);
if(_fd_arr[i] > maxfd){
maxfd = _fd_arr[i];
}
}
struct timeval timeout = {1, 0};
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch(n){
case 0 : log_msg(NORMAL, "timeout..."); break;
case -1 : log_msg(WARNING, "select error, code: %d, errmsg: %s", errno, strerror(errno)); break;
default :
log_msg(NORMAL, "have event ready");
handle_event(rfds);
break;
}
// std::string client_ip;
// uint16_t client_port = 0;
// int sock = Sock::Accept(_listen_sock, client_ip, client_port);
// if(sock < 0){
// continue;
// }
}
}
private:
void handle_event(fd_set& rfds)
{
for(int i = 0; i < max_fd_num; i++){
if(_fd_arr[i] == default_fd_num) continue;
if(FD_ISSET(_fd_arr[0], &rfds) && i == 0){
my_accept(_fd_arr[0]);
}
else if(FD_ISSET(_fd_arr[i], &rfds)){
my_recv(_fd_arr[i], i);
}
}
}
void print()
{
for(int i = 0; i < max_fd_num; i++){
if(_fd_arr[i] != default_fd_num){
std::cout << _fd_arr[i] << " ";
}
}
std::cout << std::endl;
}
void my_accept(int listen_sock)
{
std::string client__ip;
uint16_t client_port;
int sock = Sock::Accept(listen_sock, client__ip, client_port);
if(sock < 0) return;
log_msg(NORMAL, "accept success[%s: %d]", client__ip.c_str(), client_port);
int i = 0;
for(;i < max_fd_num; i++){
if(_fd_arr[i] != default_fd_num) continue;
else break;
}
if(i == max_fd_num){
log_msg(WARNING, "server is full");
close(sock);
}
else{
_fd_arr[i] = sock;
}
print();
}
void my_recv(int fd, int pos)
{
//这样读在实际情况下有问题,需要定协议,以下仅供参考,不影响测试
char buffer[1024];
ssize_t s = recv(fd, buffer, sizeof(buffer), 0);
if(s > 0){
buffer[s] = 0;
log_msg(NORMAL, "client>> %s", buffer);
}
else if(s == 0){
close(fd);
_fd_arr[pos] = default_fd_num;
log_msg(NORMAL, "client quit");
}
else{
close(fd);
_fd_arr[pos] = default_fd_num;
log_msg(ERROR, "recv failed: %s", strerror(errno));
}
//写也不能这么写,需要select额外关注写事件
std::string response = _func(buffer);
write(fd, response.c_str(), response.size());
}
private:
int _port;
int _listen_sock;
int* _fd_arr;
func_t _func;
};
}
main.cc
#include"selectServer.hpp"
#include<memory>
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << "\n\n" << std::endl;
}
std::string echo(const char* request)
{
return request;
}
int main(int argc, char* argv[]){
if(argc != 2){
Usage(argv[0]);
exit(USAGE_ERR);
}
std::unique_ptr<Des1re::selectServer> server(new Des1re::selectServer(echo, atoi(argv[1])));
server->init();
server->start();
return 0;
}
makefile
select:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f select