【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现

news2024/10/5 22:39:23

🌞前言

这里我们会实现一个项目:在linux操作系统下基于OpenCV和Socket的人脸识别系统。

目录

🌞前言

🌞一、项目介绍

🌞二、项目分工

🌞三、项目难题

🌞四、实现细节

🌼4.1 关键程序

🌼4.2 运行结果

🌞五、程序分析

🌷5.2 客户端client.cpp

🌷5.3 服务端server.cpp


🌞一、项目介绍

项目简介:我们的项目是在linux操作系统下基于OpenCV和Socket的人脸识别系统。

客户端:
用于向服务器发送摄像头捕获的图像数据。

服务端:

在接收客户端发送的图像数据后,使用人脸检测算法检测图像中的人脸,并使用三种不同的人脸识别模型对检测到的人脸进行识别。然后,根据识别结果,在图像中绘制相应的标签(人名)以表示识别的结果。在绘制人脸标签时,使用了putText函数将标签绘制在原始图像上。

项目成就:我们的项目评分取得了99分,并且在考核中排名第一。

项目流程示意图:


🌞二、项目分工

在项目中,我主要负责的是

  1. 项目的整体协调和管理,包括团队沟通、进度追踪、质量控制等
  2. 项目的数据采集与标注
  3. 负责客户端和服务端的使用socket通信的代码开发
  4. 人脸检测的优化:基于给定弱分类器的Bagging集成学习算法,训练出了三个模型,通过众数投票选择最终的预测结果对人脸进行预测。
  5. 项目的路演答辩

🌞三、项目难题

1. 视频过大,难以进行网络传输

摄像头视频流中的一帧图片为480 * 640 * 3 = 921600 Bytes,一秒需要传输30帧画面,即需要网络带宽 26 MB/S,如果不对图片进行二进制编码是无法进行网络传输的,因此客户端需要对视频流进行二进制编码。

由于编码后字节数不确定,因此需要对传输进行简单协议,我们的方案是在每一帧图片传输前发送本次图片的字节大小,以让服务器明确下次所需要接受的字节数。因为字节大小的位数在4到6位不等,因此确定传输6位字节大小,小于6位的字节数,在高位填充0以达到6位(即1440填充为001440),这样即保证了传输的稳定性。

经过测试,传输带宽需求理论上降低64倍,达到了实际使用需求。

2. 视频流中的数据异常,导致客户端/服务器卡死:

对大多数显式异常进行补救处理,即尽量使得服务器运行不被异常打断,如服务器当前接收到的图片格式有误,则直接跳过本次运行,直接接收下个图片数据等一系列异常处理操作。

3. 父进程无法知道子进程是否结束

为了解决僵尸进程和孤儿进程导致的问题,我们构建了set进程池+信号机制函数,当父进程收到程序终止信号或来自子进程的终止信号,能够先终止所有的子进程,释放系统资源。

项目的进程池使用set进行构建,传统的使用vector + atomic 的构建方式无法很好的解决数据冒险的问题,原因在于虽然atomic数据类型能够保证对单个元素的操作是原子化的,但是本质原因在于对vector进行的不是原子化操作,如多进程删除vector中的多个元素,很有可能导致删除的不是正确元素,假设两个进程分别删除下标为1、2的元素,如果进程先删除了下标为1的元素,那么原来下标为2的元素此时下标将变为1,这导致了删除下标2的进程删除了原本下标为3的元素。

而set的增删改查是具体针对单个元素,删除元素是通过查找到特定元素后进行删除,本质上是删除红黑树上的节点。

注意:

"数据冒险"用于描述在处理数据时可能出现的问题或风险。它指的是当数据被不正确地处理、解释或使用时,可能导致不良的后果或意外的结果。这可能包括数据丢失、数据泄露、数据损坏或数据被误用的情况。数据冒险强调了数据质量管理和数据安全性的重要性,以避免可能造成的潜在风险和损失。

4. 人脸识别精度低

由于模型复杂度和数据集性能限制,本项目的预测性能无法十分优秀。机器学习中的传统特征匹配算法对复杂环境下的人脸识别无法尽如人意,但是本项目在此基础上设计了基于给定弱分类器的Bagging集成学习算法,其本质上是通过组合多个弱分类器,共同进行分类预测,通过众数投票选择出预测结果的一种算法,其效果往往比单一分类器更加优秀。


🌞四、实现细节

🌼4.1 关键程序

wkcv.link

#ifndef _WAKLOUIS_OPENCV_H_
#define _WAKLOUIS_OPENCV_H_

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/face.hpp>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/wait.h>
#include <atomic>
#include <unordered_set>
#include <set>
#include <fstream>

using namespace std;
using namespace cv;

#define MAX_LEN 1000000 // 最大长度定义为1000000
#define PORT_NUM 5409 // 端口号定义为5409
#define MAX_LISTEN 10 // 最大监听数定义为10
#define HANDLER_QUIT_CODE 103 // 处理器退出代码定义为103
#define IMAGE_ROWS 480 // 图像行数定义为480
#define IMAGE_COLS 640 // 图像列数定义为640
#define IMAGE_TYPE 16 // 图像类型定义为16
#define PIC_FIGURES 6 // 图片数字位数定义为6
#define PIC_MAX_BYTES 921600 // 图片最大字节数定义为921600

typedef basic_string<unsigned char> ustring; // 使用无符号字符的基本字符串类型
typedef unsigned char BYTE; // 字节类型定义为无符号字符类型

void sigquitHandler(int pid); // 定义信号处理函数

namespace wk
{
    // 将整数转换为字符串并填充零
    bool to_string_fill_zero(int num, BYTE *str)
    {
        int pos = 0;
        string temp = to_string(num); // 将整数转换为字符串
        if (temp.size() > PIC_FIGURES) // 如果转换后的字符串长度超过预定义的位数
        {
            perror("to_string_fill_zero"); // 输出错误信息
            return -1; // 返回 false
        }
        else if (temp.size() == PIC_FIGURES) // 如果转换后的字符串长度与预定义的位数相等
        {
            for (auto i : temp)
            {
                str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
            }
            return 0; // 返回 true
        }
        else // 如果转换后的字符串长度小于预定义的位数
        {
            int res = PIC_FIGURES - temp.size(); // 计算需要填充的零的数量
            for (int i = 0; i < res; i++)
            {
                str[pos++] = '0'; // 填充零
            }

            for (auto i : temp)
            {
                str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
            }

            return 0; // 返回 true
        }
    }

    // 将字节数组解析为整数
    int to_integer(BYTE *str)
    {
        int num = 0;
        for (int i = 0; i < PIC_FIGURES; i++)
        {
            int temp = str[i] - '0'; // 将字符转换为数字
            num = num * 10 + temp; // 计算整数值
        }
        return num; // 返回解析后的整数值
    }

