IO模型:阻塞IO模型的多进程形式

163 阅读3分钟

阻塞IO模型的多进程形式

前言

《IO模型:传统IO模型(阻塞IO模型)》中,使用的是阻塞IO阻塞IO会导致进程阻塞,在处理完当前的连接之前是无法获取新的连接的,这也就导致了服务器的吞吐量低。那么如何在使用阻塞IO的前提下能够获取更多的连接请求呢?这个时候就需要用到多进程,服务器在接收到一个请求时不会直接在本进程处理,而是另外创建一个子进程进行处理,之后继续阻塞等待客户端连接,这样就可以做到接收连接请求和处理客户端请求同时进行了,实现多客户端并发。相比之前的传统IO模型中的循环服务器快上不少。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#define PORT 8080
#define BACKLOG SOMAXCONN
#define BUFFERSIZE 4089


void process_connection(int connfd) {
    char buffer[BUFFERSIZE];
    ssize_t n = read(connfd, buffer, BUFFERSIZE);
    if (n > 0) {
        buffer[n] = '\0';
        printf("收到的请求信息:\n%s\n", buffer);

        char *response = "HTTP/1.1 200 OK\r\n"
                         "Content-Type: text/html;charset=utf-8\r\n"
                         "\r\n"
                         "<html><body><h1>Hello, World!</h1></body></html>\r\n";
        write(connfd, response, strlen(response));
    }
    close(connfd);
    exit(0);
}

int main() {
    int listenfd, connfd;
    struct sockaddr_in serv_addr, client_addr;

    socklen_t addr_size;

    pid_t pid;
    int status = 0;//初始化状态

    listenfd = socket(AF_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(PORT);

    if (bind(listenfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1) {
        perror("绑定失败");
        exit(EXIT_FAILURE);
    }

    if (listen(listenfd, BACKLOG) == -1) {
        perror("监听失败");
        exit(EXIT_FAILURE);
    }

    addr_size = sizeof(client_addr);

    printf("服务器启动, 监听端口 %d\n", PORT);


    while (1) {

        connfd = accept(listenfd, (struct sockaddr *) &client_addr, &addr_size);

        int port = ntohs(client_addr.sin_port);
        struct in_addr in = client_addr.sin_addr;
        char str[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &in, str, sizeof(str));

        printf("客户端信息: ip:port = %s:%d\n", str, port);


        if (connfd == -1) {
            perror("accept失败");
            break;
        }

        pid = fork();

        if (pid > 0) {
            //父进程
            //关闭连接套接字,原因和下面子进程关闭监听套接字类似
            close(connfd);

            //options=WNOHANG,如果pid指定的子进程之一不能立即获得状态,则waitpid () 函数不应阻塞调用线程的执行。
            //options=WUNTRACED,由 pid 指定的任何子进程的状态,如果子进程已停止,并且其状态自停止以来尚未报告,也应报告给请求进程。
            waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);

            if (WIFEXITED(status)) {//如果子进程正常结束,它就返回真;否则返回假。
                //WEXITSTATUS(status) 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
                printf("status = %d\n", WEXITSTATUS(status));
            }
            if (WIFSIGNALED(status)) { //如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
                //如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
                printf("signal status = %d\n", WTERMSIG(status));
            }
            if (WIFSTOPPED(status)) {//如果当前子进程被暂停了,则返回真;否则返回假。
                //如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。
                printf("stop sig num = %d\n", WSTOPSIG(status));
            }

        } else if (pid == 0) {
            //子进程
            //子进程会导致父进程中socket引用计数+1
            //close的时候只有当引用计数为0时才会真正关闭socket
            //关闭监听套接字,这个套接字是从父进程继承过来,防止内存泄露
            close(listenfd);
            process_connection(connfd);
        } else {
            printf("创建子进程失败\n");
            exit(EXIT_FAILURE);
        }
    }

    close(listenfd);
    return 0;
}

代码解释

上面的代码实际逻辑非常简单,主要说一下几个比较重要的点:

  • fork 函数:创建子进程处理客户端请求,以实现多客户端并发处理。

  • close 函数:描述符的计数就会减1,直到计数为0。当计数为0时,也就是所有进程都调用了close,这时程序会调用shutdown函数释放套接字。

  • waitpid 函数:父进程等待子进程结束,回收子进程的资源。如果不加waitpid或wait,会导致出现僵尸进程,因为系统所能使用的进程号是有限的,如果僵尸进程积累多了会导致系统无法再创建新的进程。

其他相关文章