[network programming] IO multiplexing select

executive summary

         In this experiment, we try to use the IO multiplex select interface.

         stay   [network programming] TCP socket single machine communication experiment   A demo of socket communication between a client thread and a server thread is implemented in. For the server thread, it not only listens and accepts connection requests, but also receives, sends and processes the data interacting with the client. A server thread can only handle one client at a time. When processing the data of one client, it cannot accept the connection request of another client. For multi client scenarios, such a server has little practical value.

          An idea to solve the problem is: one thread / process listens to connection requests, and multiple other threads / processes connect to each client respectively. In this way, the server can process data interaction with multiple clients at the same time and accept connection requests from new clients in time:

         However, if there are many connection requests, the server cannot afford the resource consumption caused by one client and one processing thread, and thread switching also requires cost. In fact, most of the time, there is no data interaction between client and server. In view of this, IO multiplexing (select, poll, epoll) can be considered: IO multiplexing can realize one thread to monitor multiple file handles; Once a file handle is ready, it can notify the application to read and write accordingly; When no file handle is ready, the application will be blocked and the cpu will be surrendered. Multiplexing refers to multiple network connections, and multiplexing refers to multiplexing the same thread. In essence, IO multiplexing centralizes the work of accept, recv, send blocking and waiting for event readiness to the kernel for management.

Main process structure

         stay   [network programming] TCP socket single machine communication experiment   On the basis of, the number of client threads is increased to 3, and a single server thread is divided into a listening thread and a working thread. Listen to the port in the listening thread, establish a connection with the client, and pass the connection handle to the working thread; In the working thread, use select to monitor whether all connected handles are readable. If readable, send and receive data.

experimental result

select interface

Function prototype

int select (int maxfdp, 
            fd_set *readset, 
            fd_set *writeset, 
            fd_set *exceptest, 
            struct timeval *timeout);

parameter   maxfdp

         First parameter maxfdp   Is the maximum value of all file descriptors to be monitored plus 1. Note: it is not the number of file descriptors to be monitored plus 1. File descriptors are essentially integers. File descriptors with values greater than maxfdp will not be monitored by select.

If 3 + 1 is passed to maxfdp (the number of file descriptors to be monitored plus 1):

select does not monitor the connection handles of three clients:

The data sent by the three clients are accumulated in the receiving window of the server and will not be read by the application layer of the server:

The parameter points to fd_ Pointer to set

         The second, third and fourth parameters all point to fd_ Pointer to set. Second parameter   readset corresponds to the descriptor set of readable events; Third parameter   writeset corresponds to the descriptor set of writable events; Fourth parameter   exceptest is the descriptor set corresponding to the exception event.

typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;

         fd_set is essentially an array of unsigned long shapes. The size of the array__ FDSET_ Dragons is a macro definition. The value is 1024/(8 * sizeof(unsigned long)). The actual space occupied by the array is   1024 / 8 = 128 Byte = 1024 bit.

         fd_set represents the set of file descriptors in the form of bitmap: a bit corresponds to a file descriptor, and 0 or 1 of the bit of the corresponding position indicates whether the file descriptor is monitored or whether the file descriptor is ready.

         Because of this array size and representation, select can only monitor file descriptors of 0 ~ 1023 at most, a total of 1024 - the data structure FD for storing file descriptors_ set   This limits the maximum number of file descriptors that a select can listen to to to 1024.

         select provides some   macro   It can be used to operate the file descriptor set, that is, to change the 0 and 1 values of the corresponding bits:

FD_ZERO(fd_set* fds);

Clear the file descriptor set fds, that is, set all bits to 0

FD_SET(int fd, fd_set* fds);

Add the file descriptor fd to the set fds, that is, the corresponding bit position is 1

FD_ISSET(int fd, fd_set* fds); 

Judge whether the file descriptor fd is in the set fds, that is, judge whether the corresponding bit is 1

FD_CLR(int fd, fd_set* fds);

File descriptor   fd   From collection   fds   Delete, that is, set the corresponding bit position to 0

         In terms of usage mode, call FD first_ Zero will fd_set cleared, and then called FD_. Set adds the file descriptor to be monitored to fd_set, and then call the function select to test FD_ For all file descriptors in set, the macro FD is used after the select function returns_ Isset checks whether the bit corresponding to a file descriptor is still 1. If it is 1, it indicates that an event is ready, and if it is 0, it is vice versa.

Parameter timeout

         Last parameter   Timeout is the timeout time, which indicates how long to wait before giving up the wait. Passing NULL means waiting for an infinite time. It continues to block until an event is ready.

         timeout points to   The timeval structure has two members: seconds and microseconds:

struct timeval
{
    __time_t tv_sec;        /* Seconds. */
    __suseconds_t tv_usec;  /* Microseconds. */
};

         When using timeval, just assign values to the members directly, eg

struct timeval timeout = {0};
timeout.tv_sec = 1;
timeout.tv_usec = 0;

Return value

         If select fails, return - 1; If timeout, return 0; If there are ready descriptors, the number of ready descriptors is returned.