    // 将字符串解析为整数
    int to_integer_model(string str)
    {
        int num = 0;
        for (int i = 0; i < str.size(); i++)
        {
            int temp = str[i] - '0'; // 将字符转换为数字
            num = num * 10 + temp; // 计算整数值
        }
        return num; // 返回解析后的整数值
    }

}

#endif

客户端client.cpp

#include "wkcv.link" // 包含自定义的头文件 "wkcv.link"

struct sockaddr_in server_addr, client_addr; // 定义服务器和客户端地址结构体变量
int client_sockfd, returnValue; // 客户端套接字文件描述符和返回值变量

int main(int argc, char *argv[]) // 主函数,接受命令行参数
{
    if (argc != 2) // 如果参数数量不为2
    {
        cout << "Format : ./client [Server ip]" << endl; // 输出正确的程序使用格式
        exit(-1); // 退出程序
    }

    // 创建套接字
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
    if (client_sockfd < 0) // 如果创建套接字失败
    {
        perror("Socket"); // 输出错误信息
        exit(-1); // 退出程序
    }

    // 填充服务器信息
    string ipAddress = argv[1]; // 获取服务器IP地址

    bzero(&server_addr, sizeof(server_addr)); // 清零服务器地址结构体变量
    server_addr.sin_family = AF_INET; // 设置地址族为IPv4
    server_addr.sin_port = PORT_NUM; // 设置端口号为预定义常量值
    server_addr.sin_addr.s_addr = inet_addr((char *)ipAddress.data()); // 将IP地址转换为网络字节序,并赋值给服务器地址结构体变量

    // 服务器连接
    returnValue = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 连接到服务器
    if (returnValue < 0) // 如果连接失败
    {
        perror("Connect"); // 输出错误信息
        exit(-1); // 退出程序
    }

    cout << "Connection Success to " << ipAddress << endl; // 打印连接成功的消息

    VideoCapture capture(0); // 打开摄像头,初始化摄像头捕获对象
    Mat image; // 定义Mat类型的图像对象
    vector<int> quality; // 定义保存图像压缩质量的向量
    quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数
    quality.push_back(50); // 设置图像压缩质量为50
    vector<BYTE> data_encode; // 定义保存编码后图像数据的向量

    BYTE nextImageSize_s[PIC_FIGURES]; // 定义保存下一张图像大小的字节数组
    while (1) // 进入主循环
    {
        data_encode.clear(); // 清空编码后图像数据的向量
        memset(nextImageSize_s, '\0', sizeof(nextImageSize_s)); // 将下一张图像大小的字节数组清零
        capture >> image; // 获取摄像头捕获的图像
        if (image.empty() || image.data == NULL) // 如果图像为空
        {
            continue; // 跳过当前循环,继续下一次循环
        }

        imencode(".jpeg", image, data_encode, quality); // 将图像编码为JPEG格式,并存储到data_encode中
        int nSize = data_encode.size(); // 获取编码后图像数据的大小
        wk::to_string_fill_zero(nSize, nextImageSize_s); // 将图像数据大小转换为字符串并填充零,存储到nextImageSize_s数组中
        write(client_sockfd, nextImageSize_s, PIC_FIGURES); // 将下一张图像的大小发送到服务器

        BYTE *encodeImg = new BYTE[nSize]; // 动态分配内存,用于保存编码后的图像数据
        for (int i = 0; i < nSize; i++) // 遍历编码后的图像数据
        {
            encodeImg[i] = data_encode[i]; // 将编码后的图像数据存储到encodeImg数组中
        }

        int count = write(client_sockfd, encodeImg, nSize); // 将编码后的图像数据发送到服务器
        cout << "sent " << count << endl; // 打印发送的字节数

        flip(image, image, 1); // 翻转图像,使其显示在窗口中
        imshow("client", image); // 显示图像到窗口中
        if (waitKey(30) > 0) // 等待按键输入,若检测到按键输入
        {
            break; // 跳出循环
        }

        usleep(33333); // 等待一段时间
    }

    close(client_sockfd); // 关闭套接字
    return 0; // 退出程序
}

服务端server.cpp

#include "wkcv.link"

struct sockaddr_in server_addr, client_addr;
char buffer_[MAX_LEN]{0};
int server_sockfd, client_commfd, returnValue;
set<pid_t> childLists;

// 显示的标签
string name[] = {"LiYuan", "liuZhiCong", "HuangYiFeng", "LeiKunRu",
                 "LinJingYang", "TanXin", "ZhangGuanYu", "ZhaoYuQiu", "XieDunJie",
                 "FangChengTao", "LiXueZhi", "XiaXuan", "WuWenFeng", "LiuJunFeng",
                 "LiXingHai", "ZhangZhenZhou", "ChenDaLi", "YaoYiJie", "ZhangYueYang",
                 "ZhangBeiJing", "HaoJingNa", "WuKe", "YangFeiXiang", "LiuBao", "YangJiaMing",
                 "ZhangSuJun"};

