C++网络编程踩坑记之多进程服务器,详解代码细节,多问为什么

  首先介绍多进程并发服务器是什么,然后按步骤描述怎么用,在最后给出完整server.c的代码,wrap.c错误处理代码,和client.c的代码。希望大佬多交流,敬请斧正。

是什么:

提示:这节是多进程并发服务器,其实现原理是,服务端通过lfd负责监听客户端的连接。当接受到一个客户端的连接时,建立一个子进程通过cfd与客户端进行数据通信,那么这个服务端就可以响应多个客户端。涉及到的是信号,多进程的相关知识。

服务器按处理方式可以分为迭代服务器和并发服务器两类。

  • 迭代服务器:服务器每次只能处理一个客户的请求,它实现简单但效率很低。
  • 并发服务器:同时处理多个客户请求,效率很高却实现复杂。

linux有3种实现并发服务器的方式:多进程并发服务器,多线程并发服务器,多路IO复用。


怎么用:

多进程并发服务器的实现框架。

在这里插入图片描述 图片出处

编写多进程并发服务器的基本思路:

1. 服务端与客户端建立连接:

lfd=socket() //创建 socket
bind(lfd) //绑定服务器地址结构
listen(lfd) //设置监听上限
cfd=accept() //阻塞监听客户端连接
复制代码

2. 服务器调用fork()产生新的子进程,用于处理数据通信

pid=fork();
复制代码

3. 父进程关闭连接套接字,子进程关闭监听套接字

cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
if (errno == EINTR)
    continue;
pid=fork();
if(pid==-1){
    sys_err("fork error");
}else if(pid ==0){
    close(lfd);   //子进程关闭lfd
    break;
}else{
    struct sigaction act, oldact;
    act.sa_handler= catch_child;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    ret = sigaction(SIGCHLD,&act,&oldact);
    if(ret==-1){
        sys_err("sigaction error");
    }
    close(cfd);  //父进程关闭cfd
    continue;
}
复制代码

3.1为什么要关fd?
1、节省系统资源。2、防止上面提到的父、子进程同时对共享描述符进程操作。3、确保close函数能够正确关闭套接字描述符。

3.2为什么可以确保正确关闭?
  因为引用计数的存在。在每个文件描述符或者套接字都有一个引用计数机制,只有当它的引用计数变为0的时候才会真正清理和释放该套接字的资源。

3.3为什么父进程close(cfd),子进程close(lfd),没有导致连接的断开?
  因为父进程fork(),产生子进程。是copy自己的地址空间给子进程,此时子进程拥有与父进程相同的打开的文件描述符!即,父子进程都有一个监听套接字、一个连接套接字。不严谨的说,就是在某个时刻lfd和cfd的引用计数都是2。连接建立后,父进程关闭连接套接字,子进程关闭监听套接字。两者的引用计数变成1。

3.4如果父进程从来都不关闭连接套接字会发生什么?
  子进程如果不对lfd做误操作,没什么太大影响,因为它手里就lfd和cfd这两个fd。但是,父进程不是啊,一直在accept,一直在产生cfd,由于任何进程在任何时刻可拥有的打开着的描述符数量通常是有限制的。如果父进程不关闭连接套接字会导致套接字资源的耗尽,而且,没有一个客户连接会被终止。因为这些连接套接字的引用计数值永远是1,不可能为0.

3.5为什么会导致套接字资源的耗尽?
  在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄)。可使用ulimit -n命令查看系统允许当前用户进程打开的文件数限制。
  这里要说的是,不仅仅是资源有限,而是socket建立的机制,也可以说是文件描述符建立的机制。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。进程刚被创建时,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2 记录在表中。在进程中打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中。这样相当于在一个个的消耗fd资源。
  注意:Linux中一个进程最多只能打开NR_OPEN_DEFAULT(即1024)个文件,故当文件不再使用时应及时调用close函数关闭文件。

3.6 accept()反复赋值给cfd,为什么cfd没有被覆盖,没有导致文件描述符表上的资源被覆盖?
  用户使用文件描述符(file descriptor)来访问文件,fd创建了它就在那里,只要通过close减少引用计数的方式关闭。通过赋值,服务端失去了cfd的旧值,得到了cfd的新值,只是仅仅代表着服务端失去了通过cfd对旧文件的控制,但不代表旧cfd消失在文件描述符表中。

4. 子进程处理与客户端信息通信,父进程等待其他客户端的连接。

