Linux使用匿名管道实现进程池得以高效通信

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:Nonsense—Sabrina Carpenter

                                                                0:50━━━━━━️💟──────── 2:43
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

思路梳理

匿名管道知识回忆

匿名管道实现进程池思路

池化技术怎么提高效率?

具体实现

进程以及管道的创建操作

分发任务操作

回收资源操作

解决上述所提到Bug

总体代码及代码效果

Makefile

Task.hpp

mulpipe.cpp

实现效果


思路梳理

匿名管道知识回忆

        上一篇的文章中,详细介绍了管道的知识点,下面还是复习一下对于匿名管道相关的知识点。如下是匿名管道的一个示例图,这表现了两个“有血缘关系”的两个进程之间通过匿名管道进行通信的过程,我们通过控制struct files *fd_array[]中读或者写的struct files的开关来实现两个进程间的通信:

        我们主要通过如下的接口创建匿名管道来实现以上的匿名管道通信:

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

匿名管道实现进程池思路

        说大白话(●—●):我们使用一个父进程创建很多的管道,再创建对应数量的进程,然后这些管道分别与其他的进程进行直接的连接。这样我们就提前创建和联系好了一定数量的进程。我们想让父进程向其中一个子进程发消息就可通过选择管道直接发消息,而如果不发消息其它进程只会等待。通过信息的发送,我们可以就通过选择管道从而让子进程分别执行对应的任务。大致的图解如下:

池化技术怎么提高效率?

        池化技术通过资源共享和优化来提高效率,具体表现在以下几个方面:

  • 资源复用:池化技术通过重用已创建的资源,减少了频繁创建和销毁资源的开销。例如,线程池中的线程可以被多个任务重复使用,这样可以避免每次任务执行时都创建新线程的开销。
  • 减少等待时间:池化技术可以减少请求的等待时间。因为资源是预先分配好的,当有新的请求到来时,可以立即使用池中的资源,而不需要等待资源的创建过程。
  • 提高响应速度:由于资源已经准备好,池化技术可以快速响应请求,提高了处理速度。这对于需要快速响应的系统来说尤其重要。
  • 统一管理:池化技术提供了对资源的集中管理,这有助于监控系统资源的使用情况,及时回收不再使用的资源,避免资源浪费。
  • 优化性能:在大数据处理等场景中,池化技术可以通过合并多个请求来减少数据处理的时间和空间复杂度,从而提高数据处理的性能。

具体实现

进程以及管道的创建操作

        创建一个channel类用来来存储管道的读文件描述符ctrlfd以及进程描述符workerid,完成初始化操作以及后续的销毁操作。

static int number = 1;//标识对应的进程和管道

class channel
{
public:
    channel(int fd, pid_t id) : ctrlfd(fd), workerid(id)
    {
        name = "channel-" + std::to_string(number++);
    }

public:
    int ctrlfd;
    pid_t workerid;
    std::string name;
};

        再根据先描述在组织的原则,我们在主函数使用一个std::vector<channel> channels来管理上面的结构体,可以根据需求进行增删查改等等操作。在完成这些预备操作后,创建对应数量的进程以及管道。

        特别注意:如下函数中的std::vector<int> old;以及如下代码是为了解决父子进程继承而产生的一些bug,这个将在最后解释:

        	std::vector<int> old;
			if(!old.empty())
            {
                for(auto fd : old)
                {
                    close(fd);
                }
                PrintFd(old);
            }
       		 old.push_back(pipefd[1]);

        如下函数的操作为创建管道以及进程,可以先忽略上述所提到的解决bug的代码,通过循环创建对应的子进程、关闭父子进程对应的读写文件,并且存储到vector<channel> *c(也就是主函数的中channels),下面的work()函数为子进程要做的工作,可以理解了后面的分发任务操作再来理解。

const int num = 5;//全局定义创建管道以及进程数
void CreateChannels(std::vector<channel> *c)
{
    std::vector<int> old;
    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0) // child
        {
            if(!old.empty())
            {
                for(auto fd : old)
                {
                    close(fd);
                }
                PrintFd(old);
            }
            close(pipefd[1]);//关闭写
            dup2(pipefd[0], 0);//重定向写入
            Work();//子进程工作
            exit(0); // 会自动关闭自己打开的所有的fd
        }

        // father
        close(pipefd[0]);
        c->push_back(channel(pipefd[1], id));
        old.push_back(pipefd[1]);
        // childid, pipefd[1]
    }
}

        因为前面我们已经重定向了管道的写入作为子进程的写入,接下来通过read就会读取对应的操作,需要注意的是:我们是通过一个int类型的变量来控制要完成的任务,后续再task.hpp中会定义对应要完成的任务。通过read的返回值来判断是要执行任务还是退出,我们在管道的知识中知道,read会等待write,也就是等待数据的输入。当写进程退出,读进程会跟着退出,我们根据以上特性来判断是要执行任务还是等待任务还是退出进程:

