TCP/IP Network Programming Chapter 12: I/O Multiplexing

Server-side based on I/O multiplexing

Disadvantages and solutions of multi-process server side

To build a concurrent server, new processes are created whenever there is a client connection request. This is indeed the solution used in practice, but it is not perfect, because there is a huge price to pay when creating a process. This requires a lot of computing and memory space. Since each process has an independent memory space, the data exchange between each other also requires a relatively complex method (IPC is a relatively complex communication method). You should also feel that when IPC is needed, the programming difficulty will increase. "What's the solution? Is it possible to serve multiple clients at the same time without creating a process?"


Of course! The I/O multiplexing explained in this section is such a technique. Are you all excited to hear that there is such an approach? But please don't rely too much on this model! This solution is not applicable to all situations, and different implementation methods should be adopted according to the characteristics of the target server. Let's first understand the meaning of "Multiplexing".

Understand reuse

In network programming, multiplexing refers to the simultaneous transmission of multiple independent data streams on a physical communication link (such as a network transmission medium). It improves the efficient use of network resources by merging multiple data streams into one stream and decomposing them at the receiving end .

Multiplexing techniques can be implemented in several ways:

  1. Time Division Multiplexing (TDM): Time is divided into several intervals, and each interval is assigned to a different data stream for transmission. The sending end sends data in each time interval according to certain rules, and the receiving end extracts and restores data according to the interval.

  2. Frequency Division Multiplexing (FDM): The frequency range is divided into multiple narrowband channels, each dedicated to the transmission of a data stream. After the data stream is modulated, it is transmitted on different frequencies, and the receiving end demodulates the signal to obtain the original data.

  3. Code Division Multiplexing (CDM): Different code sequences are used to distinguish each data stream. The sending end uses a specific code sequence to spread the data, and the receiving end uses the same code sequence for despreading, thereby separating the data streams.

These multiplexing technologies above all aim to realize the transmission of multiple data streams on the same physical communication link, so as to improve the bandwidth utilization and transmission efficiency of the network. In network programming, we can use different multiplexing techniques to handle multiple client requests at the same time or transmit multiple data streams on a single connection.

Application of multiplexing technology on server side

For IO multiplexing (IO multiplexing) in network programming, it is a mechanism to efficiently handle multiple IO events. In the traditional IO model, each IO operation will block the thread, causing the program to be unable to process other IO events at the same time when processing one IO, resulting in waste of resources.

IO multiplexing uses specific system call functions (such as select, poll, epoll, etc.) to monitor multiple IO events, and manages and processes multiple IO operations in one thread, thereby realizing the ability to process multiple IO events at the same time. Its basic principle is to add the IO events that need to be monitored into an event collection, and then block and wait for any one of the events to be ready through the system call. Once there is a ready event, the program can perform the corresponding operation.

The main benefits of IO multiplexing include:

  1. High resource utilization: Using IO multiplexing can prevent each IO operation from blocking threads, thereby reducing the number of threads and improving resource utilization efficiency.

  2. Fast response: IO multiplexing can monitor multiple IO events at the same time. Once an event is ready, it will be processed immediately, which greatly reduces the delay of event response.

  3. Simple programming: Compared with the multi-thread or multi-process model, the use of IO multiplexing can simplify the code and reduce the difficulty of development and maintenance.

All in all, IO multiplexing is a mechanism for efficiently handling multiple IO events, which can reduce the number of threads, improve resource utilization and response speed, and is one of the commonly used technologies in network programming.

Let me give another example to understand the IO multiplexing server side. There are 10 students and 1 teacher in a classroom. These children are not idle people, and they keep asking questions during class. The school has no choice but to assign one teacher to each student, that is to say, there are currently 10 teachers in the classroom. After that, as long as there are new transfer students, 1 teacher will be added, because transfer students also like to ask questions. In this story, if the students are regarded as the client and the teacher is regarded as the server-side process for data exchange with the client, then the operation mode of the classroom is a multi-process server-side method.


One day, a teacher with superpowers came to the school. This teacher is available to all student questions and answers quickly without keeping students waiting. Therefore, in order to improve the efficiency of teachers, the school transferred other teachers to other classes. Now, students must raise their hands before asking questions, and the teacher will answer the questions after confirming the questions raised by the students. In other words, the current classroom operates in IO multiplexing mode.
Although the example is a bit strange, you can understand the IO multiplexing method through it: the teacher must confirm whether there is a student who raises his hand. Similarly, the IO multiplexing server-side process needs to confirm the socket of the raised hand (received data), and receive the data through the socket of the raised hand.

Understand the select function and implement the server side