if(pid==0){ //子进程
        while(1){
            ret = read(cfd, buf, sizeof(buf));
            if (ret == 0) {
                close(cfd);
                exit(1);  //谨记,多进程可以这么直接退出,但是多线程不可以!!!!上一篇文章有讲。
            } else if (ret == -1) {
                sys_err("read error");
            } else {
                write(STDOUT_FILENO, buf, ret);
                for (int i = 0; i < ret; i++) {
                    buf[i] = toupper(buf[i]);
                }
                write(cfd, buf, ret);
            }
        }
    }
复制代码
while(1){
        cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);  //父进程
        if (errno == EINTR)
            continue;
        pid=fork();
        if(pid==-1){
            sys_err("fork error");
        }else if(pid ==0){
            close(lfd);
            break;
        }else{  //父进程
            struct sigaction act, oldact;
            act.sa_handler= catch_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags=0;
            ret = sigaction(SIGCHLD,&act,&oldact);
            if(ret==-1){
                sys_err("sigaction error");
            }
            close(cfd);
            continue;
        }
    }
复制代码

5. 父进程注册信号捕捉函数: SIGCHLD,在回调函数中, 完成子进程回收while(waitpid())。

struct sigaction act, oldact;
act.sa_handler= catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
ret = sigaction(SIGCHLD,&act,&oldact); //注册信号捕捉函数
if(ret==-1){
    sys_err("sigaction error");
}
复制代码
void catch_child(int signum){  //捕捉到信号之后的动作
    while((waitpid(0,NULL,WNOHANG))>0){}
}
复制代码

5.1为什么要进行子进程回收?
  为了处理僵尸进程,避免占据内核资源。
  进程终止存在两种可能:
  父进程先于子进程终止会产生孤儿进程。所有子进程的父进程被改为 init 进程,就是由 init 进程领养。在一个进程终止是,系统会逐个检查所有活动进程,判断 这些进程是否是正要终止 的进程的子进程。如果是,则该进程的父进程 ID 就更改为 1(init 的 ID)。这就保证了每个 进程都有一个父进程。
  子进程先于父进程终止会产生僵尸进程。系统内核会为每个终止子进程保存一些信息,这样父进 程就可以通过调用 wait()或 waitpid()函数,获得子进程的终止信息。终止子进程保存的信息 包括进程 ID、该进程的终止状态,以及该进程使用的 CPU 时间总量。当父进程调用 wait() 或 waitpid()函数时,系统内核可以释放终止进程所使用的所有存储空间,关闭其所有打开文 件。一个已经终止,但是其父进程尚未对其进行善后处理的进程称为僵尸进程。

5.2怎么处理父进程没来得及去回收这个子进程,产生的僵尸进程?
  可以通过在父进程捕获信号,SIGCHLD信号:当一个进程正常或异常终止时,它将给其父进程发送一个SIGCHLD信号,默认情况下, 父进程忽略该信号,父进程可以注册信号捕捉函数,捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程。

5.3为什么是while((waitpid(0,NULL,WNOHANG))>0),关键为什么是while?
  options参数的WNOHANG:若由pid指定的子进程没有退出则立即返回,则waitpid不阻塞,此时返回值为0。
  当多个信号同时发送时, catch_child 只执行了一次,但由于while的存在,执行完一个waitpid后又重新回到 waitpid , 那么waitpid 就会检查是否有进程状态改变。如果有,那么继续执行,就可以回收其他子进程的资源,如果没有则返回0,退出循环。所以要用循环去处理.把僵尸进程回收完。

5.4为什么是waitpid(),而不是wait()?
  一个并发服务器, 每一个客户端连接服务器就fork一个子进程.当同时有n多个客户端断开连接时,服务器端同时有n多个子进程终止,这时候父进程的n个子进程同时向父进程发送SIGCHILD时.既然sigchld是不可靠的信号,进程就不可能对sigchld进行排队, 直接丢弃了sigchld信号(当进程注册信号的时候,发现已有sigchld注册进未决信号, 因为内核同时发送多个sigchld).
  那如何保证回收僵尸进程呢?父进程只会调用一次信号处理程序,并丢弃其余四个信号。虽然只接受了一个SIGCHLD信号,但是while((waitpid(0,NULL,WNOHANG))>0)这条语句会处理所有的僵尸子进程。这就意味着,即使丢失了一部分SIGCHLD信号,父进程也能够回收所有的子进程。
  事实上,waitpid 和 SIGCHLD 没关系,waitpid函数不是由SIGCHILD信号驱动的,即使是某个子进程对应的 SIGCHLD 丢失了,只要父进程在任何一个时刻调用了 waitpid,那么这个进程还是可以被回收的。waitpid的函数构造是,回收第一个僵尸子进程。而加上while之后就能够回收所有在while运行期间内结束的子进程。而此时如果用wait却只能处理一个进程。
  我们不能在循环内调用wait,因为没有办法阻止wait在正运行的子进程中还有未终止的进程时阻塞。当有多个进程时,有一个发送了 SIGCHLD, 那么就会进入 catch_child 处理完,wait 将会阻塞,等待其他的进程状态改变,那么这里将回不到被中断的代码中去了。而使用 waitpid 时, 因为有了 WNOHANG (return imediately if no child has exited)这个 option , 那么 waitpid 将不会阻塞在这里。
  所以waitpid可以循环调用,等待所有任意进程结束,而wait只有一次机会。


