Multiplexing (Part 1) – select

Table of Contents

1. Select interface

1. Understand the select system call

2. Understanding of each parameter

2. Write select server

1. Two tool categories

2. Network socket encapsulation

3. Server class writing

4. Source file writing

5. Run


1. select interface

1. Understand the select system call

int select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval* timeout);

Header files: sys/time.h, sys/types.h, unistd.h

Function: Select the part responsible for multiple descriptors in IO. This function will remind the process to process when the descriptor’s read and write exception events are ready.

Parameters: discussed later.

Return value: A return value greater than 0 indicates that the corresponding file descriptor is ready. A return value equal to 0 indicates that no file descriptor is ready. It returns after a timeout. A return value less than 0 indicates that the select call failed.

2. Understanding of each parameter

(1)struct timeval* timeout

It is a class pointer of type struct timeval, defined as follows:

struct timeval
{
    time_t tv_sec; /* Seconds. */
    suseconds_t tv_usec; /* Microseconds. */
};

There are two members internally, the first is seconds and the second is microseconds.

If nullptr is passed as the parameter for this timeout, the default selection is blocking and waiting. Only when one or more file descriptors are ready will the upper layer process be notified to read data.

If the parameter is passed in struct timeval timeout = {0, 0}, the seconds and microseconds are set to 0. At this time, select uses non-blocking waiting, and the programmer needs to write polling detection code.

If the parameter is set to a specific value, such as struct timeval timeout = {5, 0}, and the time is set to 5 seconds, select will block and wait within 5 seconds. If a file descriptor is ready within 5 seconds, the upper layer will be notified; if If not, it will time out and return.

(2)int nfds

Indicates the maximum value of all file descriptors to wait for plus one.

Assuming that you want to wait for 3 file descriptors, namely 3, 4 and 7, you need to pass 7 + 1 = 8 to nfds when passing parameters.

(3)fd_set

fd_set is a bitmap waiting for read-ready file descriptors.

Each bit of it represents a file descriptor, and the status of the bits 0 and 1 indicates whether the bit is monitored by select.

The following is a schematic diagram of the fd_set bitmap, indicating that the three file descriptors with offsets 1, 3, and 5 need to be monitored by select.

The size of the fd_set type is 128 bytes, and each byte has 8 bits, so the fd_set type can contain 1024 bits, which also indicates that select can monitor up to 1024 file descriptors.

Although we know that fd_set is a bitmap structure, we do not know its internal implementation.

Therefore, when adding, deleting, checking and modifying bitmap data, you must use the adding, deleting, checking and modifying interface provided by the system.

  • FD_ZERO(fd_set *fdset);–Clear fd_set. The set does not contain any file descriptors and can be used for initialization.
  • FD_SET(int fd, fd_set *fdset);–Add fd to the fd_set collection
  • FD_CLR(int fd, fd_set *fdset);–Remove fd from the fd_set collection
  • FD_ISSET(int fd, fd_set *fdset);–Detect whether fd is in the fd_set set, if not, return 0

(4)fd_set* reads

The three parameters among fd_set* reads, fd_set* writefds, and fd_set* exceptfds are output parameters.

Here I will use fd_set* reads as an example to explain.

fd_set* reads means reading a bitmap, and the parameters passed represent the file descriptors that need to be monitored, and select only cares about whether there is data in these file descriptors that needs to be read.

Suppose we define a fd_set variable, use FD_SET to fill in file descriptors 1, 3, and 5 into the variable, and finally pass the pointer of the variable into the function.

It will change this variable when select returns normally or times out.

For example, after the select call is completed, the bitmap is changed to the following style, indicating that file descriptors 1 and 3 are ready and can be read by the system call. Since both file descriptors are ready, the return value is 2.

The next time we make a select call, we can modify the bitmap again to increase or decrease the file descriptors that need to be monitored.

When select returns again, the bitmap will still be modified to indicate which file descriptors are ready after this call.

That is to say, when passing parameters, this bitmap represents the descriptor that needs to be monitored, and when the call returns, this bitmap represents the ready file descriptor.

fd_set* reads are the same as fd_set* writefds and fd_set* exceptfds in use, except that fd_set* writefds only cares about the process of writing data to the file descriptor, while fd_set* exceptfds only cares about whether there is an error in the file descriptor. .

They will also modify their corresponding fd_set variables in the same way to notify the process.

2. Write select server

1. Two tool classes

The code requires the use of two tool classes. err.hpp stores all error codes, and log.hpp, which originally printed logs, continues to be used.

err.hpp

#pragma once

#include<iostream>

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

log.hpp

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>

//One file is used to save normal operation logs, and one file is used to save error logs.
#define LOG_FILE "./log.txt"
#define ERROR_FILE "./error.txt"

//Define five macros according to the current running status of the program
//NORMAL means normal, WARNING means there is a problem but the program can still run, ERROR means a common error, FATAL means a serious error
#define DEBUG 0
#define NORMAL 1
#defineWARNING 2
#defineERROR 3
#define FATAL 4

