【操作系统和计网从入门到深入】(六)进程间通信

news2024/11/24 20:10:07

前言

在这里插入图片描述
这个专栏其实是博主在复习操作系统和计算机网络时候的笔记,所以如果是博主比较熟悉的知识点,博主可能就直接跳过了,但是所有重要的知识点,在这个专栏里面都会提到!而且我也一定会保证这个专栏知识点的完整性,大家可以放心订阅~

复习六.进程间通信

1. 什么是进程间通信

进程运行具有独立性,想要进程间通信,难度是比较大的。

进程间通信的本质:交换数据,本质:让不同进程看到同一份资源(内存空间)

2. 初识管道

3. 进程间通信的一些标准

  1. Linux原生能提供的 — 管道
  2. SystemV – 内存共享、消息队列(不常用)、信号量(不讲,只说原理)
  3. Posix — 多线程 — 网络通信

4. 管道

管道:

  1. 分别以读/写的方式打开同一个文件
  2. fork()创建子进程
  3. 双方进程关闭自己不需要的文件描述符

管道就是文件!

但是,进程间通信我们要保证这是一个纯内存级别的通信方式,所以是不能落盘的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

int piped[2]这是一个输出型参数,期望通过调用pipe 得到被打开的文件fd 如果调用成功返回0,调用失败返回-1,而且错误码被设置。

写一份管道通信的代码。

mypipe:mypipe.cc
	g++ -o $@ $^ -std=c++11 # -DDEBUG
.PHONY:clean
clean:
	rm -f mypipe


#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>

void MakePipeCommunication()
{
    // 1. 创建管道
    int pipefd[2] = {0}; // pipefd[0]: 读端, pipefd[1]: 写端
    int n = pipe(pipefd);
    assert(n != -1); // 编译模式debug模式下assert才是有效的,release的时候是无效的
    (void)n;         // 表明n被使用过
#if DEBUG
    // 看看pipefd[]里面存的文件描述符都是些啥东西
    std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 3
    std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 4
#endif
    // 2. 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        // 子进程
        // 3. 构建单向通信的管道 -- 父进程写入,子进程读取
        // 3.1 关闭子进程不需要的fd
        close(pipefd[1]); // 把写端关闭
        // 通信
        char buffer[1024];
        // 写入的一端:如果fd没有关闭,读端如果有数据就读,如果没有数据,就等(阻塞)
        // 写入的一端:如果fd关闭了,读取的一端read会返回0,表示读到了文件的结尾!
        while (true)
        {
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                // 读取成功
                buffer[s] = 0;
                std::cout << "child get a mesg, pid: [" << getpid() << "] Father's mesg: " << buffer << std::endl;
            }
            else if (s == 0)
            {
                // read返回时0,说明写端fd关闭了
                std::cout << "writer(father) quit, I(child) quit too!" << std::endl;
                break;
            }
        }
        exit(0); // 子进程关闭
    }
    // 这里是父进程
    close(pipefd[0]); // 关闭读取端
    // 写入
    std::string mesg = "我是父进程,我正在给你发消息";
    int count = 0; // 记录父进程发送信息的条目数量
    char send_buffer[1024];
    while (true)
    {
        // 3.2 构建一个变化的字符串
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d", mesg.c_str(), getpid(), count);
        // 3.3 发送
        write(pipefd[1], send_buffer, strlen(send_buffer)); // 不需要+1
        count++;
        // 3.4 故意sleep一下
        sleep(1);
        if (count == 10)
        {
            std::cout << "writer(father/me) quit" << std::endl;
            break;
        }
    }
    close(pipefd[1]); // 推出之后关闭文件描述符
    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret > 0);
    (void)ret;
}

int main()
{
    MakePipeCommunication();
    return 0;
}

那此时就有人问了。如果我们定义全局缓冲区呢?是不是也可以得到相同的效果?

不可以的!

因为会有写时拷贝的原因。

父进程里面我们count++ 子进程是不知道的。

而我们通过管道实现的这种通信,是可以一直传输数据的,所以我们可以看到父进程发过来变化的字符串。

管道的一些特点

所以通过管道,我们可以构建一个进程池。当我们父进程接收到任务的时候,我就就指派给子进程去完成。

5. 利用管道写一个简单的进程池

