进程间通信-命名管道

这篇具有很好参考价值的文章主要介绍了进程间通信-命名管道。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

        先前已经了解了匿名管道,但是这是适用于有血缘关系的进程间,如果无血缘关系的进程要实现通信,此时需要有另一种通信方案-命名管道。为什么命名管道可以用于无血缘关系的进程间通信,什么是命名管道,为什么说它是有名字的,后面我们会一一了解。

一 函数介绍

进程间通信-命名管道,Linux,服务器,linux,运维

        参数1就是管道文件名,参数2则是管道文件的权限,返回0表示创建成功,返回-1表示创建失败。这个管道文件看似在磁盘之所以要用mkfifo打开一个管道文件,而不是直接open打开一个普通的文件,就是为了不让数据刷新到磁盘上,管道文件还是在内存,所以大小为0,因为大小是inode里的属性,而inode是用来描述磁盘文件的属性,但是磁盘并没有为文件开辟空间,所以大小为零。

进程间通信-命名管道,Linux,服务器,linux,运维

        此时echo 是写端,为什么会阻塞住,读端没打开,有意思的是匿名管道那里我曾说过如果读端关闭,写端打开后去写会直接出异常终止进程,但那是打开写端调用write函数后才被终止,而我们这里的命名管道是卡在open环节了,这里要等读写双方均打开后才会往后运行,所以只是阻塞而不是出异常。

        还有就是我先前在缓冲区博客中缓冲区介绍-CSDN博客曾提及,两个进程用不同的方式打开一个文件,此时系统会创建两个struct file结构体,但是只会有一个缓冲区,而且文件的属性等应该是只有一份的,为什么缓冲区只有一份呢?不会发生错乱吗,应该是会的,例如我还没读完,然后进程切换了,另一个进程就过来写,可能就会覆盖住要读的数据,但是linux为什么不限制呢,因为linux要给上层自由,要让我们自己来控制读写,自己处理缓冲区冲突问题。

二 实现通信

        先前匿名管道通信我们知道通信是要让不同的进程看到同一份资源,那命名管道又如何保证不同进程打开的是同一个文件呢,就是用路径+文件名,这个具有唯一性,显然这就是管道的名字,所以称该管道文件为命名管道,而非匿名管道,而且打开同一个文件就能看到这个文件唯一的缓冲区,也就是看到同一份资源。

        分client.cc和server.cc,log.hpp三个文件,其中log.hpp存头文件和创建管道mkfifo的相关参数,例如管道文件的文件名,给管道文件的初始化权限,这样两个进程都包含这个头文件就可以拿到管道名,后续打开这个文件时就看到了同一份资源,而server.cc则负责创建管道,管理管道文件,client.cc是客户端代码,不用创建管道文件,直接以写方式打开,然后发送指令就可以了。

1 创建管道

        这里是用类来封装创建管道,当Init 类创建对象,就会自动调用构造函数,然后把管道创建出来,如果要删除管道就要用unlink函数,显然可以放到析构函数处。

class Init
{
public:
    Init()
    {
        int ret = mkfifo(FILENAME, MODE);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(MKFIFO_ERR);
        }
    }
    ~Init()
    {
        unlink(FILENAME);
    }
};

错误分享:注意下面两个进程都是以读方式打开这个管道。

server.cc
int main()
{
    // 1 创建管道
    Init it;
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE); 
    
    //int fd = open(FILENAME, O_RDONLY);

    return 0;
}

client.cc
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);    
    
    return 0;
}

        有时候我发现如果我./client先运行,client起的进程不会在open被卡住,但是./server先运行两个可执行文件都会在open卡住,./server先运行会导致两个进程一起被卡住我勉强理解,因为双方当时都用写打开,命名管道没有读端,就阻塞住了,而且我再次测试的时候server.cc用读方式打开,此时不会阻塞住也证实要打开管道的读写端才能往后执行,至于普通文件在open处都不会被阻塞,随便你怎么打开。可是为什么./client运行起来又没事呢,其实这是因为./client运行时没创建管道,所以open直接出错返回了,但是./server先运行的时候是先创建的管道,再打开,此时管道没有读端就一直卡着。

2 开始读写

当我们有了读写端,就可以开始读写来简单实现通信了。

server进程负责读client进程发送的信息。

#include "log.hpp"
int main()
{
    // 1 创建管道
    Init it;

    // 2 打开管道
    int fd = open(FILENAME, O_RDONLY);
    
    while (true)
    {
        char buffer[1024] = {0};
        int n = read(fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n]=0;
            cout<<"server已经读到:"<<buffer<<endl;
        }
        else写端关闭,此时read返回0,break读端结束读取
        {
            break;
        }
    }
    return 0;
}

