2024.2.21 模拟实现 RabbitMQ —— 实现转发规则

news2024/10/2 16:28:57

目录

需求分析

直接交换机(Direct )

主题交换机(Topic )

扇出交换机(Fanout )

Topic 交换机转发规则

routingKey 组成

bindingKey 组成

匹配规则

情况一

情况二

情况三

实现 Router 类

校验 bindingKey 和 routingKey

消息匹配机制

Topic 交换机匹配规则

针对 Router 单元测试


需求分析

直接交换机(Direct )

  • 通过设定 消息的 routingKey = 队列名,以此指定该消息需传给哪个队列

实例理解

  • 如下图所示,此时可直接无视绑定关系,直接从内存中拿到对应队列名的队列
  • 然后再将消息传给该队列即可


主题交换机(Topic )

  • 依据 Topic 交换机的转发规则,判定消息需传给哪些队列

扇出交换机(Fanout )

  • 给该交换机中所有绑定好的队列均传入消息

Topic 交换机转发规则

  • bindingKey:创建绑定时,给绑定指定的特殊字符串(相当于出题)
  • routingKey:发布消息时,给消息上指定的特殊字符串(相当于做答案)
  • 当 routingKey 的答案能够与 bindingKey 相对应时,便可将消息转发给该队列 

routingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 routingKey 分成多个部分

实例理解

  •  aaa.bbb.ccc    合法
  • aaa.110.bbb     合法
  • aaa                   合法

bindingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 bindingKey 分成多个部分
  3. 支持两种特殊的符号作为通配符( * 和 # 必须是作为被 . 分割出来的独立的部分)
  •  * ——> 可以匹配任何一个 独立的部分
  • # ——> 可以匹配任何 0 个或者多个独立的部分

实例理解

  • aaa.*.bbb       合法
  • aaa.*bb.ccc   非法

匹配规则

情况一
  • 当 bindingKey 中没有 * 或 # 这两个特殊符号时,必须要求 routingKey 和 bindingKey 一模一样,才能算匹配成功!

实例理解

  • bindingKey = aaa.bbb.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.bbb.ccd (匹配失败)

注意:

  • 情况一非常类似于 Direct 交换机的转发
  • 尤其是将 bindingKey 设置成和队列名字相同,此时就完全等价于 Direct 交换机了!

情况二
  • 当 bindingKey 中有特殊符号 * 时

实例理解 

  • bindingKey = aaa.*.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc(匹配失败)

情况三
  • 当 bindingKey 中有特殊符号 # 时

实例理解

  • bindingKey = aaa.#.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc (匹配成功)
  • routingKey = aaa.ccc(匹配成功)
  • routingKey = aaa.b.b.b(匹配失败)

问题:

  • 将交换机中每个队列的 bindingKey 设置成一个 # 时,会有啥效果呢?

回答:

  • 此时,该交换机中的 全部队列 都能匹配所有的 routingKey
  • 即该交换机就相当于 Fanout 交换机了!

注意点一:

  • Direct 交换机 和 Fanout 交换机,均属于 Topic 交换机的 特例

注意点二:

  • 上述规则都是 AMQP 协议所约定的!RabbitMQ 仅仅只是实现了该规则而已!

实现 Router 类

校验 bindingKey 和 routingKey

//    bindingKey 的构造规则:
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
//    3、允许使用 * 和 # 作为通配符,但是通配符只能作为独立的分段
    public boolean checkBindingKey(String bindingKey) {
        if(bindingKey.length() == 0) {
//            空字符串也是合法情况,比如在使用 direct / fanout 交换机的时候,bindingKey 是用不上的
//            因为我们在使用 direct 交换机时,是直接将 routingKey 作为 消息队列的名字,直接根据名字来进行匹配
//            在使用 fanout 交换机时,无需匹配,直接将该消息转给交换机中绑定的所有消息队列
            return true;
        }
//        检查字符串中不能存在非法字符
        for (int i = 0;i < bindingKey.length();i++) {
            char ch = bindingKey.charAt(i);
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
            if(ch >= '0' && ch <= '9') {
                continue;
            }
            if(ch == '_' || ch == '.' || ch == '*' || ch == '#') {
                continue;
            }
            return false;
        }
//        检查 * 或者 # 是否是独立的部分
//        aaa.*.bbb 合法情况; aaa.a*.bbb 非法情况
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
//            检查 word 长度 > 1 并且包含了 * 或者 #,就是非法的格式了
            if(word.length() > 1 && (word.contains("*") || word.contains("#")) ) {
                return false;
            }
        }
//        约定一下,通配符之间的相邻关系 (人为约定的)
//        为啥这么约定?因为前三种相邻的时候,实现匹配的逻辑会非常繁琐,同时功能性提升不大
//        1、 aaa.#.#.bbb 非法情况
//        2、 aaa.#.*.bbb 非法情况
//        3、 aaa.*.#.bbb 非法情况
//        4、 aaa.*.*.bbb 合法情况
        for (int i = 0;i < words.length;i++) {
//            连续两个 ##
            if(words[i].equals("#") && words[i+1].equals("#")) {
                return false;
            }
//            # *
            if(words[i].equals("#") && words[i+1].equals("*")) {
                return false;
            }
//            * #
            if(words[i].equals("*") && words[i+1].equals("#")) {
                return false;
            }
        }
        return true;
    }