进程池核心代码的简单实现

  • 本进程池通过父进程创建多个子进程,并通过管道派发任务的方式实现

  • 本代码只是进程池的建议实现,没有进行很好的封装,供学习使用

  • 本代码进程池所要执行任务的来源,来自于本地自己定义,本代码主要的学习目的是理解管道通信,读者可以通过添加接口的方式从其他地方获取任务,如网络

  • 本进程池采用的进程分配方式是:单机版随机分配的负载均衡

ProcessPool.hpp


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

/*
    现在我们写的这个进程池处理的任务是自己定义好的
    如果任务是从网络里面来的呢?

    代码中关于fd的处理,有一个小小的问题,不影响我们的使用,能找到吗?
*/

#define PROCESS_NUMBER 5

class ProcessPool
{
public:
    ProcessPool() = default;
    ProcessPool(const ProcessPool &) = delete;            // 不允许拷贝
    ProcessPool &operator=(const ProcessPool &) = delete; // 不允许拷贝
    ~ProcessPool() = default;

private:
    void __showUserOperMenu()
    {
        std::cout << "-------------------------------" << std::endl;
        std::cout << "-   1. show funcitons     -----" << std::endl;
        std::cout << "-   2. send funcitons(auto)  --" << std::endl; // 为了方便调试,让系统自己派发任务
        std::cout << "-------------------------------" << std::endl;
        std::cout << "please select>: " << std::endl;
    }
    int __waitCommand(int waitFd, bool &quit)
    {
        // 规定这个命令只能是4个字节
        uint32_t command = 0;
        ssize_t s = read(waitFd, &command, sizeof(command));
        if (s == 0)
        {
            quit = true;
            return -1;
        }
        assert(s == sizeof(uint32_t)); // 如果命令不是4个字节说明发命令发错了
        // 读成功了
        return command;
    }
    void __sendAndWakeUp(pid_t who, int fd, uint32_t command)
    {
        // who: 给哪个子进程发送命令
        // fd: 给这个子进程发送命令的管道的文件描述符
        // command: 发送什么命令
        write(fd, &command, sizeof(command));
        // 可以简单写一写日志
        std::cout << "call process: " << who << " execute: " << desc[command] << " through: " << fd << std::endl;
    }

public:
    void StartThePool()
    {
        load();
        // 因为我们需要在进程池里面选择一个进程去帮助我们完成任务
        // 因此我们需要一张表
        std::vector<std::pair<pid_t, int>> slots;
        // 创建多个子进程
        for (int i = 0; i < PROCESS_NUMBER; i++)
        {
            // create pipe
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            assert(n == 0);
            (void)n;

            // 创建子进程
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                // child 子进程是需要读取的
                close(pipefd[1]); // 关闭写端
                while (true)
                {
                    // pipefd[0];
                    // 现在子进程就要等待父进程的命令
                    bool quit = false;
                    int command = __waitCommand(pipefd[0], quit); // 如果对方不发命令,我们就阻塞
                    // 如果quit是true,写端推出,直接break
                    if (quit == true)
                        break;
                    // 执行对应的命令
                    if (command >= 0 && command < handlerSize()) // 说明这个命令是一个合法的命令
                    {
                        callbacks[command](); // 执行对应的方法
                    }
                    else
                    {
                        std::cout << "非法 command: " << command << std::endl;
                    }
                }
                exit(1);
            }
            // father, write 关闭读端
            close(pipefd[0]);
            slots.push_back(std::pair<pid_t, int>(id, pipefd[1]));
        }
        // 通过上面这个循环之后
        // 一个进程池已经创建好了
        // 开始任务
        // 父进程派发任务
        /*
            首先,我们在派发任务的时候,要均衡地给每一个子进程派发任务
            这个叫做单机版的负载均衡 -- 实现它的算法很多,比如rr,随机等
        */
        // 这里是父进程
        // 创建一个随机数种子
        srand((unsigned long)time(nullptr) ^ getpid() ^ 2324232);
        while (true)
        {
            int command = 0;
            int select = 0;
            __showUserOperMenu(); // 向用户展示菜单
            std::cin >> select;
            std::cout << std::endl;
            if (select == 1)
                showHandler();
            else if (select == 2)
            {
                // 用户要选择任务执行了
                // cout << "Enter Your Command> ";
                // 选择任务
                // cin >> command;
                while (true)
                {
                    // 选择任务(自动)
                    int command = rand() % handlerSize();
                    // 选择进程
                    int choice = rand() % slots.size();
                    // 布置任务,把任务给指定的进程
                    __sendAndWakeUp(slots[choice].first, slots[choice].second, command); // 发送命令,同时把子进程唤醒,让它去干活了
                    sleep(1);
                }
            }
        }
        // 关闭fd,结束所有的进程
        for (const auto &slot : slots)
        {
            close(slot.second); // 我们只需要关闭所有写的fd,所有的子进程都会退出
        }
        // 回收所有的子进程信息
        for (const auto &slot : slots)
        {
            waitpid(slot.first, nullptr, 0);
        }
    }
};