void Work()
{
    while (true)
    {
        int code = 0;
        ssize_t n = read(0, &code, sizeof(code));
        if (n == sizeof(code))
        {
            if (!init.CheckSafe(code))
                continue;
            init.RunTask(code);
        }
        else if (n == 0)
        {
            break;
        }
        else
        {
            // do nothing
        }
    }

    std::cout << "child quit" << std::endl;
}

分发任务操作

        当我们创建好管道以及进程后,可以注意到接下来的都是没有经过 if (id == 0) 控制下的程序,也就是说接下来的程序都是父进程运行的程序,因此我们就可以让父进程来分配任务使得子进程完成任务。接下来,我们创建一个task.hpp来模拟创建的任务以及对应的封装、任务分发等等操作如下:

#pragma once

#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <stdlib.h>

// using task_t = std::function<void()>;
typedef std::function<void()> task_t;

void Download()
{
    std::cout << "我是一个下载任务"
              << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务"
              << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "这是一个推送视频流的任务"
              << " 处理者: " << getpid() << std::endl;
}

// void ProcessExit()
// {
//     exit(0);
// }

class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;

public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());
    }
    bool CheckSafe(int code)
    {
        if (code >= 0 && code < tasks.size())
            return true;
        else
            return false;
    }
    void RunTask(int code)
    {
        return tasks[code]();
    }
    int SelectTask()
    {
        return rand() % tasks.size();
    }
    std::string ToDesc(int code)
    {
        switch (code)
        {
        case g_download_code:
            return "Download";
        case g_printlog_code:
            return "PrintLog";
        case g_push_videostream_code:
            return "PushVideoStream";
        default:
            return "Unknow";
        }
    }
};

Init init; // 定义对象

        传入主函数用于管理的channels,flag用于控制进程执行完任务后是否需要退出(1表示要退出,0表示不退出),num为要执行任务的次数,需要注意的是num是要在flag为1的前提下才能有效的,如果不传则程序只会执行一次。在选择完任务以及进程后,通过write来向指定的管道写入。具体读写操作可看:Linux进程间通信(IPC)机制之一:管道(Pipes)详解:匿名管道的特性与情况

void SendCommand(const std::vector<channel> &c, bool flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务
        int command = init.SelectTask();

        // 2. 选择信道(进程)
        const auto &channel = c[pos++];
        pos %= c.size();

        // debug
        std::cout << "send command " << init.ToDesc(command) << "[" << command << "]"
                  << " in "
                  << channel.name << " worker is : " << channel.workerid << std::endl;

        // 3. 发送任务
        write(channel.ctrlfd, &command, sizeof(command));

        // 4. 判断是否要退出
        if (!flag)
        {
            num--;
            if (num <= 0)
                break;
        }
        sleep(1);
    }

    std::cout << "SendCommand done..." << std::endl;
}

回收资源操作

        通过close关闭对应的管道,waitpid等待子进程的结束。

const int num = 5;//全局定义创建管道以及进程数
void ReleaseChannels(std::vector<channel> c)
{
    // version 2
    // int num = c.size() - 1;

    // for (; num >= 0; num--)
    // {
    //     close(c[num].ctrlfd);
    //     waitpid(c[num].workerid, nullptr, 0);
    // }

    // version 1
    for (const auto &channel : c)
    {
        close(channel.ctrlfd);
        waitpid(channel.workerid, nullptr, 0);
    }
    // for (const auto &channel : c)
    // {
    //     pid_t rid = waitpid(channel.workerid, nullptr, 0);
    //     if (rid == channel.workerid)
    //     {
    //         std::cout << "wait child: " << channel.workerid << " success" << std::endl;
    //     }
    // }
}

