IO多路复用之select的简单介绍与使用

前言

        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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值