//Convert run level to string
const char* to_string(int level)
{
    switch(level)
    {
        case(DEBUG):
            return "DEBUG";
        case(NORMAL):
            return "NORMAL";
        case(WARNING):
            return "WARNING";
        case(ERROR):
            return "ERROR";
        case(FATAL):
            return "FATAL";
        default:
            return nullptr;
    }
}

//Output fixed format logs to the screen and files
//The first parameter is the level, the second parameter is the string that needs to be output
void logmessage(int level, const char* format, ...)
{
    //output to screen
    char logprefix[1024];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//Put errors in a certain format string
    
    char logcontent[1024];
    va_list arg; //Variable parameter list
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout << logprefix << logcontent << std::endl;
    //Output to file
    //Open two files
    FILE* log = fopen(LOG_FILE, "a");
    FILE* err = fopen(ERROR_FILE, "a");
    if(log != nullptr & amp; & amp; err != nullptr)
    {
        FILE* cur = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING)
            cur = log;
        if(level == ERROR || level == FATAL)
            cur = err;
        if(cur)
            fprintf(cur, "%s%s\
", logprefix, logcontent);
        fclose(log);
        fclose(err);
    }
}

2.Network Socket Encapsulation

Encapsulate the socket, bind, accept and other functions written previously into a Sock class.

#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"log.hpp"
#include"err.hpp"

classSock
{
private:
    static const int backlog = 32; //The queue length is 32
public:
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);//Create socket
        if(listensock < 0)//Failed to create socket and print error reason
        {
            logmessage(FATAL, "create socket error");//Socket failure is the most serious error
            exit(SOCKET_ERROR);//Exit
        }
        logmessage(NORMAL, "create socket success:%d", listensock);//The socket is created successfully and printed for the user to observe

        //Open port reuse to ensure that the program can start normally immediately after exiting
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, & amp;opt, sizeof(opt));

        return listensock;
    }

    static void Bind(int listensock, int port)
    {
        struct sockaddr_in local;//Storage local network information
        local.sin_family = AF_INET;//The communication method is network communication
        local.sin_port = htons(port);//Fill in the port number in network byte order
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY is the macro of ip address 0.0.0.0
        
        if(bind(listensock, (struct sockaddr*) & amp;local, sizeof(local)) < 0)//Bind IP, print information if unsuccessful
        {
            logmessage(FATAL, "bind socket error");//bind failure is also the most serious error
            exit(BIND_ERROR);//Exit
        }
        logmessage(NORMAL, "bind socket success");//Bind IP successfully, print for users to observe
    }

    static void Listen(int listensock)
    {
        //listen sets the socket to listening mode
        if(listen(listensock, backlog) < 0) // The second parameter backlog is used to fill in this hole.
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(NORMAL, "listen socket success");
    }

    static int Accept(int listensock, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;//Storage local network information
        socklen_t len = sizeof(peer);
        int sock = accept(listensock, (struct sockaddr*) & amp;peer, & amp;len);
        
        if(sock < 0)
        {
            logmessage(ERROR, "accept fail");//Failed to receive new file descriptor
        }
        else
        {
            logmessage(NORMAL, "accept a new link");//Receive new file descriptor successfully
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }
};

3. Server class writing

The related functions of the server class are all defined in selectserver.hpp, and the select server we implement only cares about read events.

Let me briefly talk about the running process: after constructing the object, initserver creates the socket and initializes the member variables. The start function calls the select function in a loop. Then we filter out the valid read event-ready descriptors and put them into the _fdarray array, and then use The handler_read function handles events.

Finally, determine whether the descriptor is a normal descriptor or a listening descriptor in the handler_read function.

The normal descriptor read event ready indicates that data needs to be read, and we implement a Receiver for processing; the listening descriptor read event ready indicates that there is a link that needs to be received, and we implement an Accepter function for processing.

#pragma once
#include<iostream>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<functional>
#include"sock.hpp"

namespace select_func
{
    static const int default_port = 8080;//The default port number is 8080
    static const int fdnum = sizeof(fd_set) * 8;//The maximum port number is 1024
    static const int default_fd = -1;//Put all file descriptors that need to be managed into an array, -1 is an invalid element in the array

    using func_t = std::function<std::string (const std::string & amp;)>;

    class SelectServer
    {
    public:
        SelectServer(func_t func, int port = default_port)
            :_listensock(-1)
            ,_port(default_port)
            ,_fdarray(nullptr)
            ,_func(func)
        {}

        ~SelectServer()
        {
            if(_listensock > 0)
                close(_listensock);//Close the listening file descriptor
            if(_fdarray)
                delete []_fdarray;//Release the array storing file descriptors
        }

        void initserver()
        {
            //Create a listen socket, bind the port number, and set it to listening state
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            //Construct an array to store all file descriptors that need to be managed, and set all elements of the array to -1
            _fdarray = new int[fdnum];
            for(int i = 0; i<fdnum; + + i)
            {
                _fdarray[i] = default_fd;
            }
            //Put the listen socket first and it will not be modified during the entire process of program running.
            _fdarray[0] = _listensock;
        }