//    routingKey 的构造规则
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
    public boolean checkRoutingKey(String routingKey) {
        if(routingKey.length() == 0) {
//            空字符串,合法的情况,比如在使用 fanout 交换机的时候,routingKey 用不上,就可以设定为 ""
            return true;
        }
        for (int i = 0;i<routingKey.length();i++) {
            char ch = routingKey.charAt(i);
//            判定该字符是否是大写字母
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
//            判定该字符是否是小写字母
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
//            判定该字符是否是阿拉伯数字
            if(ch >= '0' && ch <= '9') {
                continue;
            }
//            判定是否是 _ 或者 .
            if(ch == '_' || ch == '.') {
                continue;
            }
//            该字符,不是上述任何一种合法情况,就直接返回 false
            return false;
        }
//        把每个字符都检查过,没有遇到非法情况,此时直接返回 true
        return true;
    }

问题:

  • 观察下图,为啥 split 方法中的参数 "." 需要加两个反斜杠呢?

回答:

  • 首先 "." 在正则表达式中,是一个特殊的符号,此处是将 . 作为原始文本来进行匹配
  • 要想使用 . 的原始文本,就需要进行转义,即 在正则中使用 "\." 的方式来表示
  • 又因为在 Java 的字符串中,"\" 是一个特殊字符
  • 所以要想写入 "\." 这样的文本,又得在其前面加上一个反斜杠来进行转义,即 "\\."

消息匹配机制

//    这个方法用来判定该消息是否可以转发给这个绑定对应的队列
    public boolean route(ExchangeType exchangeType,Binding binding,Message message) throws MqException {
//        根据不同的 exhcangeType 使用不同的判定转发规则
        if(exchangeType == ExchangeType.FANOUT) {
//            如果是 fanout 类型,该交换机上绑定的所有队列都需要转发
            return true;
        }else if(exchangeType == ExchangeType.TOPIC) {
//            如果是 topic 主题交换机,规则就要更复杂一些
            return routeTopic(binding,message);
        }else {
//            其他情况是不应该存在的
            throw new MqException("[Router] 交换机类型非法! exchangeType = " + exchangeType);
        }
    }
  • 当为 Fanout 交换机时,无需匹配 bindingKey 和 routingKey,直接返回 true
  • 让该消息转发给所以绑定了 Fanout 交换机的队列
  • 当为 Topic 交换机时,则需要进行 bindingKey 和 routingKey 的匹配

Topic 交换机匹配规则