select disadvantages

select has several major disadvantages, which are similar to the file descriptor set fd_set about:

  1. Every time you call select, you need to set fd_set is copied from the user state to the kernel state. When the select detects a ready event or a timeout return, it will send FD again_ Set is copied back from the kernel state to the user state, which is expensive;
  2. select monitors file descriptors by polling. It performs linear scanning on the file descriptor set over and over again to check whether there are events ready, which is inefficient;
  3. select can only monitor 1024 file descriptors at most, and the number is limited;

Complete code implementation

Header file

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/syscall.h>

Macro definition

#define LOCAL_IP_ADDR       "127.0.0.1"
#define SERVER_LISTEN_PORT  5197
#define MAX_LISTEN_EVENTS   16
#define NET_MSG_BUF_LEN     128
#define CLINET_SEND_MSG     "Hello Server~"
#define SERVER_SEND_MSG     "Hello Client~"

Function to get thread ID

pid_t gettid(void) {
    // The header file sys/syscall.h needs to be imported
    return syscall(SYS_gettid);
}

Client thread entry function

void* client(void* param) {
    int iRes = 0, iConnFd = 0, iNetMsgLen = 0;
    pthread_t thdId = gettid();
    char szNetMsg[NET_MSG_BUF_LEN] = {0};
    struct sockaddr_in stServAddr;


    iConnFd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == iConnFd) {
        printf("Client[%u] failed to create socket, err[%s]\n", 
               thdId, strerror(errno));
        return NULL;
    }


    // Fill in the target address structure and specify the protocol family, target port and target host IP address
    stServAddr.sin_family = AF_INET;
    stServAddr.sin_port = htons(SERVER_LISTEN_PORT);
    stServAddr.sin_addr.s_addr = inet_addr(LOCAL_IP_ADDR);
    // 1. Pass the socket handle, 2. Pass the pointer of the target address structure to be connected, and 3. Pass the size of the address structure
    while (1) {
        iRes = connect(iConnFd, (struct sockaddr *)&stServAddr, 
                       sizeof(stServAddr));
        if (0 != iRes) {
            printf("Client[%u] failed to connect to[%s:%u], err[%s]\n",
                   thdId, LOCAL_IP_ADDR, SERVER_LISTEN_PORT, 
                   strerror(errno));
            sleep(2);
            continue;
        } else {
            printf("Client[%u] succeeded to connect to[%s:%u]\n", 
                   thdId, LOCAL_IP_ADDR, SERVER_LISTEN_PORT);
            break;
        }
    }


    iNetMsgLen = send(iConnFd, CLINET_SEND_MSG, 
                      strlen(CLINET_SEND_MSG), 0);
    if (iNetMsgLen < 0) {
        printf("Client[%u] failed to send msg to server, err[%s]\n", 
               thdId, strerror(errno));
        close(iConnFd);
        return NULL;
    }


    iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
    if (iNetMsgLen < 0) {
        printf("Client[%u] failed to read from network, err[%s]\n", 
               thdId, strerror(errno));
    } else {
        printf("Client[%u] recv reply[%s]\n", thdId, szNetMsg);
    }
    
    close(iConnFd);
    return NULL;
}

Function functions handle readable events

int eventProc(int *piConnFdSet, fd_set *pfsReadSet) {
    int iIndex = 0, iConnFd = 0, iNetMsgLen = 0;
    char szNetMsg[NET_MSG_BUF_LEN] = {0};


    for (iIndex = 0; iIndex < MAX_LISTEN_EVENTS; iIndex++) {
        if (!FD_ISSET(piConnFdSet[iIndex], pfsReadSet)) {
            continue;
        }


        // Simplify code with temporary variables
        iConnFd = piConnFdSet[iIndex];
        
        // Receive client message
        iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
        if (iNetMsgLen < 0) {
            printf("Server failed to recv from network, err[%s]\n", 
                   strerror(errno));
            close(iConnFd);
            return -1;
        }
        
        printf("Server recv msg[%s]\n", szNetMsg);


        // Reply to client
        iNetMsgLen = send(iConnFd, SERVER_SEND_MSG, 
                          strlen(SERVER_SEND_MSG), 0);
        if (iNetMsgLen < 0) {
            printf("Server failed to reply client, err[%s]\n", 
                   strerror(errno));
            close(iConnFd);
            return -1;
        }


        piConnFdSet[iIndex] = 0;
        close(iConnFd);
    }
    
    return 0;
}

Server worker thread entry function