Using the select function is the most representative way to achieve multiplexing server-side. There is also a function with the same name under the Windows platform to provide the same function
, so it has good portability.

The function and calling sequence of the select function

When using the sclect function, multiple file descriptors can be gathered together for unified monitoring. The items are as follows.
□Is there a socket to receive data?
□What are the sockets that do not need to block data transmission?
□Which sockets are abnormal?

The use of the select function is quite different from general functions, and more precisely, it is difficult to use. But in order to achieve IO multiplexing server side, we should master the select function and apply it to socket programming. It is not an exaggeration to think that "select function is the whole content of IO multiplexing". Next, introduce the calling method and order of the select function.

Step 1:
Set the file descriptor
, specify the monitoring range
, set the timeout
Step 2:
call the select function
Step 3:
view the call result

It can be seen that some preparatory work is required before calling the select function, and the result needs to be checked after the call. Next, explain one by one in the above order.

set file descriptor

Use the select function to monitor multiple file descriptors at the same time. Of course, monitoring file descriptors can be regarded as monitoring sockets. At this point, the file descriptors to be monitored first need to be gathered together. When concentrating, it is also necessary to distinguish according to the monitoring items (reception, transmission, and abnormality), that is, they are divided into 3 categories according to the above 3 monitoring items.


Do this using the fd_set array variable as shown. The array is an array of bits containing 0s and 1s.

 The leftmost bit in the figure represents file descriptor 0 (where it is located). If this bit is set to 1, it indicates that the file descriptor
is the object of monitoring. So which file descriptors in the figure are the monitoring objects? Obviously, file descriptors 1 and 3.
"Should the value be registered directly to the fd_set variable by the number of the file descriptor?"
Of course not! The operation on the fd_set variable is carried out in units of bits, which also means that it will be
cumbersome to directly operate the variable. Are you required to do it yourself? In fact, the operation of registering or changing the value in the fd_set variable is completed by the following macro

□FD_ZERO(fd_set *fdset): Initialize all bits of the fd_set variable to 0.
□ FD_SET(int fd, fd_set *fdset): Register the file descriptor information in the variable pointed to by the parameter fdset.

□ FD_CLR(int fd, fd_set *fdset): Clear the file descriptor information from the variable pointed to by the parameter fdset.

□ FD_ISSET(int fd, fd_set *fdset): If the variable pointed to by the parameter fdset contains the information of the file descriptor, then return "true".
In the above function, FD_ISSET is used to verify the call result of the select function.

Set the inspection (monitoring) range and timeout

First briefly introduce the select function.

#include<sys/select.h>
#include<sys/time.h>

int select(int maxfd,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval *timeout);//成功时返回大于0的值,失败时返回-1
      maxfd     //监视对象文件描述符数量。
      readset   //将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值。 
      writeset  //将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值。  
      exceptset //将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值。
      timeout   //调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
      返回值    //发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数

As mentioned above, the selcct function is used to verify the changes of the three monitoring items. Declare 3 fd_set variables according to the monitoring item, register the file descriptor information with them respectively, and pass the address value of the variable to the second to fourth parameters of the above function. But before that (before calling the select function) you need to decide the following 2 things.
"What is the monitoring (checking) scope of the file descriptor?"
"How to set the timeout period of the select function?"
First, the monitoring scope of the file descriptor is related to the first parameter of the select function. In fact, the select function requires passing the number of monitor object file descriptors through the first parameter. Therefore, it is necessary to obtain the number of file descriptors registered in the fd_set variable. But every time a new file descriptor is created, its value will increase by 1, so you only need to add 1 to the largest file descriptor value and then pass it to the select function. Adding 1 is because the value of the file descriptor starts from 0.
Second, the timeout period of the select function is related to the last parameter of the select function, where the timeval structure is defined as follows.

struct timeval{
     long tv_sec;   //seconds
     long tv_usec;  //microseconds
}

Originally, the select function only returns when the monitored file descriptor changes. If there is no change, it enters the blocking state. The timeout is specified to prevent this from happening. By declaring the above structure variable, fill the seconds into the tv_sec member, fill the microseconds into the tv_usec member, and then pass the address value of the structure to the last parameter of the select function. At this time, even if there is no change in the file descriptor, you can return from the function as long as the specified time has elapsed. But in this case, the select function returns 0. Therefore, you can understand the reason for the return through the return value. Pass NULL parameter if you don't want to set timeout.

Check the result after calling the select function

It is equally important to see the result after the function call. We have discussed the return value of the select function. If an integer greater than 0 is returned, it means that the corresponding number of file descriptors has changed. So how does this change change? After the select function call is completed, the fd_set variable passed to it will change. All bits that were 1 become 0, except for the bit corresponding to the file descriptor that changed. Therefore, it can be considered that the file descriptor at the position where the value is still 1 has changed.