client负责向管道内发送消息。

#include"log.hpp"
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);
 
    //2 发送指令
    string s;
    cout<<"client准备写入:"<<s<<endl;
    while(getline(cin,s))
    {
        cout<<"client已经写入:"<<s<<endl;
        write(fd,s.c_str(),s.size());
    }
    close(fd);
    return 0;
}

三 日志代码实现

        log.hpp不仅让两个进程可以看到同一个文件名,还可以往这里添加一些日志代码,用于出错时输出提示信息。日志一般包括时间,事件等级,内容等,具体格式肯定是每个公司不一样的。不同的事件等级一般对应不同的处理方法,例如有normal(常规消息),warning(报警消息),error(出错消息),fatal(致命的),不处理就无法继续运行,debug(正常的调试信息),而warning和error都是运行出现了问题,可能需要立即处理,我们结合代码来理解。

1 logmessage介绍

进程间通信-命名管道,Linux,服务器,linux,运维

        日志函数,参数为,包含等级,输出格式和可变参数列表,c的可变参数使用和c++有点不同。

2 可变参数使用

进程间通信-命名管道,Linux,服务器,linux,运维

c语言中的可变参数使用如下

int sum(int n,...)
{
    va_list s;
    va_start(s,n);
    int sum = 0;
    while(n)
    {
        sum += va_arg(s,int);
        n--;
    }
    va_end(s);
    return sum;
}

        va_list s 就是定义一个变量s,s就是char*类型的,不过类型做了层层封装,看不出来,va_start就是&(n)+1然后赋值给s,此时s指向了可变参数的起始位置因为sum函数变量是要压栈的,而函数压栈是自右向左压栈,栈又是高地址到低地址生长的,所以sum函数中的n变量是最低地址,+1刚好指向可变模板参数部分,怪不得函数参数不能直接用可变参数...接收全部参数,必须要像sum函数一样有一个int n,就是因为设计者要我们有个具体的变量来取地址找到可变参数的起始位置。

        此后使用就是s根据va_start传的类型,依次往高地址找压栈的变量,由于va_start只能指定一种类型,所以参数必须是统一类型的,不然底层也不知道要跳过几个字节,所以为什么printf有个格式字符串呢,就是为了知道可变参数时每个参数占字节数。显然va_arg就是宏,因为int是个类型,而语法规定我们不可给函数传个类型。

进程间通信-命名管道,Linux,服务器,linux,运维

3 logmessage函数细节

        当知道了可变参数的使用,后面就是真正介绍函数内部实现。

void logmessage(const char* level, const char *format, ...)
{
    char leftbuffer[SIZE];

    时间获取
    time_t t = time(nullptr);
    struct tm * ltime = localtime(&t);

    //默认部分 包含事件等级和时间
    snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",level,
    ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);
    
    //可变部分
    char rightbuffer[SIZE];
    va_list s;
    va_start(s,format);
    vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
    
    合并输出两个部分
    printf("%s %s\n",leftbuffer,rightbuffer);
}

获取时间,localtime函数,传的参数是用time函数的返回值。进程间通信-命名管道,Linux,服务器,linux,运维

然后localtime会返回一个结构体指针,这个指针内部成员如下。

进程间通信-命名管道,Linux,服务器,linux,运维

        我们直接访问成员打印年月日就好了,就是年份要加上1900,因为那一年计算机刚刚诞生,显然这个时间戳也是从这个时候开始计数。

进程间通信-命名管道,Linux,服务器,linux,运维

        当日志时间和日志等级都有了后,我们就可以考虑输出日志信息了,日志格式:默认部分+可变部分,默认部分就是日志时间和等级,显然日志时间直接打印即可。日志等级也可以传整型,就是要用下面的函数转字符串,不然你打印个1,2,3,4,谁知道对应什么等级。

进程间通信-命名管道,Linux,服务器,linux,运维

        可变部分就是日志内容了,可变内容是用可变参数传入的,打印格式在format中,刚好vsnprintf函数就是用于处理可变参数,而且都无需我们解析可变参数了。

进程间通信-命名管道,Linux,服务器,linux,运维

进程间通信-命名管道,Linux,服务器,linux,运维

最后大致使用如下图。

进程间通信-命名管道,Linux,服务器,linux,运维

进程间通信-命名管道,Linux,服务器,linux,运维

