当前位置:网站首页>Common programming models for writing web service programs

Common programming models for writing web service programs

2020-12-07 17:44:32 osc_ wl6d9wri

Today, I'd like to summarize the common programming models used to write server-side programs . Reference resources UNP Chapter 30 of the third edition and Chen Shuo's muduo That book , It is strongly recommended to read carefully . Note that the following code is for an explicit framework or programming model , It's not a complete program , Writing late at night is capricious , Don't be surprised .

accept + read/write

This is not a concurrent server , It's an iterative server (iterative server). It serves one customer at a time . Not suitable for long connections , fit daytime such write-only Short connection service .

 void handle(client_socket, client_address)
{
while(true)
{
data = client_socket.recv(4096);
if(data)
client_socket.send(data);
else
do_error();
}
}
while(true){
connfd = server_socket.accept();
handle(client_socket, client_address);
}

accept + fork

This model is also called process-per-connection, Write a multi process concurrent server program , Call... For each customer fork Spawn a child process , Enables the server to serve multiple clients at the same time . It's traditional Unix Concurrent network programming solution , Suitable for small number of concurrent connections , fit “ The workload of calculating the response is much greater than fork() The cost of ” This situation , Every customer site fork A subprocess is more expensive CPU Time , The limit on the number of connections is how many threads can be opened .

while(true){
connfd = accept(listenfd, clientaddr, &clientlen);
if((childpid = fork()) == 0){ // child process
close(listenfd);
handle_client(connfd);
exit(0);
}
close(connfd);
}

accept + thread

This model is also called thread-per-connection, Is to change the above process into a thread , That is to create a thread for each customer request , It's a multithreaded model , In this way, the cost is much less than that of the process , In this way, it is much faster to create a thread for each customer site than to derive a process for each customer site , And it's usually faster than the pre birth process .

while(true)     //  The main thread loop is blocked in  accept call 
{
connfd = accept(listenfd, clientaddr, &clientlen);  //  When a connection is returned 
pthread_create(&tid, NULL, &doit, (void *)connfd);  // Just create a new thread , take connfd Pass to thread 
}

void* doit(void *arg)
{
……
handle_client((int)arg);
close((int)arg);
……
}

pre fork

This scheme does not derive a process for each client site as the traditional concurrent server does , Instead, a certain number of subprocesses are derived in advance during the startup phase , When multiple customer connections arrive , These subprocesses can serve them immediately . After creating the process in advance , After that, there are several different treatments , such as Each subprocess calls accept( No lock protection )、 Protect by file locking accept、 Protected by thread mutex lock accept、 Pass socket descriptor from parent to child .

The obvious advantage is that the scene is omitted fork The cost of the child process , The disadvantage is to guess how many child processes are derived in advance , This can be dealt with by means of , Continuous monitoring of the parent process is available / Number of idle child processes , Once the value drops to a certain threshold, additional child processes are derived ; Once another process is terminated, the same threshold is exceeded .

Look at the first one : Subprocess call accept, No lock protection

static pid_t   *pids;               //  Store each subprocess ID Array of 
pids = alloc_mem(childnumbers, sizeof(pid_t));      //  To apply for space 

for(i=0; i<childnumbers; i++)
pids[i] = child_make(i, listenfd, addrlen);     //   Create sub processes 

pid_t child_make(int i, int listenfd, int addrlen)
{
pid_t pid;
if( (pid=fork()) > 0)   //  The parent process 
return pid;     
child_main(i, listenfd, addrlen);   //  In this loop call  accept , Then process the customer request , Finally, close the connection 
}
void child_main(i, listenfd, addrlen)
{
while(1)
{
connfd = accept(listenfd, clientaddr, &clientlen);  //  Accept the connection 
handle_client(connfd);  //  Handle 
close(fd);  //  close 
}
}

In this way “ Jing group ” The phenomenon , How do you say ? The server process is spawned during program startup N Subprocess , They call accept And they are all put into sleep , When the first customer connection arrives , all N All child processes are awakened ( Because all the listening descriptors used by all subprocesses point to the same socket structure , They all sleep here socket Structure on the waiting queue ), Only the first running child gets that client connection , The rest continue to sleep . This can cause performance damage , How much measurement performance is affected is beyond the scope of this article ( I will not ……).

So what's the solution ? Let the application process call accept Put some form of lock before and after , In this way, only one child process is blocked at any time accept in call , Other subprocesses are blocking in an attempt to acquire for protection accept Lock it . That's what's mentioned above Protect by file locking accept、 Protected by thread mutex lock accept, Code will not be posted here .