解决上述所提到Bug

        如下代码:

        	std::vector<int> old;
			if(!old.empty())
            {
                for(auto fd : old)
                {
                    close(fd);
                }
                PrintFd(old);
            }
       		 old.push_back(pipefd[1]);

        这是个什么Bug呢?如果不加上上这段代码,那么关闭进程及管道就必须从最后面生成的进程和管道向前关闭。为啥呢?这是因为当我们父进程依次创建子进程,其中的对于管道的读写操作struct file也被继承了下来,也就是说有着上一个生成的子进程会被下一个子进程的写操作struct file指向,而下下个生成的子进程会指向前面两个子进程,以此类推...因此,我们需要在创建的时候关闭新创建子进程对应的写操作。

        具体的子进程继承写操作的struct file例子如下:

总体代码及代码效果

Makefile

processpool:mulpipe.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f processpool

Task.hpp

#pragma once

#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <stdlib.h>

// using task_t = std::function<void()>;
typedef std::function<void()> task_t;

void Download()
{
    std::cout << "我是一个下载任务"
              << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务"
              << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "这是一个推送视频流的任务"
              << " 处理者: " << getpid() << std::endl;
}

// void ProcessExit()
// {
//     exit(0);
// }

class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;

public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());
    }
    bool CheckSafe(int code)
    {
        if (code >= 0 && code < tasks.size())
            return true;
        else
            return false;
    }
    void RunTask(int code)
    {
        return tasks[code]();
    }
    int SelectTask()
    {
        return rand() % tasks.size();
    }
    std::string ToDesc(int code)
    {
        switch (code)
        {
        case g_download_code:
            return "Download";
        case g_printlog_code:
            return "PrintLog";
        case g_push_videostream_code:
            return "PushVideoStream";
        default:
            return "Unknow";
        }
    }
};

Init init; // 定义对象

mulpipe.cpp

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

const int num = 5;
static int number = 1;

class channel
{
public:
    channel(int fd, pid_t id) : ctrlfd(fd), workerid(id)
    {
        name = "channel-" + std::to_string(number++);
    }

public:
    int ctrlfd;
    pid_t workerid;
    std::string name;
};

void Work()
{
    while (true)
    {
        int code = 0;
        ssize_t n = read(0, &code, sizeof(code));
        if (n == sizeof(code))
        {
            if (!init.CheckSafe(code))
                continue;
            init.RunTask(code);
        }
        else if (n == 0)
        {
            break;
        }
        else
        {
            // do nothing
        }
    }

    std::cout << "child quit" << std::endl;
}

void PrintFd(const std::vector<int> &fds)
{
    std::cout << getpid() << " close fds: ";
    for(auto fd : fds)
    {
        std::cout << fd << " ";
    }
    std::cout << std::endl;
}

// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&
void CreateChannels(std::vector<channel> *c)
{
    // bug
    std::vector<int> old;
    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0) // child
        {
            if(!old.empty())
            {
                for(auto fd : old)
                {
                    close(fd);
                }
                PrintFd(old);
            }
            close(pipefd[1]);//关闭写
            dup2(pipefd[0], 0);//重定向写入
            Work();
            exit(0); // 会自动关闭自己打开的所有的fd
        }

        // father
        close(pipefd[0]);
        c->push_back(channel(pipefd[1], id));
        old.push_back(pipefd[1]);
        // childid, pipefd[1]
    }
}

void PrintDebug(const std::vector<channel> &c)
{
    for (const auto &channel : c)
    {
        std::cout << channel.name << ", " << channel.ctrlfd << ", " << channel.workerid << std::endl;
    }
}

void SendCommand(const std::vector<channel> &c, bool flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务
        int command = init.SelectTask();

        // 2. 选择信道(进程)
        const auto &channel = c[pos++];
        pos %= c.size();

        // debug
        std::cout << "send command " << init.ToDesc(command) << "[" << command << "]"
                  << " in "
                  << channel.name << " worker is : " << channel.workerid << std::endl;

        // 3. 发送任务
        write(channel.ctrlfd, &command, sizeof(command));

        // 4. 判断是否要退出
        if (!flag)
        {
            num--;
            if (num <= 0)
                break;
        }
        sleep(1);
    }

    std::cout << "SendCommand done..." << std::endl;
}
void ReleaseChannels(std::vector<channel> c)
{
    // version 2
    // int num = c.size() - 1;

    // for (; num >= 0; num--)
    // {
    //     close(c[num].ctrlfd);
    //     waitpid(c[num].workerid, nullptr, 0);
    // }

    // version 1
    for (const auto &channel : c)
    {
        close(channel.ctrlfd);
        waitpid(channel.workerid, nullptr, 0);
    }
    // for (const auto &channel : c)
    // {
    //     pid_t rid = waitpid(channel.workerid, nullptr, 0);
    //     if (rid == channel.workerid)
    //     {
    //         std::cout << "wait child: " << channel.workerid << " success" << std::endl;
    //     }
    // }
}
int main()
{
    std::vector<channel> channels;
    // 1. 创建信道,创建进程
    CreateChannels(&channels);

    // 2. 开始发送任务
    const bool g_always_loop = true;
    // SendCommand(channels, g_always_loop);
    SendCommand(channels, !g_always_loop, 10);

    // 3. 回收资源,想让子进程退出,并且释放管道,只要关闭写端
    ReleaseChannels(channels);

    return 0;
}

