8-多进程并发服务器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82529395

1. 多进程并发服务器

  前面的学习基于TCP的客户端和服务端通信都是一个服务器处理一个客户端的连接请求,只有在处理完这个客户端时才会去处理下一个客户端的请求,这种服务器我们称之为迭代型服务器。

  假如现在有多个客户端去请求访问一个服务器,会导致这个服务器的处理速度变得很慢不说,后面还有很多客户端正在等待处理请求,这时服务器的压力会很大,显然迭代型服务器已经满足不了需求了,于是就有了并发型服务器。

这里写图片描述
图1-多进程并发服务器

  并发型服务器对于每一个客户端请求,服务器的父进程就fork创建一个子进程来处理客户端的请求,子进程就可以通过accept返回的新的文件描述符跟客户端进行数据通信,且这些子进程都能独立运行,因此可以同时处理多个客户端请求。

  然后父进程就可以继续调用accept取出一个客户端连接,调用fork创建一个服务器子进程处理客户端的请求,从这个过程可以看出,服务器的父进程主要的工作就是处理客户端的请求,即为每个客户端创建一个子进程,而具体的数据交互由子进程来完成。


2. 多进程并发服务器的细节

关于设计并发服务器有以下几个细节需要注意:

  1. 防止文件描述符耗尽
  2. 父进程注册SIGCHILD信号回收子进程

2.1 防止文件描述符耗尽

  父进程在调用fork的时候,子进程会继承父进程的accept函数返回的cfd文件描述符和socket函数创建的lfd文件描述符。

  然后子进程就可以通过cfd文件描述符与客户端进程通信,而子进程继承的lfd文件描述符对子进程来说没有太大的作用,那么子进程需要close(lfd),以防止文件描述符耗尽。父进程只需要处理来自客户端的连接,并不需要与客户端进行数据交互,所以父进程也需要close(cfd)。

  实际上在fork创建子进程后,子进程和父进程是共享listenfd和connfd的,这就使得listenfd和connfd两个套接字的引用计数变成2了。如果父进程不close(cfd)的话,此时cfd的引用计数是2,当子进程close(cfd)时只会让cfd的引用计数减1,而不会关闭与客户端的tcp连接。为了防止这种情况,父进程同样也需要close(cfd)。

扫描二维码关注公众号,回复: 3130290 查看本文章


2.2 注册信号回收子进程

  2. 当客户端一退出的时候同时也关闭了socket套接字,子进程调用read读取客户端会返回0,紧接着子进程应该close(cfd),然后子进程调用exit退出。

  但是子进程一退出就会变成僵尸进程,需要父进程对子进程回收(父进程除了要负责回收子进程,还要处理客户端的连接,那么父进程应该调用waitpid函数以非阻塞方式回收子进程),另外在学习进程和信号时我们知道,子进程在退出时会发送SIGCHLD信号,该信号的作用就是用来通知父进程对子进程进行回收,因此父进程可以捕捉并注册SIGCHLD信号函数的方式来回收子进程。


3. 并发服务器示例

服务端程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SERV_PORT 10001

////注册SIGCHLD信号回收子进程
void wait_child(int signo)
{
    //非阻塞方式回收子进程
    while (waitpid(0, NULL, WNOHANG) > 0);
}

int main(void)
{
    pid_t pid;
    int lfd, cfd;
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    char buf[BUFSIZ], clie_IP[BUFSIZ];
    int n, i;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(lfd, 128);

    while (1) {
        clie_addr_len = sizeof(clie_addr);
        cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
        printf("client IP:%s, port:%d\n", 
                inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
                ntohs(clie_addr.sin_port));

        //创建子进程
        pid = fork();
        if (pid < 0) {
            perror("fork error");
            close(cfd);
            exit(1);
        } else if (pid == 0) {
            close(lfd);
            break;
        } else {
            //父进程
            close(cfd);
            //捕捉SIGCHLD信号
            signal(SIGCHLD, wait_child);
        }
    }

    //子进程
    if (pid == 0) {
        while (1) {
            n = read(cfd, buf, sizeof(buf));
            if (n == 0) {      //如果read返回0,说明对端已关闭,打印退出客户端信息
            printf("client IP:%s, port:%d ----- exit\n",
                    inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)) ,
                    ntohs(clie_addr.sin_port));
                close(cfd);
                exit(0);   //子进程退出
            } else if (n == -1) {
                perror("read error");
                exit(1);
            } else {
                for (i = 0; i < n; i++){
                    buf[i] = toupper(buf[i]);
                }
                write(cfd, buf, n);
            }
        }
    }
    close(lfd);
    return 0;
}



客户端程序:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 1024
#define SERV_PORT 10001

int main(void)
{
    struct sockaddr_in servaddr;
    //缓冲区
    char buf[MAXLINE];
    int sockfd, n;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
        //连接服务器
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

        //从标准输入读取数据到缓冲区
    while (fgets(buf, MAXLINE, stdin) != NULL) {
        write(sockfd, buf, strlen(buf));
        n = read(sockfd, buf, MAXLINE);
        //判断对端是否被关闭
        if (n == 0) {
            printf("the other side has been closed\n");
            break;
        }
        else{
            //把数据输出到标准输出
            write(STDOUT_FILENO, buf, n);
        }
    }
    close(sockfd);
    return 0;
}



程序运行结果:
这里写图片描述

  服务端对每一个连接的客户端都会创建一个子进程并进行处理请求,并将请求处理的结构返回给客户端,客户端退出时,服务端打印了退出的客户端信息,此时服务端依然在accept处等待处理其他客户端发起连接


4. 总结

多进程并发服务器和单进程服务器的区别:
  对于单进程的服务器来说,一次性只能处理一个客户端请求,只有在处理完这个客户端然后断开连接时,才能继续处理下一个客户端的请求。但是如果没有客户端请求时,单进程服务端则会一直等待阻塞在accept处,其实阻塞在accept处倒还好,如果阻塞在read调用处将会导致服务端无法给其他客户端提供服务,这是很可怕的事情。而在多进程并发服务器中父进程只负责处理来自客户端的连接请求,由子进程负责处理客户端的数据请求,所以多进程并发服务器中不会出现这个问题。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82529395