4 日志往文件输出

        上面代码里最后是直接printf出来的,如果我们可以选择将日志信息往文件打印或者往屏幕打印,此时这个日志输出就更加灵活,且符合实际了。首先我们把之前了解的logmessage函数封装到类内,这样封装的比较美观,不然代码多了,一堆函数这怎么找,直接弄到一个类内多好。

#define FILENAME "myfifo"
#define Logname "log.txt"
enum PMethod
{
    Screen = 1,//输出到屏幕
    OneFile ,//输出到一个文件上
    ClassFile//分类输出到多个文件中
};
enum ErrorLevel
{
    Info = 1,
    Warning,
    Fatal,
    Debug
};
class Log
{
public:
    Log(int method = Screen)//用该成员变量记录输出目标地点,默认是到屏幕
    :printmethod(method)
    {
        ;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
            case Info:
                return "Info";
            case Warning:
                return "Warning";
            case Fatal:
                return "Fatal";
            case Debug:
                return "Debug";
            default: 
                return "None";    
        }
    }
    //日志信息
    void logmessage(int level, const char *format, ...)
    {
        char leftbuffer[SIZE];
        time_t t = time(nullptr);
        struct tm * ltime = localtime(&t);
        //默认部分 事件等级和时间
        snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),
        ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);
    
        //可变部分
        char rightbuffer[SIZE];
        va_list s;
        va_start(s,format);
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
        char Logbuffer[SIZE*2];
        snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);
        LogPrint(level,Logbuffer);
    }

    调用LogPrint函数,输出buffer信息。

    void LogPrint(int level , string lbuffer)
    {
        switch(printmethod)    由printmethod选择输出到文件还是输出到屏幕
        {
            case Screen://输出到屏幕
               cout<<lbuffer<<endl;
                break;
            case OneFile: //输出到一个文件上
                PrintOnefile(Logname,lbuffer);
                break;
            case ClassFile://将错误信息按等级分流到不同的文件
                PrintClassFile(level,lbuffer);
                break;
        }
    }
    
    void PrintOnefile(const char *filename,string& lbuffer)
    {
        lbuffer+='\n';
        int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);
        if(fd < 0)
            return;
         write(fd,lbuffer.c_str(),lbuffer.size());   
    }
    void PrintClassFile(int level,string& lbuffer)
    {
        string filename = Logname;
        filename += ".";
      
    //log.txt.Info log.txt.Warning log.txt.Fatal

        filename += leveltostring(level);//这样文件名就会以事故等级的方式区分开来
        PrintOnefile(filename.c_str(),lbuffer);
       然后直接复用先前的函数
    }
   
private:
    int printmethod;
};

四 进程池实现

        先前是用了匿名管道来实现进程池,现在学了命名管道,就要试试用命名管道来给进程池做一些添加。这里有server.cc模拟服务端进程,client.cc模拟客户端进程,当客户端发指令时,服务端会创建子进程去执行,但是由于来一个指令,才创建子进程,影响响应时间,所以设计出了进程池,服务端提前创建子进程,有任务来就给子进程发指令去执行,而父子进程间的通信我们用的是匿名管道,用命名管道反而有点麻烦。

1 server.cc

#include "log.hpp"
void slaver()
{
    //创建日志
    Log log;
    cout<<"子进程准备读取"<<endl;
     while (true)
    {
        char buffer[1024] = {0};
        int n = read(0, buffer, sizeof(buffer));
        if(n > 0)
        {
            cout<<"子进程server已经读到:"<<buffer<<" 立刻执行"<<endl;
        }
        else
        {
            log.logmessage(Debug,"子进程读取结束 退出信息:%s 退出码:%d",strerror(errno),errno);
            break;
        }
    }
}
void InitChannels(vector<channel>& channels)
{
    vector<int> ProcessPid;
    for(int i = 0; i < PRONUM; i++)
    {
        创建匿名管道
        
        int fd[2]={0};
        int ret = pipe(fd);
        int id = fork();
        if(id < 0)
        {
            perror("fork");
            exit(FORK_ERR);
        }
        else if(id == 0)  子进程读
        {
            for(auto e : ProcessPid)  关闭残留写端,在匿名管道曾详细解释过
                close(e);
            close(fd[1]);
            dup2(fd[0],0);
            slaver();
            exit(0);
        }

        父进程负责写

        close(fd[0]);
        string proname = "process->";
        proname += to_string(i);
        channels.push_back(channel(fd[1],id,proname));
        ProcessPid.push_back(fd[1]);
    }
}