Select function call example

#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUF_SIZE 30

int main(int argc,char *argv){
    fd_set reads,temps;
    int result,str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;

    FD_ZERO(&reads);
    FD_SET(0,&reads);

    while(1){
       temps=reads;
       timeout.tv_sec=5;
       timeout.tv_usec=0;
       result=select(1,&temp,0,0,&timeout);
       if(result==-1){
            puts("select() error!");
            break;
       }
       else if(result==0){
            puts("Time-out!");
       }
       else{
            if(FD_ISSET(0,&temps)){
               str_len=read(0,buf,BUF_SIZE);
               buf[str_len]=0;
               printf("message from console: %s",buf);
            }
       }
    }
return 0;
}

There are two points to note about the above:

1. Since the content in the monitoring fd_set will be modified after the select function is called, we need to save a copy, and the above code is saved in temps.

2. Since the time in the timeout will be replaced with the remaining time before the timeout when the select function is called, the initial amount of the timeout also needs to be initialized each time.

Realize IO multiplexing server side

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>

#define BUF_SIZE 100
void error_handling(char *buf);

int main(int argc,char *argv[]){
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    struct timeval timeout;
    fd_set reads,cpy_reads;

    socklen_t addr_sz;
    int fd_max,str_len,fd_num,i;
    char buf[BUF_SIZE];
    if(argc!=2){
         printf("Usage: %s <port>\n",argv[0]);
         exit(1);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi([argv[1]]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
         error_handling("bind() error");
    if(listen(serv_sock,5)==-1)
         error_handling("listen() error");

    FD_ZERO(&reads);
    FD_SET(serv_sock,&reads);
    fd_max=serv_sock;

    while(1){
         cpy_reads=reads;
         timeout.tv_sec=5;
         timeout.tv_usec=5000;

         if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
              break;
         if(fd_num==0)
              continue;

         for(i=0;i<fd_max+1;++i){
              if(FD_ISSET(i,&cpy_reads)){     
                     if(i==serv_sock){     //连接请求到来
                         addr_sz=sizeof(clnt_addr);
                         clnt_sock=accept(serv_sock,(structsockaddr*)&clnt_addr,addr_sz);
                         FD_SET(clnt_sock,&reads);
                         if(fd_max<clnt_sock)fd=clnt_sock;
                         printf("connected client: %d \n",clnt_sock);
                     }
                     else{
                         str_len=rad(i,buf,BUF_SIZE);
                         if(str_len==0){
                               FD_CLR(i,&reads);
                               close(i);
                               printf("closed client: %d \n",i);
                         }
                         else{
                             write(i,buf,str_len);//回声
                         }
                     }
              }
         }
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *buf){
    fputs(buf,stderr);
    fputc('\n',stderr);
    exit(1);
}

Windows-based implementation

Call select function on Windows platform

Windows also provides the select function, and all parameters are exactly the same as the Linux select function. It's just that the first parameter of the select function on the Windows platform is added to maintain compatibility with the UNIX series operating systems (including Linux), and has no special meaning.

#include <winsock2.h>
int select(int nfds, fd_set *treadfds, fd_set *writefds, fd_set *excepfds, const struct
timeval * timeout);//成功时返回0,失败时返回-1。

The order and meaning of the return value and parameters are the same as those of the select function in Linux before, so they are omitted.
The definition of the timeval structure is given below .

typedef struct timeval{
      long tv_sec;
      long tv_usec;
} TIMEVAL;

As you can see, the basic structure is the same as the previous definition in Linux, but the typedef declaration is used in Windows. Next observe the fd_set structure. This is what needs to be paid attention to when implementing in Windows. It can be seen that Windows' fd_set does not use bit arrays like Linux.

typedef struct fd_set{
    u_int fd_count;
    SOCKET fd_array[FD_SETSIZE];
} fd_set;

The fd_set of Windows is composed of members fd_count and fd_array, fd_count is used for the number of socket handles, and fd_array is used to save socket handles, as long as you think about it, you can understand the reason for this statement. Linux's file descriptors start incrementing from 0, so you can find out the relationship between the current number of file descriptors and the last generated file descriptor. However, the socket handle of Windows does not start from 0, and there is no rule to follow between the integer values ​​of the handle, so it is necessary to directly save the array of handles and the variable for recording the number of handles. Fortunately, the names, functions, and usage methods of the four FDXXX macros that deal with the fd_set structure are exactly the same as those in Linux (so omitted), which may be Microsoft's consideration to ensure compatibility.

Guess you like

Origin blog.csdn.net/Reol99999/article/details/131748815