实现效果


                   感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity 外观模式(实例详解)

文章目录 示例1&#xff1a;初始化游戏场景中的多个子系统示例2&#xff1a;管理音频播放示例3&#xff1a;场景加载流程示例4&#xff1a;UI管理器示例5&#xff1a;网络服务通信 在Unity中使用外观模式&#xff08;Facade&#xff09;时&#xff0c;主要目的是为了简化复杂子…

Android创建工程

语言选择Java&#xff0c;我用的Java 最小SDK&#xff1a;就是开发的APP支持的最小安卓版本 Gradle 是一款Google 推出的基于 JVM、通用灵活的项目构建工具&#xff0c;支持 Maven&#xff0c;JCenter 多种第三方仓库;支持传递性依赖管理、废弃了繁杂的xml 文件&#xff0c;转而…

如何快速掌握DDT数据驱动测试?

前言 网盗概念相同的测试脚本使用不同的测试数据来执行&#xff0c;测试数据和测试行为完全分离&#xff0c; 这样的测试脚本设计模式称为数据驱动。(网盗结束)当我们测试某个网站的登录功能时&#xff0c;我们往往会使用不同的用户名和密码来验证登录模块对系统的影响&#x…

2023年:个人年度成长与团队协作成就

文章目录 个人职业发展的喜悦团队成就的辉煌公众号CSDN申请了移动安全领域新星创作者获得6月城市之星北京TOP 10获得23年博客之星TOP 41年度总结 知识星球 开拓新领域的决心免费知识大陆付费知识大陆 展望未来福利时间知识星球会员一年知识星球立减88券 在这个充满挑战与机遇的…

Linux 挂载读取、卸载 ntfs格式硬盘

windows常用的ntfs硬盘分区格式&#xff0c;在linux通常不能直接读取&#xff0c;不过挂载也是非常容易 一、挂载ntfs分区 1.安装 apt-get install ntfs-3g2.查看现在接上的硬盘 fdisk -l可以找到类似如下的&#xff0c;会显示microsoft basic data 3.创建挂载的目录 创…

全能相似度计算与语义匹配搜索工具包,多维度实现多种算法,涵盖文本、图像等领域。支持文图搜索,满足您在不同场景下的搜索需求

全能相似度计算与语义匹配搜索工具包,多维度实现多种算法,涵盖文本、图像等领域。支持文图搜索,满足您在不同场景下的搜索需求。 Similarities:精准相似度计算与语义匹配搜索工具包,多维度实现多种算法,覆盖文本、图像等领域,支持文搜、图搜文、图搜图匹配搜索 Similar…

腾讯云超60款产品,限时免费试用!

懂行的开发同学都应该知道&#xff0c;腾讯云的产品嘎嘎好用&#xff01; 很多同学只能眼红&#xff0c;却不能体验一二。毕竟付费的门槛在那里&#xff0c;体验不到&#xff0c;自然没办法做出评价。 今天&#xff0c;开放10个免费体验产品的名额给你&#xff0c;想体验哪款…

4核16G幻兽帕鲁服务器优惠价格表,阿里云和腾讯云报价

幻兽帕鲁服务器价格多少钱&#xff1f;4核16G服务器Palworld官方推荐配置&#xff0c;阿里云4核16G服务器32元1个月、96元3个月&#xff0c;腾讯云幻兽帕鲁服务器服务器4核16G14M带宽66元一个月、277元3个月&#xff0c;8核32G22M配置115元1个月、345元3个月&#xff0c;16核64…

【文本到上下文 #7】探索 NLP 中的 Seq2Seq、编码器-解码器和注意力机制

一、说明 今天&#xff0c;我们将探讨序列到序列 &#xff08;seq2seq&#xff09; 模型的复杂工作原理&#xff0c;特别关注编码器-解码器架构和注意力机制。这些概念是各种 NLP 应用的基础&#xff0c;从机器翻译到问答系统。 这是可以期待的&#xff1a; Seq2Seq模型中的编码…