        void start()
        {
            while(1)
            {
                //Fill in the bitmap
                fd_set fds;
                FD_ZERO( & amp;fds);
                int maxfd = _fdarray[0];//Initially, the three descriptors 012 are opened by default, and 3 is the listening descriptor, so the maximum value of the descriptor is 3
                for(int i = 0; i<fdnum; + + i)
                {
                    if(_fdarray[i] != default_fd)//Filter out valid file descriptors
                    {
                        FD_SET(_fdarray[i], & amp;fds);//Add the file descriptor to the bitmap
                        if(_fdarray[i] > maxfd)//fdarray stores newly added valid file descriptors
                            maxfd = _fdarray[i];//maxfd needs to increase according to the element
                    }
                }
                //logmessage(NORMAL, "maxfd:%d", maxfd);

                //Call select
                //struct timeval timeout = {1, 0};
                int n = select(maxfd + 1, & amp;fds, nullptr, nullptr, nullptr);//Non-blocking call
                switch(n)
                {
                    case 0://no descriptor ready
                        logmessage(NORMAL, "time out.");
                        break;
                    case -1://select went wrong
                        logmessage(ERROR, "select error, error code:%d %s", errno, strerror(errno));
                        break;
                    default:// has the descriptor ready (getting the link is read ready)
                        //logmessage(NORMAL, "server get new tasks.");
                        handler_read(fds);//process data
                        break;
                }
            }
        }

        void Accepter()
        {
            //Go here to explain that the waiting process select has been completed.
            std::string clientip;
            uint16_t clientport = 0;
            //select is only responsible for waiting. Accept is still required to receive the link, but this call will not block.
            int sock = Sock::Accept(_listensock, & amp;clientip, & amp;clientport);
            if (sock < 0)//Do not execute if receiving error
                return;
            logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);

            //The link has been established and new descriptor communication is generated
            //We also need to insert this descriptor into the array again
            int i = 0;
            for(i = 0; i<fdnum; + + i)
            {
                if(_fdarray[i] == default_fd)
                    break;
            }
            if(i == fdnum)//The array is full
            {
                logmessage(WARNING, "server if full, please wait");
                close(sock);//Close the link
            }
            else
                _fdarray[i] = sock;//Insert data into the array

            print_list();//Print the contents of the array
        }

        void Receiver(int sock, int pos)
        {
            //Receive data from the client
            char buffer[1024];
            ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
            if (n > 0)
            {
                buffer[n] = 0;//Add /0 at the end
                logmessage(NORMAL, "client# %s", buffer);
            }
            else if (n == 0)
            {
                close(sock);
                _fdarray[pos] = default_fd;
                logmessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(sock);
                _fdarray[pos] = default_fd;
                logmessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            //Use callback function to process data
            std::string response = _func(buffer);

            //Send back response
            write(sock, response.c_str(), response.size());
        }

        void handler_read(fd_set & amp; fds)
        {
            //We divide the processing of reading data into two types:
            //The first is to obtain a new link
            //The second is that there is data that needs to be read.
            for(int i = 0; i<fdnum; + + i)
            {
                //Filter out valid file descriptors
                if(_fdarray[i] != default_fd)
                {
                    //listensock ready indicates that the process has obtained a new link
                    if(FD_ISSET(_fdarray[i], & amp;fds) & amp; & amp; _fdarray[i] == _listensock)
                        Accepter();//Establish a link
                    //Other ordinary file descriptors are ready
                    else if(FD_ISSET(_fdarray[i], & amp;fds))
                        Receiver(_fdarray[i], i);//Receive data
                }
            }
        }

        void print_list()
        {
            std::cout << "fd list:" << std::endl;
            for(int i = 0; i<fdnum; + + i)
            {
                if(_fdarray[i] != default_fd)
                    std::cout << _fdarray[i] << " ";
            }
            std::cout << std::endl;
        }

    private:
        int _listensock;
        int _port;
        int* _fdarray;
        func_t _func;
    };
}

4. Source file writing

Still using the old method, initserver is initialized, start starts running, and unique_ptr manages the object.

#include"selectserver.hpp"
#include"err.hpp"
#include<memory>

using namespace std;
using namespace select_func;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\
\t" << proc << "port" << "\
\
";
}

string transaction(const string & str)
{
    return str;
}

int main(int argc, char *argv[])
{
    unique_ptr<SelectServer> p(new SelectServer(transaction));
    p->initserver();
    p->start();
    return 0;
}

5.Run

To save trouble, we can directly use telnet as the client.

Next we connect to the server, send data, and check its operation.

Note that to send data via telnet, you need to press Ctrl + ], and then after telnet> appears, press the Enter key before you can enter the data and press Enter to send. Finally, if you want to exit telnet, press Ctrl + ], and then enter q or quit to exit.