5. 管道进阶

5.1 管道的读写规则

其实O_NONBLOCK这个就是非阻塞的标志符。

  • 没有数据可以读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
    • O_NONBLOCK enable:read调用返回-1,errno的值设置为EAGAIN。
  • 当管道满的时候:
    • O_NONBLOCK disable:write调用阻塞,直到管道有位置写
    • O_NONBLOCK:调用返回-1,errno的值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
  • 当要写入的数据量不大于PIPE_BUF的时候,Linux将保证写入的原子性
  • 当要写入的数据量大于PIPE_BUF的时候,Linux不再保证写入的原子性

5.2 mkfifo

现在我们要把创建命名管道这个任务放到代码里面去。

第一个参数是要创建管道文件的路径,第二个参数是这个管道文件的权限。

准备好这些文件。

makefile

.PHONY:all
all:client server

client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f server client

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>

#define MODE 0666 // 定义管道文件的权限
#define SIZE 128
std::string ipcPath = "./fifo.ipc";
#endif

client.cc

#include "comm.hpp"

void WriteToIpc()
{
    // 1. 获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY); // 按照写的方式打开
    assert(fd >= 0);
    // 2. ipc过程
    // 把数据写到管道中去
    std::string buffer;
    while (true)
    {
        std::cout << "Please Enter the mesg: ";
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }
    // 3. 关闭
    close(fd);
}
int main()
{
    WriteToIpc();
    return 0;
}

server.cc

#include "comm.hpp"

void ReadFromIpc()
{
    // 1. 创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        // 创建管道文件失败
        perror("mkfifo");
        exit(-1);
    }
    // 2. 正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    assert(fd >= 0); // 文件打开失败
    // 3. 开始通信
    // 读文件
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer)); // 先把读取的缓冲区设置为0
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        // 最好不要让缓冲区写满,因为没有\0
        if (s > 0)
        {
            // 读到客户端的字符串了
            std::cout << "client say: " << buffer << std::endl;
        }
        else if (s == 0)
        {
            // 写端关闭了文件描述符
            std::cerr << "client quit, server quit too" << std::endl;
            break;
        }
        else
        {
            perror("read");
            break; // 读取有问题
        }
    }
    close(fd);
}

int main()
{
    ReadFromIpc();
    return 0;
}

执行效果如下。

其实,在这个基础上,我们还可以去创建子进程。

所以,管道通信不止可以和一个进程通信,管道可以和很多个进程进行通信。

6. SystemV框架

6.1 基本原理

共享内存的建立: 它属于那个进程呢? — 谁都不属于 它属于操作系统。

操作系统需不需要管理共享内存?肯定要,如何管理?先描述后组织!

共享内存 = 共享内存块 + 对应的共享内存的内核数据结构。

6.2 基本代码

这个key是多少?不重要,只需要在系统里面唯一即可!

comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <cassert>
#include "Log.hpp"

// 这些都可以随便写,只要保证这个路径是有可以访问的权限即可
#define PATH_NAME "/home/yufc"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 // 共享内存的大小,最好是页(PAGE: 4096)的整数倍

makefile

.PHONY:all
all:shmClient shmServer

shmClient:shmClient.cc
	g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmClient shmServer

shmServer.cc 先暂时这样写

#include "comm.hpp"

int main()
{
    // 这里可以生成一个唯一的k,在client里面也这样搞,也能得到这个k
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);
    Log("create key done!", Debug) << "client key: " << k << std::endl;
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);
    if (shmid == -1)
    {
        // 创建共享内存失败
        perror("shmget");
        exit(1);
    }
    return 0;
}

查看内存中的ipc资源:

ipcs -m # -m代表查看共享内存资源

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我想把它删了!

ipcrm -m 0 # 这个0是shmid

注意!systemV IPC资源,生命周期随内核,不是随进程!