void* serverWork(void* param) {
    int iRes = 0, iIndex = 0, iMaxFd = 0, iEventNum = 0;
    int *piConnFdSet = (int *)param;
    fd_set fsReadSet;
    struct timeval timeout = {0};
    
    // monitor
    while (1) {
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;
		
        FD_ZERO(&fsReadSet);
        #if 0
        iMaxFd = 0;
        for (iIndex = 0; iIndex < MAX_LISTEN_EVENTS; iIndex++) {
            FD_SET(piConnFdSet[iIndex], &fsReadSet);
            iMaxFd = piConnFdSet[iIndex] > iMaxFd ? 
                     piConnFdSet[iIndex] : iMaxFd;
        }
        #else
		iMaxFd = 3;
        #endif


        iEventNum = select(iMaxFd + 1, &fsReadSet, NULL, NULL, &timeout);
        if (-1 == iEventNum) {
            printf("Server failed to select event.\n");
            break;
        }
		
        printf("Server select get [%u] event\n", iEventNum);
        if (0 == iEventNum) {
            continue;
        }


        iRes = eventProc(piConnFdSet, &fsReadSet);
        if (0 != iRes) {
            printf("Server failed to proc event.\n");
            break;
        }        
    }


    return NULL;
}

Server listening thread entry function

void* serverLsn(void* param) {
    int iRes = 0, iIndex = 0;
    int iLsnFd = 0, iConnFd = 0, iReusePort = 0, iSockAddrLen = 0;
    int *paiConnFdSet = (int *)param;
    struct sockaddr_in stLsnAddr;
    struct sockaddr_in stCliAddr;


    // Create socket
    iLsnFd = socket(AF_INET, SOCK_STREAM, 0);                       
    if (-1 == iLsnFd) {
        printf("Server failed to create socket, err[%s]\n", 
               strerror(errno));
        return NULL;
    }


    // Set port multiplexing
    iReusePort = 1;
    iRes = setsockopt(iLsnFd, SOL_SOCKET, SO_REUSEPORT, &iReusePort, 
                      sizeof (iReusePort));
    if (-1 == iRes) {
        printf("Server failed set reuse attr, err[%s]\n", 
               strerror(errno));
        close(iLsnFd);
        return NULL;
    }


    stLsnAddr.sin_family = AF_INET;
    stLsnAddr.sin_port = htons(SERVER_LISTEN_PORT);
    stLsnAddr.sin_addr.s_addr = INADDR_ANY;    
    // Binding port
    iRes = bind(iLsnFd, (struct sockaddr*)&stLsnAddr, 
                sizeof(stLsnAddr));   
    if (-1 == iRes) {
        printf("Server failed to bind port[%u], err[%s]\n", 
               SERVER_LISTEN_PORT, strerror(errno));
        close(iLsnFd);
        return NULL;
    } else {
        printf("Server succeeded to bind port[%u], start listen.\n", 
               SERVER_LISTEN_PORT);
    }


    // monitor
    iRes = listen(iLsnFd, MAX_LISTEN_EVENTS);
    if (-1 == iRes) {
        printf("Server failed to listen port[%u], err[%s]\n", 
               SERVER_LISTEN_PORT, strerror(errno));
        close(iLsnFd);
        return NULL;
    }
    
    while (iIndex < MAX_LISTEN_EVENTS) {
        iSockAddrLen = sizeof(stCliAddr);
        // 1. Pass in the listening handle, 2. Pass in the address structure pointer to receive the client's address
        // 3 parameter incoming address structure size
        iConnFd = accept(iLsnFd, (struct sockaddr*)&stCliAddr, 
                         &iSockAddrLen);
        if (-1 == iConnFd) {
            printf("Server failed to accept connect request, err[%s]\n",
                   strerror(errno));
            break;
        } else {
            printf("Server accept connect request from[%s:%u]\n", 
                   inet_ntoa(stCliAddr.sin_addr), 
                   ntohs(stCliAddr.sin_port));
            paiConnFdSet[iIndex] = iConnFd;
            iIndex++;
        }
    }
    
    close(iLsnFd);
    return NULL;
}

Main function

int main() {
    // Thread ID, which is essentially an unsigned long integer
    pthread_t thdServerWork = 101;
    pthread_t thdServerLsn = 102;
    pthread_t thdClient1 = 1;
    pthread_t thdClient2 = 2;
    pthread_t thdClient3 = 3;
    
    // Set of socket file descriptors to monitor for readability 
    int aiConnFdSet[MAX_LISTEN_EVENTS] = {0};
    
    // 1 reference thread ID, 2 reference thread attribute,
    // Parameter 3 specifies the thread entry function, and parameter 4 specifies the parameters passed to the entry function
    pthread_create(&thdServerWork, NULL, serverWork, &aiConnFdSet[0]);
    pthread_create(&thdServerLsn, NULL, serverLsn, &aiConnFdSet[0]);
    pthread_create(&thdClient1, NULL, client, NULL);
    pthread_create(&thdClient2, NULL, client, NULL);
    pthread_create(&thdClient3, NULL, client, NULL);


    // The 1 Parameter passes in the thread ID, and the 2 parameter is used to receive the return value of the thread entry function. If the return value is not required, set it to NULL
    pthread_join(thdServerWork, NULL);
    pthread_join(thdServerLsn, NULL);
    pthread_join(thdClient1, NULL);
    pthread_join(thdClient2, NULL);
    pthread_join(thdClient3, NULL);
    
    return 0;
}

Tags: network socket

Posted on Mon, 11 Oct 2021 13:40:58 -0400 by TheMightySpud