int main()
{
    // 加载人脸识别模型
    Ptr<face::LBPHFaceRecognizer> modelLBPH = face::LBPHFaceRecognizer::create();
    modelLBPH->read("../../model/save/MyFaceLBPHModel.xml");

    Ptr<face::FisherFaceRecognizer> modelFisher = face::FisherFaceRecognizer::create();
    modelFisher->read("../../model/save/MyFaceFisherModel.xml");

    Ptr<face::FaceRecognizer> modelPCA = face::EigenFaceRecognizer::create();
    modelPCA->read("../../model/save/MyFacePCAModel.xml");

    // 创建套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd < 0)
    {
        perror("Socket");
        return -1;
    }

    // 填充服务器地址信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = PORT_NUM;
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 填充
    bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero));

    // 设置套接字选项避免地址使用错误
    int on = 1;
    if ((setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
    {
        perror("setsockopt failed");
        return -1;
    }

    // 绑定
    returnValue = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (returnValue < 0)
    {
        perror("Bind");
        exit(-1);
    }

    // 侦听
    returnValue = listen(server_sockfd, MAX_LISTEN);
    if (returnValue < 0)
    {
        perror("Listen");
        exit(-1);
    }

    int connectionNum = 0;
    // 使用并发服务器模型,始终准备接收客户端连接请求
    while (1)
    {
        // 输出等待连接的消息及连接次数
        cout << "Waiting Connection " << ++connectionNum << " ... " << endl;

        // 等待接受客户端发来的连接请求
        unsigned int len = sizeof(client_addr);
        client_commfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
        if (client_commfd < 0) // 如果接受连接失败
        {
            perror("Accept"); // 输出错误信息
            continue;         // 继续等待下一个连接请求
        }

        // 输出与客户端连接成功的消息及客户端IP地址
        cout << "Get connection with : " << inet_ntoa(client_addr.sin_addr) << endl;

        // 设置信号处理函数
        signal(SIGQUIT, sigquitHandler);

        // 创建子进程处理客户端请求
        pid_t son = fork();
        if (son < 0) // 如果创建子进程失败
        {
            perror("Fork");    // 输出错误信息
            sigquitHandler(0); // 调用信号处理函数
            exit(1);           // 退出程序
        }
        childLists.insert(son); // 将子进程加入进程池

        if (son > 0) // 如果是父进程
        {
            continue; // 继续监听新的连接
        }

        // 子进程继续执行以下代码

        BYTE buffer_[PIC_MAX_BYTES];            // 定义存储图像数据的缓冲区
        BYTE nextImageSize_s[PIC_FIGURES];      // 定义存储下一张图像大小的缓冲区
        ustring full_buffer_;                   // 定义存储完整图像数据的字符串
        vector<BYTE> image_s_encoded;           // 定义存储编码后图像数据的向量
        int exitFlag = 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量

        // 人脸检测部分变量初始化
        CascadeClassifier cascade;                                            // 创建级联分类器对象
        cascade.load("../../model/save/haarcascade_frontalface_default.xml"); // 加载人脸检测模型
        vector<Rect> faces;                                                   // 定义存储检测到的人脸矩形区域的向量

        // 人脸识别部分,加载预训练的人脸识别模型

        // 循环接收客户端发送的图像数据并处理
        while (1)
        {
            // 清空数据
            image_s_encoded.clear();                             // 清空编码后图像数据向量
            memset(buffer_, '\0', sizeof(buffer_));              // 清空图像数据缓冲区
            memset(nextImageSize_s, 0, sizeof(nextImageSize_s)); // 清空下一张图像大小缓冲区
            full_buffer_.clear();                                // 清空完整图像数据字符串

            // 读取下一张图像大小信息
            read(client_commfd, nextImageSize_s, PIC_FIGURES);
            nextImageSize = wk::to_integer(nextImageSize_s); // 将缓冲区转换为整数,表示图像大小

            int received = 0;
            // 循环读取图像数据,直到接收完整
            while (1)
            {
                count = read(client_commfd, buffer_, nextImageSize - received); // 读取图像数据
                if (count < 0)                                                  // 如果读取失败
                {
                    break; // 跳出循环
                }
                for (int i = received; i < received + count; i++)
                {
                    full_buffer_[i] = buffer_[i - received]; // 将数据存入完整图像数据字符串中
                }
                received += count;             // 更新已接收的数据量
                full_buffer_[received] = '\0'; // 在字符串末尾添加结束符
                if (received == nextImageSize) // 如果接收完整
                {
                    break; // 跳出循环
                }
            }

            // 如果累计100帧没有输入信号,则中断该进程
            if (count == -1 || count == 0)
            {
                exitFlag++;          // 增加退出标志
                if (exitFlag == 100) // 如果累计到100帧
                {
                    destroyWindow(to_string(getpid()));                  // 销毁窗口
                    cout << getpid() << " Client loss, exiting" << endl; // 输出客户端丢失连接信息
                    close(client_commfd);                                // 关闭客户端连接
                    break;                                               // 跳出循环,结束子进程
                }
                continue; // 继续下一次循环
            }
            else // 如果接收到数据
            {
                exitFlag = 0; // 重置退出标志
            }

            // 将图像数据存入向量
            int temp = 0;
            while (temp < nextImageSize)
            {
                image_s_encoded.push_back(full_buffer_[temp++]); // 存入图像数据向量
            }

            // 解码图像数据
            Mat imageColor = imdecode(image_s_encoded, IMREAD_COLOR); // 解码为彩色图像
            if (imageColor.data == NULL)                              // 如果解码失败
            {
                continue; // 继续下一次循环
            }
            Mat image;
            cvtColor(imageColor, image, COLOR_BGR2GRAY); // 转换为灰度图像

            // 人脸检测
            flip(imageColor, imageColor, 1); // 图像翻转
            flip(image, image, 1);
            faces.clear();                                                    // 清空人脸矩形区域向量
            cascade.detectMultiScale(image, faces, 1.1, 20, 0, Size(70, 70)); // 检测人脸矩形区域

            // 遍历检测到的人脸
            for (int i = 0; i < faces.size(); i++)
            {
                // 如果人脸区域大小不合适,则跳过
                if (faces[i].width <= 0 || faces[i].height <= 0 || faces[i].x + faces[i].width > 640 || faces[i].y + faces[i].height > 480)
                {
                    perror("Size"); // 输出错误信息
                    continue;       // 继续下一次循环
                }

                RNG rng(i);                                                          // 随机数生成器
                Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), 20); // 随机颜色

                // 在图像中绘制人脸矩形区域
                rectangle(imageColor, faces[i], color, 2, 8, 0);

                // 截取人脸区域并调整大小
                Mat part = image(faces[i]);
                Size dsize = Size(92, 112);
                resize(part, part, dsize, 0, 0, INTER_AREA);

                // 使用三种不同的人脸识别模型进行预测
                int label1, label2, label3;
                double confidence1, confidence2, confidence3;
                modelLBPH->predict(part, label1, confidence1);   // LBPH算法预测
                modelFisher->predict(part, label2, confidence2); // Fisher算法预测
                modelPCA->predict(part, label3, confidence3);    // PCA算法预测

                // 根据预测结果绘制标签到图像中
                if (label1 == label2 || label1 == label3)
                {
                    putText(imageColor, name[label1], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
                }
                else if (label2 == label3)
                {
                    putText(imageColor, name[label2], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
                }
                else
                {
                    putText(imageColor, "Unidentified", Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出未识别信息
                }
            }

            // 在窗口中显示图像
            imshow(to_string(getpid()), imageColor);
            if (waitKey(17) > 0) // 等待按键输入
            {
                break; // 跳出循环,结束子进程
            }
        }
    }

    // 关闭客户端和服务器套接字
    close(client_commfd);
    close(server_sockfd);

    return 0;
}

// 信号处理函数,用于结束子进程
void sigquitHandler(int pid)
{
    // 循环遍历子进程列表
    for (auto i : childLists)
    {
        cout << i << " Exiting" << endl; // 输出子进程退出信息
        kill(i, SIGTERM);                // 向子进程发送终止信号
    }
    pid_t child_pid;
    while ((child_pid = wait(nullptr)) > 0) // 等待所有子进程退出
        ;
    _exit(HANDLER_QUIT_CODE); // 退出信号处理函数
}

🌼4.2 运行结果

测试成员一出现在摄像头面前,显示成员一的姓名标签:

测试成员二出现在摄像头面前,显示成员二的姓名标签:

测试成员三出现在摄像头面前,显示成员三的姓名标签:


🌞五、程序分析

wkcv.link是一个C++头文件,定义了一些常量、类型和函数。让我们详细分析一下:

1. 包含头文件:opencv、socket

#ifndef _WAKLOUIS_OPENCV_H_
#define _WAKLOUIS_OPENCV_H_

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/face.hpp>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/wait.h>
#include <atomic>
#include <unordered_set>
#include <set>
#include <fstream>

包含一些标准的C++和OpenCV的头文件,还有一些与网络通信相关的头文件。


2. 定义命名空间wkstd+常量

using namespace std;
using namespace cv;

#define MAX_LEN 1000000 // 最大长度定义为1000000
#define PORT_NUM 5409 // 端口号定义为5409
#define MAX_LISTEN 10 // 最大监听数定义为10
#define HANDLER_QUIT_CODE 103 // 处理器退出代码定义为103
#define IMAGE_ROWS 480 // 图像行数定义为480
#define IMAGE_COLS 640 // 图像列数定义为640
#define IMAGE_TYPE 16 // 图像类型定义为16
#define PIC_FIGURES 6 // 图片数字位数定义为6
#define PIC_MAX_BYTES 921600 // 图片最大字节数定义为921600

typedef basic_string<unsigned char> ustring; // 使用无符号字符的基本字符串类型
typedef unsigned char BYTE; // 字节类型定义为无符号字符类型

void sigquitHandler(int pid); // 定义信号处理函数

定义一些常量:包括最大长度 MAX_LEN, 端口号 PORT_NUM, 最大监听数 MAX_LISTEN, 处理器退出代码 HANDLER_QUIT_CODE, 图像行数 IMAGE_ROWS, 图像列数 IMAGE_COLS, 图像类型 IMAGE_TYPE, 图片数字位数 PIC_FIGURES, 以及图片最大字节数 PIC_MAX_BYTES


3. 命名空间wk定义了三个函数

namespace wk
{
    // 将整数转换为字符串并填充零
    bool to_string_fill_zero(int num, BYTE *str)
    {
        int pos = 0;
        string temp = to_string(num); // 将整数转换为字符串
        if (temp.size() > PIC_FIGURES) // 如果转换后的字符串长度超过预定义的位数
        {
            perror("to_string_fill_zero"); // 输出错误信息
            return -1; // 返回 false
        }
        else if (temp.size() == PIC_FIGURES) // 如果转换后的字符串长度与预定义的位数相等
        {
            for (auto i : temp)
            {
                str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
            }
            return 0; // 返回 true
        }
        else // 如果转换后的字符串长度小于预定义的位数
        {
            int res = PIC_FIGURES - temp.size(); // 计算需要填充的零的数量
            for (int i = 0; i < res; i++)
            {
                str[pos++] = '0'; // 填充零
            }

            for (auto i : temp)
            {
                str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
            }

            return 0; // 返回 true
        }
    }

    // 将字节数组解析为整数
    int to_integer(BYTE *str)
    {
        int num = 0;
        for (int i = 0; i < PIC_FIGURES; i++)
        {
            int temp = str[i] - '0'; // 将字符转换为数字
            num = num * 10 + temp; // 计算整数值
        }
        return num; // 返回解析后的整数值
    }

    // 将字符串解析为整数
    int to_integer_model(string str)
    {
        int num = 0;
        for (int i = 0; i < str.size(); i++)
        {
            int temp = str[i] - '0'; // 将字符转换为数字
            num = num * 10 + temp; // 计算整数值
        }
        return num; // 返回解析后的整数值
    }

}

#endif

在命名空间 wk 中定义了几个函数

bool to_string_fill_zero(int num, BYTE *str)
这段函数的作用是将整数转换为字符串并存在字节数组中,并根据预定义的位数填充零。具体步骤如下:

  1. 首先将整数转换为字符串。
  2. 如果转换后的字符串长度超过预定义的位数 PIC_FIGURES,则输出错误信息并返回 false。
  3. 如果转换后的字符串长度与预定义的位数相等,则将转换后的字符串按位存储到字节数组中,并返回 true。
  4. 如果转换后的字符串长度小于预定义的位数,则计算需要填充的零的数量,并在字节数组中填充零,然后将转换后的字符串按位存储到字节数组中,并返回 true。

int to_integer(BYTE *str)
这段程序的作用是将字节数组解析为一个整数。具体步骤如下:

  1. 初始化一个整数 num 为 0。
  2. 使用一个循环遍历字节数组 str 的前 PIC_FIGURES 个元素。
  3. 将每个字符减去字符 '0' 的 ASCII 值,将其转换为对应的数字。
  4. 根据位置权重,将每个数字乘以 10 的相应次方并加到 num 上,得到最终的整数值。
  5. 返回解析后的整数值。

int to_integer_model(string str)
这段程序的作用是将一个字符串解析为一个整数。具体步骤如下:

  1. 初始化一个整数 num 为 0。
  2. 使用一个循环遍历字符串 str 的每个字符。
  3. 将每个字符减去字符 '0' 的 ASCII 值,将其转换为对应的数字。
  4. 根据位置权重,将每个数字乘以 10 的相应次方并加到 num 上,得到最终的整数值。
  5. 返回解析后的整数值。

🌷5.2 客户端client.cpp

client.cpp是一个客户端程序,用于与服务器进行通讯。让我们分步来看:

1. 命令行参数检查

    if (argc != 2) // 如果参数数量不为2
    {
        cout << "Format : ./client [Server ip]" << endl; // 输出正确的程序使用格式
        exit(-1); // 退出程序
    }

这段代码是在程序开始时对命令行参数进行检查。程序预期接收两个参数:服务端的IP地址和端口号。argc表示命令行参数的数量,argv是一个指向参数数组的指针。

argc != 2:检查参数数量是否等于2,如果不等于2,说明用户没有提供正确的参数数量。
这里执行客户端命令用的是./client 2003。参数分别是:

  • ./client 2003:表示程序名称。
  • 2003:表示服务端的通讯端口。

2.创建客户端socket

    // 创建套接字
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
    if (client_sockfd < 0) // 如果创建套接字失败
    {
        perror("Socket"); // 输出错误信息
        exit(-1); // 退出程序
    }

这段程序的作用是创建客户端的套接字(socket),并进行创建的错误检查。程序分析:

  • int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    这行代码创建了一个套接字,其中:
    AF_INET 指定了套接字的地址族为IPv4。
    SOCK_STREAM 指定了套接字的类型为流式套接字,即TCP套接字。
    0 表示使用默认的协议。
  • if (sockfd < -1)
    这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket() 函数返回 -1,程序通过 perror("socket") 输出相关错误信息,然后返回 -1 表示程序执行失败。

3. 将服务端发送连接请求

    // 向服务器发起连接请求
    string ipAddress = argv[1]; // 获取服务器IP地址

    bzero(&server_addr, sizeof(server_addr)); // 清零服务器地址结构体变量
    server_addr.sin_family = AF_INET; // 设置地址族为IPv4
    server_addr.sin_port = PORT_NUM; // 设置端口号为预定义常量值
    server_addr.sin_addr.s_addr = inet_addr((char *)ipAddress.data()); // 将IP地址转换为网络字节序,并赋值给服务器地址结构体变量
    returnValue = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 连接到服务器
    if (returnValue < 0) // 如果连接失败
    {
        perror("Connect"); // 输出错误信息
        exit(-1); // 退出程序
    }

    cout << "Connection Success to " << ipAddress << endl; // 打印连接成功的消息

这段代码的作用是向服务器发起连接请求,并在连接成功或失败时进行相应的处理和输出。具体来说:

  1. 从命令行参数中获取服务器的 IP 地址,该 IP 地址作为连接目标。
  2. 使用 bzero() 函数清零了一个用于存储服务器地址信息的结构体变量 server_addr,以确保其所有字段都是零。
  3. 设置了 server_addr 结构体的成员:
    • sin_family 设置为 AF_INET,表示使用 IPv4 地址族。
    • sin_port 设置为预定义的端口号常量 PORT_NUM,表示连接的目标端口。
    • sin_addr.s_addr 使用 inet_addr() 将 IP 地址转换为网络字节序,并将结果赋值给 server_addr 结构体的 sin_addr 成员。
  4. 调用 connect() 函数,向服务器发起连接请求,参数包括客户端套接字描述符 client_sockfd,指向 server_addr 结构体的指针,以及结构体的大小。
  5. 检查 connect() 的返回值,如果返回值小于 0,说明连接失败,使用 perror() 输出错误信息,然后调用 exit() 退出程序。
  6. 如果连接成功,使用 cout 输出连接成功的消息,其中包括连接的目标 IP 地址。

4. 打开默认摄像头

    //捕获摄像头图像
    VideoCapture capture(0); // 打开摄像头,初始化摄像头捕获对象
    Mat image; // 定义Mat类型的图像对象
    vector<int> quality; // 定义保存图像压缩质量的向量
    quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数
    quality.push_back(50); // 设置图像压缩质量为50
    vector<BYTE> data_encode; // 定义保存编码后图像数据的向量
    BYTE nextImageSize_s[PIC_FIGURES]; // 定义保存下一张图像大小的字节数组

这段程序的作用是捕获摄像头图像。具体步骤如下:

  1. 使用 VideoCapture 类打开摄像头,初始化摄像头捕获对象 capture
  2. 定义 Mat 类型的图像对象 image,用于存储捕获到的图像。
  3. 定义一个 vector<int> 类型的向量 quality,用于保存图像压缩质量参数。
  4. 设置图像压缩参数,将压缩质量设置为50,并将其存入 quality 向量中。
  5. 定义一个 vector<BYTE> 类型的向量 data_encode,用于保存编码后的图像数据。
  6. 定义一个字节数组 nextImageSize_s,用于保存下一张图像大小的信息。

5. 编码的视频流传输

 while (1) // 进入主循环
    {
        data_encode.clear(); // 清空编码后图像数据的向量
        memset(nextImageSize_s, '\0', sizeof(nextImageSize_s)); // 将下一张图像大小的字节数组清零
        capture >> image; // 获取摄像头捕获的图像
        if (image.empty() || image.data == NULL) // 如果图像为空
        {
            continue; // 跳过当前循环,继续下一次循环
        }

        imencode(".jpeg", image, data_encode, quality); // 将图像编码为JPEG格式,并存储到data_encode中
        int nSize = data_encode.size(); // 获取编码后图像数据的大小
        wk::to_string_fill_zero(nSize, nextImageSize_s); // 将图像数据大小转换为字符串并填充零,存储到nextImageSize_s数组中
        write(client_sockfd, nextImageSize_s, PIC_FIGURES); // 将下一张图像的大小发送到服务器

        BYTE *encodeImg = new BYTE[nSize]; // 动态分配内存,用于保存编码后的图像数据
        for (int i = 0; i < nSize; i++) // 遍历编码后的图像数据
        {
            encodeImg[i] = data_encode[i]; // 将编码后的图像数据存储到encodeImg数组中
        }

        int count = write(client_sockfd, encodeImg, nSize); // 将编码后的图像数据发送到服务器
        cout << "sent " << count << endl; // 打印发送的字节数

        flip(image, image, 1); // 翻转图像,使其显示在窗口中
        imshow("client", image); // 显示图像到窗口中
        if (waitKey(30) > 0) // 等待按键输入,若检测到按键输入
        {
            break; // 跳出循环
        }

        usleep(33333); // 等待一段时间
    }

这段程序的作用是在一个无限循环中捕获摄像头图像,将图像编码为JPEG格式,并将编码后的图像数据发送到服务器。具体步骤如下:

在一个无限循环中,不断执行以下操作:

  • 清空编码后图像数据的向量 data_encode
  • 将下一张图像大小的字节数组 nextImageSize_s 清零。
  • 使用 capture >> image 获取摄像头捕获的图像。
  • 如果图像为空或者图像数据为空,则跳过当前循环,继续下一次循环。
  • 使用 imencode() 函数将图像编码为JPEG格式,并将编码后的图像数据存储到 data_encode 向量中。
  • 获取编码后图像数据的大小,并将其转换为字符串并填充零,存储到 nextImageSize_s 数组中。
  • 使用 write() 函数将下一张图像的大小发送到服务器。
  • 动态分配内存,用于保存编码后的图像数据,并将编码后的图像数据发送到服务器。
  • 打印发送的字节数。
  • 翻转图像,以便在窗口中正常显示。
  • 显示图像到名为 "client" 的窗口中。
  • 使用 waitKey() 函数等待按键输入,如果检测到按键输入,则跳出循环。
  • 使用 usleep() 函数等待一段时间,以控制图像发送的频率。

注意:这段代码中的窗口是由 OpenCV 库提供的功能创建的。使用了 imshow() 函数来显示图像在一个名为 "client" 的窗口中,而这个窗口是由 OpenCV 提供的图像显示功能创建的。


6.关闭socket

    //关闭连接
    close(client_sockfd); // 关闭套接字

close()函数用于关闭客户端套接字,释放资源。


🌷5.3 服务端server.cpp

1. 手写标签

//显示的标签
string name[] = {"LiYuan", "liuZhiCong", "HuangYiFeng", "LeiKunRu",
                 "LinJingYang", "TanXin", "ZhangGuanYu", "ZhaoYuQiu", "XieDunJie",
                 "FangChengTao", "LiXueZhi", "XiaXuan", "WuWenFeng", "LiuJunFeng",
                 "LiXingHai", "ZhangZhenZhou", "ChenDaLi", "YaoYiJie", "ZhangYueYang",
                 "ZhangBeiJing", "HaoJingNa", "WuKe", "YangFeiXiang", "LiuBao", "YangJiaMing",
                 "ZhangSuJun"};

2. 加载人脸识别模型

    // 加载人脸识别模型
    Ptr<face::LBPHFaceRecognizer> modelLBPH = face::LBPHFaceRecognizer::create();
    modelLBPH->read("../../model/save/MyFaceLBPHModel.xml");

    Ptr<face::FisherFaceRecognizer> modelFisher = face::FisherFaceRecognizer::create();
    modelFisher->read("../../model/save/MyFaceFisherModel.xml");

    Ptr<face::FaceRecognizer> modelPCA = face::EigenFaceRecognizer::create();
    modelPCA->read("../../model/save/MyFacePCAModel.xml");

它使用了 OpenCV 的人脸识别模块中的三种不同的识别器:LBPH、Fisher、 PCA。这些模型在之前通过训练得到,并保存在 XML 文件中。

通过 read() 方法,这些模型从 XML 文件中加载到程序中,以便后续在图像上进行人脸识别。


1. 创建服务端的socket

    // 创建套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd < 0)
    {
        perror("Socket");
        return -1;
    }

这段代码的作用是创建一个套接字,用于在服务器端监听客户端的连接请求。具体来说:

  • 使用 socket() 函数创建一个套接字,指定地址族为
    IPv4(AF_INET)
    类型为流式套接字(SOCK_STREAM)
    协议为默认协议(0)。
  • 如果创建套接字失败(返回值小于 0),则输出错误信息并返回 -1 表示失败。

这段代码通常用于服务器端程序的初始化阶段,用于准备接受客户端的连接请求。


2.绑定IP地址和端口

    // 填充服务器地址信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = PORT_NUM;
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 填充
    bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero));

    // 设置套接字选项避免地址使用错误
    int on = 1;
    if ((setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
    {
        perror("setsockopt failed");
        return -1;
    }

    // 绑定
    returnValue = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (returnValue < 0)
    {
        perror("Bind");
        exit(-1);
    }

这段程序的作用是配置服务器套接字的地址信息,并将套接字与特定的网络地址和端口号绑定在一起,以便服务器能够接受客户端的连接请求。具体来说:

  • 通过 server_addr.sin_family = AF_INET; 设置地址族为 IPv4。
  • 通过 server_addr.sin_port = PORT_NUM; 设置端口号为预定义的常量 PORT_NUM
  • 通过 server_addr.sin_addr.s_addr = INADDR_ANY; 设置 IP 地址为服务器的任意可用地址。
  • 通过 bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero)); 清零结构体中未使用的部分。
  • 通过 setsockopt() 函数设置套接字选项 SO_REUSEADDR,以便在服务器重启后可以立即重用先前使用的地址和端口。
  • 最后,通过 bind() 函数将套接字绑定到指定的网络地址和端口号。如果绑定失败,程序会输出错误信息并退出。