【技术分享】远程透传网关-单网口快速实现威纶通触摸屏程序远程上下载

准备工作 一台可联网操作的电脑一台单网口的远程透传网关及博达远程透传配置工具网线一条&#xff0c;用于实现网络连接和连接触摸屏一台威纶通触摸屏及其编程软件一张4G卡或WIFI天线实现通讯(使用4G联网则插入4G SIM卡&#xff0c;WIFI联网则将WIFI天线插入USB口&#xff09;…

Linux实验记录:添加硬盘设备

前言&#xff1a; 本文是一篇关于Linux系统初学者的实验记录。 参考书籍&#xff1a;《Linux就该这么学》 实验环境&#xff1a; VmwareWorkStation 17——虚拟机软件 RedHatEnterpriseLinux[RHEL]8——红帽操作系统 目录 前言&#xff1a; 备注&#xff1a; 添加硬盘…

error: failed to open index: Database already open. Cannot acquire lock报错解决办法

ordinals节点数据同步出现报错 error: failed to open index: Database already open. Cannot acquire lock.问题分析&#xff1a; 出现问题的原因是btcoin core节点数据没有同步完我们就开始进行ordinals数据同步&#xff0c;导致/root/.local/share/ord/index.redb 文件数据…

自然语言处理,基于预训练语言模型的方法,车万翔,引言部分

文章目录 自然语言处理应用任务1. 信息抽取2. 情感分析3. 问答系统4. 机器翻译5. 对话系统 自然语言处理应用任务 1. 信息抽取 信息抽取&#xff08;Information Extraction, IE&#xff09;&#xff0c;是从非结构化的文本中&#xff0c;抽取出结构化信息的过程&#xff0c;…

GBASE南大通用分享-ConnectionTimeout 属性

GBASE南大通用分享 获取或设置连接超时时间&#xff0c;值为‚0‛时没有限制。  语法 [Visual Basic] Public Overrides ReadOnly Property ConnectionTimeout As Integer Get [C#] public override int ConnectionTimeout { get; }  实现 IDbConnection.Connecti…

拼接url - 华为OD统一考试

OD统一考试 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定一个 url 前缀和 url 后缀, 通过 “,” 分割&#xff0c; 需要将其连接为一个完整的 url 。 如果前缀结尾和后缀开头都没有 /&#xff0c;需要自动补上 / 连接符&#xff1b; 如果前缀结…

LPC804开发(5.PWM使用)

1.前言 早上花了点时间学习了一下官方的例程&#xff0c;总的来说PWM输出还是比较简单的。 2.基本使用 首先在开始前我不建议大家照搬官方的程序&#xff0c;因为官方的程序非常复杂&#xff0c;其实官方自己有更简单的方法但是SDK里没有用&#xff0c;我也不知道为什么&…

Mybatis-Plus基础

typora-copy-images-to: img Mybatis Plus 今日目标&#xff1a; 了解mybatisplus的特点能够掌握mybatisplus快速入门能够掌握mybatisplus常用注解能够掌握mybatisplus常用的增删改查能够掌握mybatisplus自动代码生成 1 MybatisPlus简介 1.1 MybatisPlus概述 ​ MyBatis-…

python3 查询mysql(obmysql)数据库表格并写入txt文件

本文将介绍如何使用Python3查询MySQL&#xff08;OBmysql&#xff09;数据库读取表格数据写入本地txt文件。 具体步骤&#xff1a; 连接数据库创建游标对象执行SQL查询语句关闭连接 一、连接数据库 在查询数据库之前&#xff0c;我们首先需要连接到MySQL/OBmysql数据库。使用…

域名解析与nginx配置

dns解析 阿里云服务器dns域名解析配置&#xff0c;记录值就是阿里云服务器的ip nginx配置 远程到阿里云服务器上对nginx进行配置&#xff1a; nginx反向代理配置&#xff1a; 修改配置后&#xff0c;重启nginx服务 进入目录&#xff1a;cd /usr/sbin 强制杀死进程&#xff…

玩转WEB接口之三续篇【HTTPS证书申请 - nginx验证】

文章目录 一&#xff0c; 概述二&#xff0c;nginx下载三&#xff0c;访问域名1. 做域名映射2. 运行nginx并通过域名访问 四&#xff0c;配置SSL证书1. 配置证书文件2. nginx 添加证书文件 五、运行并验证1. 测试、重新加载2. https访问 一&#xff0c; 概述 接上篇 玩转WEB接…
最新文章