void Ctrlprocess(const vector<channel>& channels)
{
    Init it;  创建命名管道
    Log log;
    打开命名管道
    int fd = open(FILENAME,O_CREAT|O_APPEND|O_RDONLY,0666);
    int which = 0;
    从客户端读取信息

     while (true)
    {
        char buffer[1024] = {0};
        int n = read(fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            cout<<"父进程server已经读到:"<<buffer<<" 正在让子进程处理"<<endl;
            //轮转挑选子进程
            int fd = channels[which++]._fd;
            write(fd,buffer,sizeof(buffer));
            which%=channels.size();
        }
        else
        {
            log.logmessage(Debug,"父进程读取结束 退出信息:%s 退出码:%d",strerror(errno),errno);
            break;
        }
    }
}
void QuitProcess(const vector<channel>& channels)回收子进程
{
    for(auto e : channels)
    {
        close(e._fd);
        int status = 0;
        waitpid(e._pid,&status,0);
    }
}
int main()
{

    // 1 初始化channels
    vector<channel> channels;
    InitChannels(channels);
 
    //2 控制子进程
    Ctrlprocess(channels);

    //3 回收子进程
    QuitProcess(channels);
    return 0;
}

2 client.cc

#include"log.hpp"
int main()
{
     //1 打开管道
    int fd = open(FILENAME, O_APPEND | O_WRONLY, MODE);
    //2 发送指令
    string s;
    cout<<"client准备写入:"<<s<<endl;
    while(getline(cin,s))
    {
        cout<<"client已经写入:"<<s<<endl;
        write(fd,s.c_str(),s.size());
    }
    close(fd);
    return 0;
}

3 log.hpp

下面代码在上面日志代码实现中已经解释了。文章来源地址https://www.toymoban.com/news/detail-808088.html

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include<time.h>
#include<errno.h>
#include <cstdarg>
#include<vector>
#include <sys/wait.h>
#define FILENAME "myfifo"
#define Logname "log.txt"
#define MODE 0666
#define SIZE 1024
#define INFO "Info"
#define WARNING "Warning"
#define  FATAL "Fatal"
#define  DEBUG "Debug"
#define PRONUM 2
using namespace std;
enum Fail
{
    MKFIFO_ERR = 1,
    OPEN_ERR,
    FORK_ERR
};

//错误等级
enum ErrorLevel
{
    Info = 1,
    Warning,
    Fatal,
    Debug
};
//接收输出的文件
enum PMethod
{
    Screen = 1,//输出到屏幕
    OneFile ,//输出到一个文件上
    ClassFile//分类输出到多个文件中
};
class channel
{
public:
    channel(int fd,int pid,string& proname)
    :_fd(fd)
    ,_pid(pid)
    ,_proname(proname)
    {
        ;
    }
    int _fd;//存管道fd
    int _pid;//存子进程pid
    string _proname;//进程名
};
// 实现自动创建和销毁管道文件
class Init
{
public:
    Init()
    {
        int ret = mkfifo(FILENAME, MODE);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(MKFIFO_ERR);
        }
    }
    ~Init()
    {
        unlink(FILENAME);
    }
};
class Log
{
public:
    Log(int method = Screen)
    :printmethod(method)
    {
        ;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
            case Info:
                return "Info";
            case Warning:
                return "Warning";
            case Fatal:
                return "Fatal";
            case Debug:
                return "Debug";
            default: 
                return "None";    
        }
    }
    //日志信息
    void logmessage(int level, const char *format, ...)
    {
        char leftbuffer[SIZE];
        time_t t = time(nullptr);
        struct tm * ltime = localtime(&t);
        //默认部分 事件等级和时间
        snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),
        ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);

        //可变部分
        char rightbuffer[SIZE];
        va_list s;
        va_start(s,format);
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
        char Logbuffer[SIZE*2];
        snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);
        LogPrint(level,Logbuffer);
    }
    void PrintOnefile( const char *filename,string& lbuffer)
    {
        lbuffer+='\n';
        int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);
        if(fd < 0)
            return;
         write(fd,lbuffer.c_str(),lbuffer.size());   
    }
    void PrintClassFile(int level,string& lbuffer)
    {
        string filename = Logname;//将不同错误信息分流到对应的文件
        filename += ".";
        filename += leveltostring(level);
        PrintOnefile(filename.c_str(),lbuffer);
    }
    void LogPrint(int level,string lbuffer)
    {
        switch(printmethod)
        {
            case Screen://输出到屏幕
               cout<<lbuffer<<endl;
                break;
            case OneFile: //输出到一个文件上
                PrintOnefile(Logname,lbuffer);
                break;
            case ClassFile:
                PrintClassFile(level,lbuffer);
                break;
        }
    }