3.设置监听状态

    // 侦听
    returnValue = listen(server_sockfd, MAX_LISTEN);
    if (returnValue < 0)
    {
        perror("Listen");
        exit(-1);
    }

这段代码的作用是让服务器套接字开始监听连接请求,使其处于被动等待状态,以便接受客户端的连接请求。具体来说:

  • 使用 listen() 函数告诉操作系统,该套接字处于监听状态,并且可以接受来自客户端的连接请求。
  • listen() 函数的第一个参数是要监听的套接字描述符,即 server_sockfd
  • MAX_LISTEN 是一个预定义的常量,表示服务器允许排队等待处理的最大连接数。
  • 如果 listen() 函数执行失败(返回值小于 0),则输出错误信息并退出程序。

4.接受客户端连接请求

    int connectionNum = 0;
    // 使用并发服务器模型,始终准备接收客户端连接请求
    while (1)
    {
        // 输出等待连接的消息及连接次数
        cout << "Waiting Connection " << ++connectionNum << " ... " << endl;

        // 等待接受客户端发来的连接请求
        unsigned int len = sizeof(client_addr);
        client_commfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
        if (client_commfd < 0) // 如果接受连接失败
        {
            perror("Accept"); // 输出错误信息
            continue;         // 继续等待下一个连接请求
        }

        // 输出与客户端连接成功的消息及客户端IP地址
        cout << "Get connection with : " << inet_ntoa(client_addr.sin_addr) << endl;

这段程序的作用是创建一个并发服务器模型,它始终准备接受客户端的连接请求。具体功能包括:

  1. 初始化连接计数器 connectionNum,用于记录已经建立的连接次数。
  2. 在一个无限循环中,等待客户端的连接请求。
  3. 每次循环输出等待连接的消息以及连接次数。
  4. 使用 accept 函数接受客户端的连接请求,如果连接失败,则输出错误信息并继续等待下一个连接请求。
  5. 如果连接成功,则输出与客户端连接成功的消息以及客户端的IP地址。

5. 创建一个子进程来处理客户端的请求

       // 设置信号处理函数
        signal(SIGQUIT, sigquitHandler);

        // 创建子进程处理客户端请求
        pid_t son = fork();
        if (son < 0) // 如果创建子进程失败
        {
            perror("Fork");    // 输出错误信息
            sigquitHandler(0); // 调用信号处理函数
            exit(1);           // 退出程序
        }
        childLists.insert(son); // 将子进程加入进程池

        if (son > 0) // 如果是父进程
        {
            continue; // 继续监听新的连接
        }

        // 子进程继续执行以下代码

        BYTE buffer_[PIC_MAX_BYTES];            // 定义存储图像数据的缓冲区
        BYTE nextImageSize_s[PIC_FIGURES];      // 定义存储下一张图像大小的缓冲区
        ustring full_buffer_;                   // 定义存储完整图像数据的字符串
        vector<BYTE> image_s_encoded;           // 定义存储编码后图像数据的向量
        int exitFlag = 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量

这段程序的作用是创建一个子进程来处理客户端的请求。具体功能包括:

  1. 设置信号处理函数,当接收到 SIGQUIT 信号时调用 sigquitHandler 函数。
  2. 使用 fork() 函数创建子进程,如果创建失败,则输出错误信息,并调用信号处理函数,然后退出程序。
  3. 如果成功创建子进程,则将子进程的 PID 添加到进程池 childLists 中。
  4. 如果当前进程是父进程,则继续监听新的连接请求。
  5. 如果当前进程是子进程,则执行子进程处理的代码段,该代码段负责处理客户端请求。

6. 接受数据+人脸识别

        // 人脸识别部分,加载预训练的人脸识别模型
        // 人脸检测部分变量初始化
        CascadeClassifier cascade;                                            // 创建级联分类器对象
        cascade.load("../../model/save/haarcascade_frontalface_default.xml"); // 加载人脸检测模型
        vector<Rect> faces;                                                   // 定义存储检测到的人脸矩形区域的向量

        // 循环接收客户端发送的图像数据并处理
        while (1)
        {
            // 清空数据
            image_s_encoded.clear();                             // 清空编码后图像数据向量
            memset(buffer_, '\0', sizeof(buffer_));              // 清空图像数据缓冲区
            memset(nextImageSize_s, 0, sizeof(nextImageSize_s)); // 清空下一张图像大小缓冲区
            full_buffer_.clear();                                // 清空完整图像数据字符串

            // 读取下一张图像大小信息
            read(client_commfd, nextImageSize_s, PIC_FIGURES);
            nextImageSize = wk::to_integer(nextImageSize_s); // 将缓冲区转换为整数,表示图像大小

            int received = 0;
            // 循环读取图像数据,直到接收完整
            while (1)
            {
                count = read(client_commfd, buffer_, nextImageSize - received); // 读取图像数据
                if (count < 0)                                                  // 如果读取失败
                {
                    break; // 跳出循环
                }
                for (int i = received; i < received + count; i++)
                {
                    full_buffer_[i] = buffer_[i - received]; // 将数据存入完整图像数据字符串中
                }
                received += count;             // 更新已接收的数据量
                full_buffer_[received] = '\0'; // 在字符串末尾添加结束符
                if (received == nextImageSize) // 如果接收完整
                {
                    break; // 跳出循环
                }
            }

            // 如果累计100帧没有输入信号,则中断该进程
            if (count == -1 || count == 0)
            {
                exitFlag++;          // 增加退出标志
                if (exitFlag == 100) // 如果累计到100帧
                {
                    destroyWindow(to_string(getpid()));                  // 销毁窗口
                    cout << getpid() << " Client loss, exiting" << endl; // 输出客户端丢失连接信息
                    close(client_commfd);                                // 关闭客户端连接
                    break;                                               // 跳出循环,结束子进程
                }
                continue; // 继续下一次循环
            }
            else // 如果接收到数据
            {
                exitFlag = 0; // 重置退出标志
            }

            // 将图像数据存入向量
            int temp = 0;
            while (temp < nextImageSize)
            {
                image_s_encoded.push_back(full_buffer_[temp++]); // 存入图像数据向量
            }

            // 解码图像数据
            Mat imageColor = imdecode(image_s_encoded, IMREAD_COLOR); // 解码为彩色图像
            if (imageColor.data == NULL)                              // 如果解码失败
            {
                continue; // 继续下一次循环
            }
            Mat image;
            cvtColor(imageColor, image, COLOR_BGR2GRAY); // 转换为灰度图像

            // 人脸检测
            flip(imageColor, imageColor, 1); // 图像翻转
            flip(image, image, 1);
            faces.clear();                                                    // 清空人脸矩形区域向量
            cascade.detectMultiScale(image, faces, 1.1, 20, 0, Size(70, 70)); // 检测人脸矩形区域

            // 遍历检测到的人脸
            for (int i = 0; i < faces.size(); i++)
            {
                // 如果人脸区域大小不合适,则跳过
                if (faces[i].width <= 0 || faces[i].height <= 0 || faces[i].x + faces[i].width > 640 || faces[i].y + faces[i].height > 480)
                {
                    perror("Size"); // 输出错误信息
                    continue;       // 继续下一次循环
                }

                RNG rng(i);                                                          // 随机数生成器
                Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), 20); // 随机颜色

                // 在图像中绘制人脸矩形区域
                rectangle(imageColor, faces[i], color, 2, 8, 0);

                // 截取人脸区域并调整大小
                Mat part = image(faces[i]);
                Size dsize = Size(92, 112);
                resize(part, part, dsize, 0, 0, INTER_AREA);

                // 使用三种不同的人脸识别模型进行预测
                int label1, label2, label3;
                double confidence1, confidence2, confidence3;
                modelLBPH->predict(part, label1, confidence1);   // LBPH算法预测
                modelFisher->predict(part, label2, confidence2); // Fisher算法预测
                modelPCA->predict(part, label3, confidence3);    // PCA算法预测

                // 根据预测结果绘制标签到图像中
                if (label1 == label2 || label1 == label3)
                {
                    putText(imageColor, name[label1], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
                }
                else if (label2 == label3)
                {
                    putText(imageColor, name[label2], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
                }
                else
                {
                    putText(imageColor, "Unidentified", Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出未识别信息
                }
            }

            // 在窗口中显示图像
            imshow(to_string(getpid()), imageColor);
            if (waitKey(17) > 0) // 等待按键输入
            {
                break; // 跳出循环,结束子进程
            }
        }
    }

这段代码的作用是:

  1. 加载预训练的人脸检测模型,创建级联分类器对象 CascadeClassifier,用于检测图像中的人脸。
  2. 循环接收客户端发送的图像数据,并处理每一帧图像。
  3. 清空相关数据,准备接收下一张图像的数据。
  4. 读取客户端发送的下一张图像大小信息。
  5. 循环读取图像数据,直到接收完整一张图像。
  6. 如果累计100帧没有接收到图像数据,则中断该进程。
  7. 将接收到的图像数据存入向量,并解码为彩色图像。
  8. 进行人脸检测,检测图像中的人脸矩形区域。
  9. 遍历检测到的人脸,对每个人脸区域进行处理:
    • 绘制人脸矩形区域在彩色图像中。
    • 截取人脸区域并调整大小,以便进行人脸识别。
    • 使用三种不同的人脸识别模型进行预测。
    • 根据预测结果在图像中绘制标签,显示人脸的姓名或未识别信息。
  10. 在窗口中显示处理后的图像,并等待按键输入。
  11. 如果接收到按键输入,则跳出循环,结束子进程。

对于这段函数

// 信号处理函数,用于处理退出信号
void sigquitHandler(int pid)
{
    // 循环遍历子进程列表
    for (auto i : childLists)
    {
        cout << i << " Exiting" << endl; // 输出子进程退出信息
        kill(i, SIGTERM);                // 向子进程发送终止信号
    }
    pid_t child_pid;
    while ((child_pid = wait(nullptr)) > 0) // 等待所有子进程退出
        ;
    _exit(HANDLER_QUIT_CODE); // 退出信号处理函数
}

这个函数的作用是处理退出信号。具体来说:

  1. 它在接收到退出信号时,会向所有子进程发送终止信号 SIGTERM,要求它们正常退出。
  2. 然后,等待所有子进程都退出完成。
  3. 最后,函数本身退出,使用预定义的退出码 HANDLER_QUIT_CODE

总的来说,这个函数确保了在接收到退出信号时,所有子进程都能够被正确地终止,并等待它们退出完成后再退出。


7.关闭socket,释放资源

    // 关闭客户端和服务器套接字
    close(client_commfd);
    close(server_sockfd);

这段代码的作用是关闭套接字并释放相关资源

  • close(listenfd);
    关闭服务端用于监听客户端连接请求的套接字 listenfd。一旦服务端不再需要监听新的连接请求,可以关闭这个套接字,以释放相关资源并告知操作系统不再维护该套接字的状态信息。
  • close(clientfd);
    关闭客户端连接的套接字 clientfd。一旦服务端与客户端的通信结束,可以关闭这个套接字,释放相关资源,并结束与该客户端的通信。

通过关闭套接字,程序能够清理掉所占用的系统资源,并确保程序的正常结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1507373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【APP逆向】酒仙网预约茅台程序,包含逆向过程详解

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 所属的专栏:爬虫实战,零基础、进阶教学 景天的主页:景天科技苑 文章目录 酒仙网预约抢购茅台1.抓包分析,账户名和密码登录2.短信登录3.登录+茅台预约 密码登录酒仙网预约抢购茅台 目标:账号登…

重启 explorer 进程的正确做法(二)

重启资源管理器进程的方法不唯一&#xff0c;但长期以来大家对实施方法用的不到位。 在上一篇中我认为&#xff1a;“我们往往使用 TerminateProcess 并传入 PID 和特殊结束代码 1 或者 taskkill /f /im 等方法重启资源管理器( explorer.exe )&#xff0c;其实这是不正确的。我…

jdk17出现错误无法初始化主类 和NoClassDefFoundError:Vector的解决方法

概述&#xff1a;网上流传文章大多都是编译和运行都加下面这串代码 --add-modulesjdk.incubator.vector我估计他们大多都是复制粘贴的文章&#xff0c;这种东西就是电子垃圾&#xff0c;在idea中&#xff0c;大多人都习惯用maven来构建java项目&#xff0c;接下来我将讲解使用…

Android14音频进阶:AudioTrack如何巧妙衔接AudioFlinger(五十七)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

设计模式十:原型模式

文章目录 1、原型模式1.1 类创建过程1.2 浅拷贝1.3 深拷贝 2、示例2.1 简单形式2.2 复杂形式 3、spring中的原型模式3.1 ArrayList的原型模式3.2 spring中的原型模式 1、原型模式 原型模式就是从一个对象再创建另外一个可定制的对象&#xff0c; 而且不需要知道任何创建的细节。…

前端解决跨域问题( 6种方法 )

本专栏是汇集了一些HTML常常被遗忘的知识&#xff0c;这里算是温故而知新&#xff0c;往往这些零碎的知识点&#xff0c;在你开发中能起到炸惊效果。我们每个人都没有过目不忘&#xff0c;过久不忘的本事&#xff0c;就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…

使用JDBC操作数据库

意志、工作和等待是成功的金字塔的基石。 Will, work and wait are the pyramidal cornerstones for success. 文章目录 JDBC简介&#xff1a;JDBC访问数据库步骤StatementPreparedStatement JDBC简介&#xff1a; 在Java应用程序中&#xff0c;JDBC&#xff08;Java Database…

CSS 入门指南(二)CSS 常用样式及注册页面案例

CSS 常用样式 颜色属性 常见样式的颜色属性&#xff1a; color&#xff1a;定义文本的颜色border-color&#xff1a;定义边框的颜色background-color&#xff1a;设置背景色 颜色属性值设置方式&#xff1a; 十六进制值 - 如&#xff1a;&#xff03;FF0000一个RGB值 - 如…

docker安装ollama

拉取镜像 docker pull ollama/ollama 运行容器 &#xff08;挂载路径 D:\ollama 改成你自己喜欢的路径&#xff09; CPU only docker run -d -v D:\ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama Nvidia GPU&#xff08;没试过这个&#xff09; doc…

LeetCode203:移除链表元素

题目描述 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 解题思想 使用虚拟头节点 代码 struct ListNode {int val;ListNode* next;ListNode() :val(0), next(nullptr) {};ListNode(i…

1 Tomcat服务器Servlet入门

今日目标 web知识概述tomcat【重点】创建servlet xml anno&#xff08;注解&#xff09;servlet执行原理servlet生命周期servlet体系结构 1.web相关知识概述【了解】 1.WEB简介 学习目标 了解什么是web 内容讲解 Web&#xff08;World Wide Web&#xff09;即全球广域网…

MyBatisPlus理解

MyBatisPlus是mybatis的增强&#xff0c;mybatis是数据库持久化的框架&#xff0c;但mybatisplus并不是替代mybatis&#xff0c;而是相辅相成的关系 MyBatisPlus不会对以前使用mybatis开发的项目进行影响&#xff0c;引入后仍然正常运行。 使用方法&#xff1a; 1.在引入了对…

IDM的使用详解 IDM是哪个国家的 IDM功能强大的网络下载器 idm下载

对于idm相信大家都不陌生&#xff0c;全称是Internet Download Manager。idm是一款非常经典、功能强大的Windows文件多线程下载加速软件&#xff0c;在电脑用户中口碑极好&#xff0c;被称为必装的HTTP下载神器。那么idm下载器是哪个国家的&#xff0c;idm下载器是哪个公司的呢…

重学SpringBoot3-集成Thymeleaf

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 重学SpringBoot3-集成Thymeleaf 1. 添加Thymeleaf依赖2. 配置Thymeleaf属性&#xff08;可选&#xff09;3. 创建Thymeleaf模板4. 创建一个Controller5. 运行应用并访问页面Thymeleaf基本语法小技巧 国际化步骤 …

【漏洞复现】华三用户自助服务产品dynamiccontent.properties.xhtml接口处存在RCE漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

【python 】----Pytest基础知识与进阶知识

定义 用于编写和执行Python测试全功能测试框架(工具),是一个第三方库 安装 pip insatll pytest 安装pytest --version 校验 pytest的组成构成 不写调用语句也可以执行函数内容 在用例运行语句里面: -s:指的是开启与终端的交互,如果没有-s(程序不会输入与打印),一条用…

Tiktok/抖音旋转验证码识别方案

一、引言 在数字世界的飞速发展中&#xff0c;安全防护成为了一个不容忽视的课题。Tiktok/抖音&#xff0c;作为全球最大的短视频平台之一&#xff0c;每天都有数以亿计的用户活跃在其平台上。为了保护用户的账号安全&#xff0c;Tiktok/抖音引入了一种名为“旋转验证码”的安…

[数据结构]OJ用队列实现栈

225. 用队列实现栈 - 力扣&#xff08;LeetCode&#xff09; 官方题解&#xff1a;https://leetcode.cn/problems/implement-stack-using-queues/solutions/432204/yong-dui-lie-shi-xian-zhan-by-leetcode-solution/ 首先我们要知道 栈是一种后进先出的数据结构&#xff0c…

[Spring] IoC 控制反转和DI依赖注入和Spring中的实现以及常见面试题

目录 1. 什么是Spring 2.什么是IoC容器 3.通过实例来深入了解IoC容器的作用 3.1造一量可以定义车辆轮胎尺寸的车出现的问题 3.2解决方法 3.3IoC优势 4.DI介绍 5.Spring中的IoC和DI的实现 5.1.存对象 5.1.2 类注解 5.1.3 方法注解 5.2取对像 (依赖注入) 5.2.1.属性…

esp32 idf.py cmd powershell 环境

esp32 idf.py cmd powershell 命令行 环境 win10 推荐使用 Windows Terminal 替换自己路径 设置–>添加新配置文件–>选择cmd 或者 powershell -->保存–> 去修改命令行 启动目录&#xff0c;推荐使用父进程目录 powershell C:\WINDOWS/System32/WindowsPowe…