There is also a modified version that allows only the parent process to call accept, Then put the accepted connected socket “ Pass on ” Give a process , This bypasses the... For all child processes accept Call to provide a possible need for lock protection . This code implementation is more complex , Be able to use interprocess communication , The parent process also keeps track of the free and idle state of the child process , To pass a new socket to the idle process .

pre threaded

Create a thread pool in advance , And let each thread call accept, Instead, let every thread be blocked in accept call , We use mutexes to ensure that only one thread is calling at any time accept.

thread_make();  //  First create a specified number of threads 

void* thread_func(void *arg)
{
while(1){
pthread_mutex_lock(&mutex);
connfd = accept(listenfd, clientaddr, &clientlen);
pthread_mutex_unlock(&mutex);
handle_client(connfd);
close(connfd);
}
}

There is also a modified version of the main thread unified accept, And pass each client connection to an available thread in the pool . The problem is how the main thread passes a connected socket to an available thread in the thread pool , There are many ways .

It is mentioned above that 9 Different server programming models , They're all blocking network programming , Program flow is usually blocked in read() On , Wait for the data to arrive . however TCP It's full duplex protocol , Support at the same time read() and write() operation , When a process / The thread is blocked in read() On , But the program wants to give this TCP What to do with connecting to send data ? such as echo client、proxy.

One way is to use two processes / Threads , One is responsible for reading , One is responsible for writing . Another way is to use IO multiplexing, That is to say select/poll/epoll/kqueue This series of multiplexing means , Let a program flow handle multiple connections .IO Reuse is not IO Connect , It's reuse IO Threads , At this point, we have to lead to Reactor The model , Its main meaning is to put the news (IO event ) Distributed to user provided processing functions , And keep the common code of network part unchanged , User independent business logic , If you haven't heard of it, please check by yourself .

poll(reactor)

It's single threaded reactor programme , When there is no incident , The thread is waiting in select/poll/epoll_wait And so on , When the event arrives read->decode->compute->encode->write. Only one thread event is processed sequentially , A thread can only do one thing at a time , If you want to perform a delay calculation ( hold compute The process is delayed 100ms), Then you should register a timeout callback , Cannot call sleep() Blocking calls like this , To avoid blocking the current IO Threads (Reactor The thread where the event loop is usually called IO Threads ).

while(true){
    n = epoll_wait(epfd, events, maxevents, -1);
    for(i=0; i<n; i++){
        if(events[i] == listenfd)
            //  Deal with connection 
        else if (events[i].events & EPOLLIN)
        {
            // read compute write
            //  Modify the identifier 
        }
        else
            //  Other treatments 
    }
}

reactor + thread-per-task

This is a Transitional Plan , After receiving a customer request , be not in IO Thread computing , Instead, create a new thread to calculate , To make the most of multicore . Note that it's for every request ( Not every connection ) Created a new thread , This overhead can be avoided by using thread pools , We'll talk about that later . There's another problem out-of-order, That is to create multiple threads at the same time to calculate multiple requests received on the same connection , So it's uncertain to work out the order of the pot , Maybe the second request takes less time to compute , Go back first .

reactor + worker thread

In order to determine the order of the returned results , We can create a compute thread for each connection , Requests on each connection are sent to the same thread for calculation , First come first served basis . The number of concurrent threads is limited ,

reactor + thread pool

This is a make-up plan 7 A flaw in creating threads for each request in , Use a fixed size thread pool . All of the IO All work in one Reactor Thread complete , And the computing task is left to thread poll. There is also the possibility of disorder .
Another function of thread pool is to perform blocking operations , Let the thread pool call blocked IO Function and other blocking operations , It can avoid blocking IO Threads , It doesn't affect other customer connections .

 Picture description here

reactors in threads

muduo、netty Built in multithreading scheme , Characteristic is one loop per thread. There are many. Reactor Threads ,muduo It's fixed size Reactor pool, There is one main Reactor be responsible for accept Connect , Then hook the connection to some sub Reactor in . In this way, all operations of the connection are in that sub Reactor Completed in the thread where it is , So the order of requests is guaranteed ; Multiple connections can be divided into multiple threads , To make the most of it CPU.

 Picture description here

reactors in processes

This is a Nginx Built in solutions for , If there is no interaction between the connections , This is a good option . Work processes are independent of each other , Can be hot upgraded ( First heard ……).

reactors + thread pool

Put the plan 9 And solution 10 blend , Use more than one Reactor To deal with it IO, We also use thread pool to process computation . This scheme is suitable for the existing burst IO( Using multithreading to process multiple connections IO), And then there's burst computing ( Use the thread pool to divide the computing task on a connection to multiple threads ) Application .

 Picture description here

版权声明
本文为[osc_ wl6d9wri]所创,转载请带上原文链接,感谢
https://chowdera.com/2020/12/20201207173937176v.html