完整代码:

提示:写到这里,准备wrap.c和wrap.h错误处理准备单独开一节,就没有贴这两个代码,不过server.c中的函数首字母大写改为小写就是本来的函数原型。

//server.c
// Created by 11406 on 2022/5/19.
//
#include <string.h>
#include <strings.h>
#include<netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>

#include "wrap.h"
#define IP "127.44.44.44"
#define PORT 6266

void catch_child(int signum){
    while((waitpid(0,NULL,WNOHANG))>0){}
}

int main(int argc,char *argv[]){
    int lfd,cfd;
    int ret;
    char buf[BUFSIZ];
    pid_t pid;
    struct sockaddr_in srv_addr,clt_addr;
    socklen_t clt_addr_len = sizeof(clt_addr);
    //memset(&saddr,0,sizeof(saddr));
    bzero(&srv_addr,sizeof(srv_addr));
    srv_addr.sin_family=AF_INET;
    srv_addr.sin_port= htons(PORT);
    srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);
    lfd= Socket(AF_INET,SOCK_STREAM,0);

    Bind(lfd,(struct sockaddr *)&srv_addr, sizeof(srv_addr));
    Listen(lfd,128);

    while(1){
        cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
        if (errno == EINTR)
            continue;
        pid=fork();
        if(pid==-1){
            sys_err("fork error");
        }else if(pid ==0){
            close(lfd);
            break;
        }else{
            struct sigaction act, oldact;
            act.sa_handler= catch_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags=0;
            ret = sigaction(SIGCHLD,&act,&oldact);
            if(ret==-1){
                sys_err("sigaction error");
            }
            close(cfd);
            continue;
        }
    }
    if(pid==0){
        while(1){
            ret = read(cfd, buf, sizeof(buf));
            if (ret == 0) {
                close(cfd);
                exit(1);
            } else if (ret == -1) {
                sys_err("read error");
            } else {
                write(STDOUT_FILENO, buf, ret);
                for (int i = 0; i < ret; i++) {
                    buf[i] = toupper(buf[i]);
                }
                write(cfd, buf, ret);
            }
        }
    }

    return 0;
}
复制代码
//client.c
// Created by 11406 on 2022/5/16.
//
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <fcntl.h>
#include<netinet/in.h>
#include <sys/socket.h>
#include <ctype.h>
#include <arpa/inet.h>
#define IP "127.44.44.44"
#define PORT 6266
int main(int argc,char *argv[]){

    struct sockaddr_in addr;
    int sockfd;
    int n=0;
    char buf[256];
    bzero(&addr,sizeof(addr));  //初始化结构体
    addr.sin_family=AF_INET;
    addr.sin_port= htons(PORT );
    addr.sin_addr.s_addr = inet_addr(IP);
    sockfd= socket(AF_INET,SOCK_STREAM,0);
    connect(sockfd,(struct sockaddr*)&addr,(socklen_t)sizeof(addr));
    while(1){
        n=read(STDIN_FILENO,buf,sizeof(buf));

        write(sockfd,buf,n);
        n=read(sockfd,buf,sizeof(buf));
        write(STDOUT_FILENO,buf,n);
    }
    return 0;
}

复制代码

参考:

Linux并发服务器编程之多进程并发服务器

为什么 while((pid = waitpid(-1, &stat, WNOHANG)) > 0)能处理所有子进程

对while((pid = waitpid(-1, &stat, WNOHANG)) > 0)不懂的地方,现在懂了

文件描述符概述

僵尸进程以及如何处理

并发服务器编程模型

猜你喜欢

转载自juejin.im/post/7101549535960236069