当然我们也可以代码删除。

    // 最后删除共享资源
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << "shmid: " << shmid << std::endl;
    return 0;
}

当然,我们现在这样搞出来这个共享资源还是不行的。

创建的时候带上权限。

int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);

当然,创建好了就能用了吗?不能!

我们还需要把共享内存,映射到我们自己进程的地址空间里面,才能用!

这个步骤叫做attach!

  • 第一个参数不用说

  • 第二个参数:我们可以设置特定的虚拟地址去挂接(强烈不建议,除非有用途)

  • 第三个参数:挂接方式,设为0,默认帮我们去挂接就行

  • 返回值:

    • 成功,返回一个挂接的地址!

    • 这个接口,特别像以前学过的malloc!

先写一个监控脚本,等下好看效果。

while :; do ipcs -m; sleep 1; done
// 将制定的共享内存,attach到自己的地址空间上去
sleep(5);
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
sleep(5); // 我们会看到挂接数从0变成1

解除挂接 shdt

现在我们继续完善这个代码就能写出一个基于共享内存的通信方式的代码了。

6.3 共享内存通信基本代码

具体可以见github
在这里插入图片描述

现在这个通信框架的问题就是,通信是无法控制的,读不到Server也会一直打印空白。

7. 基于管道的-可以控制的SystemV通信框架

具体代码见github

在这里插入图片描述

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

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

相关文章

python算法与数据结构---单调栈与实践

单调栈 单调栈是一个栈&#xff0c;里面的元素的大小按照它们所在栈的位置&#xff0c;满足一定的单调性&#xff1b; 性质&#xff1a; 单调递减栈能找到左边第一个比当前元素大的元素&#xff1b;单调递增栈能找到左边第一个比当前元素小的元素&#xff1b; 应用场景 一般用…

19.云原生CICD之ArgoCD入门

云原生专栏大纲 文章目录 ArgoCDArgoCD 简介GitOps介绍Argo CD 的工作流程argocd和jinkens对比kustomize介绍ArgoCD和kustomize关系 安装argocdargocd控制台介绍首页应用创建表单SYNC OPTIONS&#xff08;同步选项&#xff09;SYNC POLICY&#xff08;同步策略&#xff09; 应…

视频异常检测论文笔记

看几篇中文的学习一下别人的思路 基于全局-局部自注意力网络的视频异常检测方法主要贡献&#xff1a;网络结构注意力模块结构&#xff1a; 融合自注意力和自编码器的视频异常检测主要贡献&#xff1a;网络结构Transformer模块动态图 融合门控自注意力机制的生成对抗网络视频异常…

Kafka框架详解

Kafka 1、Kafka介绍 ​ Kafka是最初由linkedin公司开发的&#xff0c;使用scala语言编写&#xff0c;kafka是一个分布式&#xff0c;分区的&#xff0c;多副本的&#xff0c;多订阅者的消息队列系统。 2、Kafka相比其他消息队列的优势 ​ 常见的消息队列&#xff1a;Rabbit…

【Docker篇】详细讲解容器相关命令

&#x1f38a;专栏【Docker】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 &#x1f384;欢迎并且感谢大家指出小吉的问题&#x1f970; 文章目录 &#x1f6f8;容器&#x1f339;相关命令&#x1f354;案例⭐创建并运…

大模型微调实战笔记

大模型三要素 1.算法&#xff1a;模型结构&#xff0c;训练方法 2.数据&#xff1a;数据和模型效果之间的关系&#xff0c;token分词方法 3.算力&#xff1a;英伟达GPU&#xff0c;模型量化 基于大模型对话的系统架构 基于Lora的模型训练最好用&#xff0c;成本低好上手 提…

Mysql流程控制函数

1概述 Mysql中的流程控制函数非常重要&#xff0c;可以根据不同的条件&#xff0c;执行不同的流程转换&#xff0c;可以在SQL语句中实现不同的条件选择。MySQL中的流程处理函数主要包括IF()、IFNULL()和CASE()函数。 1.1 IF函数 SELECT IF(1 > 0, 正确, 错误);1.2 IFNULL…

ROS第 12 课 Launch 启动文件的使用方法

文章目录 第 12 课 Launch 启动文件的使用方法1.本节前言2.Lanuch 文件基本语法2.2 参数设置2.3 重映射嵌套 3.实操练习 第 12 课 Launch 启动文件的使用方法 1.本节前言 我们在前面的教程里面通过命令行来尝试运行新的节点。但随着创建越来越复杂的机器人系统中&#xff0c;打…