处理思路

  • 此处我们采用 双指针算法 进行匹配

  • 针对 bindingKey 的下标,判定当前下标指向部分的具体情况:
  1. 指向的是普通的字符串,此时要求和 routingKey 对应的下标指向的内容得完全一致!
  2. 指向的是 * ,此时无论  routingKey 这边指向的是啥,双方均同时下标前进
  3. 遇到了 # ,且 # 后面没有其他的内容了,直接返回 true,匹配成功!
  4. 遇到了 # ,但 # 后面还有其他的内容,拿着 # 后面的部分,去 routingKey 中查找,找到后面的部分,在 routingKey 中出现的位置,如果后面的部分,在 routingKey 中不存在,直接认为匹配失败,返回 false!如果后面的部分,在 routingKey 中存在,就将 routingKey 的箭头指向这个位置之后,然后继续往后匹配
  5. 两个箭头移动过程中,如果同时到达双方的末尾,则返回 true,如果一个箭头先到了末尾,另一个箭头还没到,则返回 false!

代码实现:

    private boolean routeTopic(Binding binding,Message message) {
//        先把这两个 key 进行切分
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");

//        引入两个下标,指向上述两个数字,初始情况下都为 0
        int bindingIndex = 0;
        int routingIndex = 0;
//        此处使用 while 更合适,每次循环,下标不一定就是 +1,不适合使用 for
        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if(bindingTokens[bindingIndex].equals("*")) {
//                【情况二】如果遇到 * 号直接进入下一轮,* 可以匹配到任意一个部分!
                bindingIndex++;
                routingIndex++;
                continue;
            }else if(bindingTokens[bindingIndex].equals("#")) {
//                如果遇到 # 先要看看有没有下一个位置
                bindingIndex++;
                if(bindingIndex == bindingTokens.length) {
//                    【情况三】该 # 后面没有东西了,说明此时一定能匹配成功了!
                    return true;
                }
//                # 【情况四】后面还有东西,拿着这个内容,去 routingKey 中往后找,找到对应的位置
//                findNextMatch 这个方法用来查找该部分在 routingKey 的位置,返回该下标,没找到,就返回 -1
                routingIndex = findNextMatch(routingTokens,routingIndex,bindingTokens[bindingIndex]);
                if(routingIndex == -1) {
//                    没找到匹配的结果,匹配失败
                    return false;
                }
//                找到匹配的情况,继续往后匹配
                bindingIndex++;
                routingIndex++;
            }else {
//                【情况一】如果遇到普通字符串,要求两边的内容是一样的
                if(!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }
//        【情况五】判断是否是双方同时到达末尾
//        比如 aaa.bbb.ccc 和 aaa.bbb 是要匹配失败的
        if(bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
            return true;
        }
        return false;
    }

    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if(routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }

针对 Router 单元测试

  • 编写测试用例代码是十分重要的!
package com.example.demo;

import com.example.demo.common.MqException;
import com.example.demo.mqserver.core.Binding;
import com.example.demo.mqserver.core.ExchangeType;
import com.example.demo.mqserver.core.Message;
import com.example.demo.mqserver.core.Router;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RouterTests {
    private Router router = new Router();
    private Binding binding = null;
    private Message message = null;

    @BeforeEach
    public void setUp() {
        binding = new Binding();
        message = new Message();
    }

    @AfterEach
    private void tearDown() {
        binding = null;
        message = null;
    }

//    【测试用例】
//    bindingKey               routingKey               result
//    aaa                      aaa                      true
//    aaa.bbb                  aaa.bbb                  true
//    aaa.bbb                  aaa.bbb.ccc              false
//    aaa.bbb                  aaa.ccc                  false
//    aaa.bbb.ccc              aaa.bbb.ccc              true
//    aaa.*                    aaa.bbb                  true
//    aaa.*.bbb                aaa.bbb.ccc              false
//    *.aaa.bbb                aaa.bbb                  false
//    #                        aaa.bbb.ccc              true
//    aaa.#                    aaa.bbb                  true
//    aaa.#                    aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.ccc                  true
//    aaa.#.ccc                aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.aaa.bbb.ccc          true
//    #.ccc                    ccc                      true
//    #.ccc                    aaa.bbb.ccc              true

    @Test
    public void test1() throws MqException {
        binding.setBindingKey("aaa");
        message.setRoutingKey("aaa");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test2() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test3() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test4() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test5() throws MqException {
        binding.setBindingKey("aaa.bbb.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test6() throws MqException {
        binding.setBindingKey("aaa.*");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test7() throws MqException {
        binding.setBindingKey("aaa.*.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test8() throws MqException {
        binding.setBindingKey("*.aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test9() throws MqException {
        binding.setBindingKey("#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test10() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test11() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test12() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test13() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test14() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test15() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test16() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }
}

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

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

相关文章

【AIGC大模型】跑通wonder3D (windows)

论文链接&#xff1a;https://arxiv.org/pdf/2310.15008.pdf windows10系统 显卡&#xff1a;NVIDIA rtx 2060 一、安装anaconda 二、安装CUDA 11.7 (CUDA Toolkit 11.7 Downloads | NVIDIA Developer) 和 cudnn 8.9.7(cuDNN Archive | NVIDIA Developer)库 CUDA选择自定…

【Android】坐标系

Android 系统中有两种坐标系&#xff0c;分别为 Android 坐标系和 View 坐标系。了解这两种坐标系能够帮助我们实现 View 的各种操作&#xff0c;比如我们要实现 View 的滑动&#xff0c;你连这个 View 的位置都不知道&#xff0c;那如何去操作呢&#xff1f; 一、Android 坐标…

【Spring Cloud】高并发带来的问题及常见容错方案

文章目录 高并发带来的问题编写代码修改配置压力测试修改配置&#xff0c;并启动软件添加线程组配置线程并发数添加Http取样配置取样&#xff0c;并启动测试访问message方法观察效果 服务雪崩效应常见容错方案常见的容错思路常见的容错组件 总结 欢迎来到阿Q社区 https://bbs.c…

《极简C++学习专栏》之结束语

朋友们&#xff0c;经过这么长的时间&#xff0c;《极简C学习专栏》的文章创作就要结束了&#xff0c;感谢你们一路陪伴&#xff01; 也希望你们能支持我接下来的其他专栏的创作&#xff01; 专栏的初衷 《极简C学习》专栏的初衷源自于我个人的学习笔记&#xff0c;记录下自己…

【刷题】牛客 JZ64 求1+2+3+...+n

刷题 题目描述思路一 &#xff08;暴力递归版&#xff09;思路二 &#xff08;妙用内存版&#xff09;思路三 &#xff08;快速乘法版&#xff09;思路四 &#xff08;构造巧解版&#xff09;Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读&#xff01;&#xff01…

Java 面向对象进阶 18 JDK8、9开始新增的方法;接口的应用;适配器设计模式;内部类(黑马)

一、JDK8开始新增的方法 默认方法不是抽象方法&#xff0c;所以不强制被重写&#xff1a; 但是如果被重写&#xff0c;就要去掉default关键字&#xff1a; public可以省略&#xff0c;但是default不可以省略&#xff1a; public是灰色的&#xff0c;代表可以省略 但是default是…

怎么把pdf转换成word?

怎么把pdf转换成word&#xff1f;Pdf和word在电脑上的使用非常广泛&#xff0c;pdf和word分别是由 Adobe和Microsoft 分别开发的电脑文件格式。PDF 文件可以在不同操作系统和设备上保持一致的显示效果&#xff0c;无论是在 Windows、Mac 还是移动设备上查看&#xff0c;都能保持…

使用Docker部署MinIO并结合内网穿透实现远程访问本地数据

文章目录 前言1. Docker 部署MinIO2. 本地访问MinIO3. Linux安装Cpolar4. 配置MinIO公网地址5. 远程访问MinIO管理界面6. 固定MinIO公网地址 前言 MinIO是一个开源的对象存储服务器&#xff0c;可以在各种环境中运行&#xff0c;例如本地、Docker容器、Kubernetes集群等。它兼…

《绝地求生》提示msvcp140.dll丢失如何修复?分享5种靠谱的解决方法

在玩绝地求生&#xff08;PUBG&#xff09;游戏过程中&#xff0c;如果遇到系统弹出“提示请重新安装软件msvcp140.dll”的信息&#xff0c;这究竟是什么原因导致的呢&#xff1f;msvcp140.dll这个文件是Microsoft Visual C Redistributable Package的一部分&#xff0c;是许多…

服务器系统安全,10招教你维护服务器的安全

网络逐渐成为了我们生活中一部分。有人说&#xff0c;断WIFI是最厉害的一种惩罚手段&#xff0c;但是其实不然&#xff0c;最狠的莫过于网站的服务器遭受攻击&#xff0c;直接访问不了网页了&#xff0c;这时候就算有wifi我们也无能为力。服务器系统安全一直是管理者最关注的事…

这个元宵节,被云开发者安排了

元宵节快乐&#xff0c;同学们&#xff01;今天吃的汤圆都是什么馅儿的&#xff1f; 都说过了元宵&#xff0c;这个年才算是正式过完&#xff0c;2024年就算是正式开启。学堂君这里也准备了一份专属于开发者的小礼物&#xff0c;作为一点心意。 欢迎私信&#xff0c;发送暗号…

Ubuntu 某软件导致卡机如何 kill 掉进程

输入 top 查看现在系统的进程&#xff0c;记下该进程第一列的 pid 编号 kill [pid] 可以杀掉此进程 参考&#xff1a; Ubuntu下查看进程pid及结束无响应程序_终止3分钟内无响应的所有pid-CSDN博客

【OneAPI】节假日查询API

OneAPI新接口发布&#xff1a;节假日查询API 可查询指定月份、年份法定节假日及调休情况。 API地址&#xff1a;https://oneapi.coderbox.cn/openapi/public/holiday 请求参数 URL参数 参数名类型必须含义说明datestring否要查询的日期可按年或月查询&#xff0c;支持前缀…

Coursera吴恩达机器学习专项课程02:Advanced Learning Algorithms 笔记 Week01

Advanced Learning Algorithms Week 01 笔者在2022年7月份取得这门课的证书&#xff0c;现在&#xff08;2024年2月25日&#xff09;才想起来将笔记发布到博客上。 Website: https://www.coursera.org/learn/advanced-learning-algorithms?specializationmachine-learning-in…

如何使用Lychee+cpolar搭建本地私人图床并实现远程访问存储图片

文章目录 1.前言2. Lychee网站搭建2.1. Lychee下载和安装2.2 Lychee网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 图床作为图片集中存放的服务网站&#xff0c;可以看做是云存储的一部分&#xff0c;既可…

嵌入式Qt 实现用户界面与业务逻辑分离

一.基本程序框架一般包含 二.框架的基本设计原则 三.用户界面与业务逻辑的交互 四.代码实现计算器用户界面与业务逻辑 ICalculator.h #ifndef _ICALCULATOR_H_ #define _ICALCULATOR_H_#include <QString>class ICalculator { public:virtual bool expression(const QSt…

AR应用的开发流程

增强现实&#xff08;Augmented Reality&#xff0c;AR&#xff09;是一种技术&#xff0c;它将虚拟信息叠加在真实世界中&#xff0c;通过计算机生成的视觉、听觉、触觉等感官反馈&#xff0c;将虚拟元素与现实世界进行交互。这种技术使得用户可以与现实世界中的虚拟对象进行互…

利用netty手写rpc框架

前言&#xff1a;利用netty异步事件驱动的网络通信模型&#xff0c;来实现rpc通信 一、大致目录结构&#xff1a; 二、两个端&#xff1a;服务端&#xff08;发布&#xff09;&#xff0c;客户端&#xff08;订阅消费&#xff09;&#xff0c;上代码&#xff1a; 1.服务端&am…

prometheus+grafana监控nginx的简单实现

1.编译安装NGINX 加入编译安装nginx-module-vts模块,目的是为了获取更多的监控数据(虚拟主机&#xff0c;upstream等) nginx下载 http://nginx.org/download/nginx-1.20.2.tar.gz nginx-module-vts下载 https://github.com/vozlt/nginx-module-vts/archive/refs/tags/v0.2…

手机打开 第三方 “微信、快手、QQ、电话、信息” 等

前期回顾 Vue3 TS Element-Plus —— 项目系统中封装表格搜索表单 十分钟写五个UI不在是问题_vue3 封装table 配置表格-CSDN博客https://blog.csdn.net/m0_57904695/article/details/135538630?spm1001.2014.3001.5501 目录 &#x1f916; 下载App如下图所示&#xff1a;…