private:
    int printmethod;
};

到了这里,关于进程间通信-命名管道的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】进程间通信(匿名管道 & 命名管道)-- 详解

    如何理解进程间通信? 进程具有独立性,所以进程想要通信难度是比较大的,成本高。 在日常生活中,通信的本质是传递信息,但站在程序员角度来看, 进程间通信的本质:让不同的进程看到同一份资源(内存空间) 。 进程间通信就是进程之间互相传递数据,那么进程间

    2024年04月28日
    浏览(37)
  • 【Linux】进程间通信 -- 命名管道 | mkfifo调用

    client.cpp : server.cpp : 然后创建 Makefile 使得我们更方便的去编译: 这样我们使用一条指令编译两个文件: 我们可以发现我们创建的 named_pipe 是以 p 开头而且有自己独立的 inode ,说明它是一个 独立的管道文件 我们执行下面脚本,主要的功能就是使用 echo 循环输出 hello world! 到管

    2024年02月13日
    浏览(29)
  • Linux通信--构建进程通信的 方案之管道(下)|使用匿名管道实现功能解耦|命名管道实现serve&client通信

    文章目录 一、管道的应用实例-父进程唤醒子进程,子进程执行某种任务 二、命名管道 1.创建一个命名管道 2.匿名管道与命名管道的区别 3.命名管道的打开规则 4.用命名管道实现serverclient通信 后续将源码上传到gitee,上传后修改链接。 管道应用的一个限制就是只能具有共同祖

    2024年02月10日
    浏览(33)
  • 【Linux从入门到精通】通信 | 管道通信(匿名管道 & 命名管道)

        本派你文章主要是对进程通信进行详解。主要内容是介绍 为什么通信、怎么进行通信。其中本篇文章主要讲解的是管道通信。希望本篇文章会对你有所帮助。 文章目录 一、进程通信简单介绍 1、1 什么是进程通信 1、2 为什么要进行通信  1、3 进程通信的方式 二、匿名管

    2024年02月09日
    浏览(35)
  • 【Linux】匿名管道与命名管道,进程池的简易实现

    本质是先让不同的进程看到同一份资源,也就是两个进程都能对管道文件的缓冲区进行操作 这里我们pipe的时候,会使用两个文件描述符,这两个文件描述里面存的file结构体是同一个,也就是管道文件的file结构体,file结构体中存储有inode以及系统缓冲区,此时fork一个子进程

    2024年02月05日
    浏览(31)
  • 【Linux后端服务器开发】管道设计

    目录 一、管道通信 二、匿名管道 1. 匿名管道通信 2. 匿名管道设计 三、命名管道 comm.hpp client.cc serve.cc 进程通信 数据传输:一个进程需要将它的数据发送给另一个进程 资源共享:多个进程之间共享同样的资源 通知事件:一个进程向另一个(一组)进程发送信息,通知它们发

    2024年02月13日
    浏览(47)
  • 进程间通信(命名管道)

    目录:            1.命名管道            2.创建命名管道 --------------------------------------------------------------------------------------------------------------------------------- 1.命名管道 1.管道的一个应用限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信 2.如果我们想在不相

    2024年02月06日
    浏览(26)
  • 进程间通信-命名管道

            先前已经了解了匿名管道,但是这是适用于有血缘关系的进程间,如果无血缘关系的进程要实现通信, 此时需要有另一种通信方案-命名管道。为什么命名管道可以用于无血缘关系的进程间通信,什么是命名管道,为什么说它是有名字的,后面我们会一一了解。

    2024年01月20日
    浏览(25)
  • 进程间通信之利用命名管道进行通信

    命名管道(Named Pipe),也被称为FIFO(First In, First Out),是一种在Unix和Unix-like操作系统中用于进程间通信的特殊文件类型。它允许不相关的进程通过文件系统中的路径名进行通信。 命名管道(Named Pipe)是一种在Unix和Unix-like系统中用于进程间通信的特殊文件类型。它的作用主

    2024年01月19日
    浏览(25)
  • 【Linux】进程通信之管道通信详解

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 其实管道通信是Unix中最古老的进程间通信的形式了: 管道通信是一种进程间通信的方式,它可以让一个进程的输出作为另一个进程的输入,实现数据的传输、资源的共享、事件的通知和进程的控制。 管道通信分为两种类型:匿名

    2023年04月19日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包