idea运行卡顿优化方案

文章目录 前言一、调整配置1. idea.properties2. idea.vmoptions3.heap size4.Plugins5.Inspections 总结 前言 本人电脑16G内存&#xff0c;处理器i7 10代&#xff0c;磁盘空间也够用&#xff0c;整体配置够用&#xff0c;但运行idea会很卡&#xff0c;记录优化过程&#xff…

【JavaEE】文件操作与IO

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…

vue3+vite:封装Svg组件

前言 在项目开发过程中&#xff0c;以svg图片引入时&#xff0c;会遇到当hover态时图片颜色修改的场景&#xff0c;我们可能需要去引入另一张不同颜色的svg图片&#xff0c;或者用css方式修改&#xff0c;为了方便这种情况&#xff0c;需要封装svg组件来自定义宽高和颜色&…

IaC基础设施即代码:Terraform 进行 lifecycle 生命周期管理

目录 一、实验 1.环境 2.Terraform 创建网络资源 3.Terraform 进行 create_before_destroy&#xff08;销毁前创建新资源&#xff09; 4.Terraform 进行 prevent_destroy&#xff08;防止资源被销毁&#xff09; 5.Terraform 进行 ignore_changes&#xff08;忽略资源的差…

记录汇川:H5U与Factory IO测试15

主程序&#xff1a; 子程序&#xff1a; IO映射 子程序&#xff1a; 出料程序 子程序&#xff1a; 视觉判断 子程序&#xff1a; 自动程序 Factory IO配置&#xff1a; 实际动作如下&#xff1a; Factory IO测试15

【本科生机器学习】【北京航空航天大学】课题报告:支持向量机(Support Vector Machine, SVM)初步研究【上、原理部分】

说明&#xff1a; &#xff08;1&#xff09;、仅供个人学习使用&#xff1b; &#xff08;2&#xff09;、本科生学术水平有限&#xff0c;故不能保证全无科学性错误&#xff0c;本文仅作为该领域的学习参考。 一、课程总结 1、机器学习&#xff08;Machine Learning, ML&am…

【Docker】安装 Nacos容器并根据Nginx实现负载均衡

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Docker实战》。&#x1f3af;&#x1f3af; &…

Go使用记忆化搜索的套路【以20240121力扣每日一题为例】

题目 分析 这道题很明显记忆化搜索&#xff0c;用py很容易写出来 Python class Solution:def splitArray(self, nums: List[int], k: int) -> int:n len(nums)# 寻找分割子数组中和的最小的最大值s [0]for num in nums:s.append(s[-1] num)#print(s)cachedef dfs(cur,…

跟着pink老师前端入门教程-day07

去掉li前面的项目符号&#xff08;小圆点&#xff09; 语法&#xff1a;list-style: none; 十五、圆角边框 在CSS3中&#xff0c;新增了圆角边框样式&#xff0c;这样盒子就可以变成圆角 border-radius属性用于设置元素的外边框圆角 语法&#xff1a;border-radius:length…

1.11马原

同一性是事物存在和发展的前提&#xff0c;一方的发展以另一方的发展为条件 同一性使矛盾双方相互吸收有利于自身的因素&#xff0c;在相互作用中各自得到发展 是事物发展根本规律&#xff0c;唯物辩证法的实质和核心 揭示了事物普遍联系的根本内容和变化发展的内在动力 是贯…

Vue3 在 history 模式下通过 vite 打包部署白屏

Vue3 在 history 模式下通过 vite 打包部署后白屏; 起因 hash 模式 url 后面跟个 # 强迫症犯了改成了 history,就此一波拉锯战开始了 ... 期间 nigix 和 router 各种反复排查尝试最终一波三折后可算是成功了 ... Vue官方文档 具体配置可供参考如下: 先简要介绍下,当前项目打包…

SpringBoot整合Dubbo和Zookeeper分布式服务框架使用的入门项目实例

文章目录 SpringBoot整合Dubbo和Zookeeper分布式服务框架使用的入门项目实例Dubbo定义其核心部分包含: 工作原理为什么要用dubbo各个节点角色说明&#xff1a;调用关系说明&#xff1a; dubbo为什么需要和zookeeper结合使用&#xff0c;zookeeper在dubbo体系中起到什么作用&…