Golang学习笔记_RabbitMQ的原理架构和使用

RabbitMQ 简介

  1. 实现了高级消息队列协议(Advanced Message Queuing Protcol)AMQP
  2. 消息队列中间件的作用(Redis实现MQ里面有写过,这里简单带过)
    1. 解耦
    2. 削峰
    3. 异步处理
    4. 缓存
    5. 消息通信
    6. 提高扩展性

RabbitMQ 架构理解

channel
binding
channel
channel
channel
Producer 生产者
Exchange交换机
Queue消息队列
Consumer消费者
Consumer消费者
Consumer消费者
  1. binding(绑定):交换机将消息路由给Queue所遵循的规则,可以定义一个路由键,用于交换机筛选特定的Queue
    1. Routing_Key(路由键):Producer 和 Consumer 协商一致的 key 策略。主要在交换机的 direct(直连)和 topic(主题) 模式下使用,fanout(广播)模式下不使用Routing_Key
  2. Exchange(交换机):主要功能是分发消息给特定的Queue,只负责转发,不具备存储消息的功能。Exchange有以下四种模式:
    1. direct(直连模式),根据携带的Routing_Key来筛选特定的Queue进行消息投递。是RabbitMQ的默认类型,可以不指定Routing_Key,在创建时会默认生成与Queue重名。
    2. hander(头模式),使用场景不多,消息路由涉及多个属性的时候,交换机使用多属性来代替Routing_key建立路由规则,还可以定义匹配单词的个数,例如any为有一个单词满足条件就匹配成功。all为所有单词都满足条件才匹配成功。
    3. fanout(广播模式),不看Routing_Key。只根据Exchange和Queue的binding情况来分发信息。所有与之binding的queue都将接收到同一条消息。
    4. topic(主题模式),相当于模糊查询。topic的routing_key是使用 . 来进行隔断的。有两种匹配方法:
      1. " * " 匹配一个单词,例子如下
      2. " # " 匹配0个~多个单词,例子如下
rabbitMQ.* == rabbitMQ.topic != rabbitMQ.topic.topic
rabbitMQ.# == rabbit.topic == rabbit.topic.topic
  1. Queue(消息队列的存储数据结构):
    1. 存储方式:
      • 持久化,在Server本地硬盘存储一份
      • 临时队列,重启后丢失数据
      • 自动删除,不存在用户连接则删除queue
    2. 队列对ACK请求的不同情况
      • consumer 接收并 ack,queue 删除数据并向 consumer 发送新消息
      • consumer 接收但是未 ack 就断开了连接,queue 会认为消息并未传送成功,consumer 再次连接时会重新发送消息
      • 如果consumer 接收消息成功 ,但是忘记 ack 则 queue 不会重复发送消息
      • 如果 consumer 拒收消息,则 queue 会向另外满足条件的 consumer 继续发送这条消息

RabbitMQ 工作流程

Producer方向
  1. Producer 与 RabbitMQ Broker 建立连接,开启一个信道 channel
  2. 声明交换机并设置属性(交换机类型、持久化等)
  3. 声明Queue并设置属性(持久化,自动删除等)
  4. 通过Routing_key来binding交换机和Queue
  5. 发送信息给交换,交换机根据Routing_key来确认投递的queue
  6. 查找成功后将消息存到queue
  7. 查找失败将消息丢弃或抛回给生产者
  8. 关闭channel
Consumer方向
  1. 与 queue 建立连接,开启channel
  2. 向queue请求队列中的msg
  3. 等待queue回应,开始接收消息
  4. 消息处理完成后 返回回调确认ack
  5. queue 将确认的消息从队列中删除
  6. 关闭channel

RabbitMQ的两种部署方式

Meta Data : 元数据(描述数据的数据)

  • vhost meta data : 为Queue、Exchange、Binding提供命名空间级别的隔离
  • exchange meta data:记录路由的名称类型和属性
  • binding mate data:映射 routing_key和queue之间的绑定关系
  • queue mate data:表队列名称和属性
普通模式

对于该模式的两个节点,消息只会存在其中一个节点,另一个节点只保存mate data,当consumer 连接节点2访问节点1的数据信息时,消息会在两个节点中传递。
该模式下p和c应尽量连接每个节点,这样起到线性拓展的作用。
但存在一个问题,如果节点上还有未消费的消息,但是节点挂了。如果节点设置了持久化,则需要在节点重启的时候消息才会恢复。如果未设置持久化,则消息会丢失。

镜像模式

消息存在多个节点中,消息会在节点与节点之间同步,可实现高可用(当一个节点挂了,另一个节点可以接替其位置,继续工作)但会降低性能,因为大量消息进入和同步,会占用大量带宽,但是为了保证高可靠性需要取舍。

面试题

  • Q:如何保证消息不被重复消费?
    • A:MQ通过确认机制ACK,进行确认。确认后消息从queue中删除,保证消息不被重复消费的。如果因为网络原因ack没有成功发出,导致消息重新投递。可以使用全局唯一消息id来避免。
    1. 消息发送者发送消息时携带一个全局唯一的消息id
    2. 消费者监听到消息后,根据id在redis或者db中查询是否存在消费记录
    3. 如果没有消费就正常消费,消费完毕后,写入redis或者db
    4. 如果消息消费过则直接丢弃
  • Q:如何保证消息的消费顺序?
    • A:RabbitMQ中存在一个设置,叫独占队列。即在同一时间只有一个消费者会消费消息。从而制止了异步操作,保证消费顺序。或者一个Producer对一个Consumer
  • Q:如何保证数据一致性?
    • A:因为MQ的使用场景多为分布式系统,所以一般不追求强一致性。而保证最终一致性就可以。
    • 而保证数据最终一致性,可以采用消息补偿机制。即消息在消费者处理完之后调用生产者的API修改数据状态。如未调用API则判断为消息处理失败或出错。此时间隔一段时间后重新投递消息进行再次操作。
    • 消费者收到消息,处理完毕后,发送一条响应消息给生产者也是消息补偿机制,本意是确认消费者成功消费消息。ACK也是处理方法

RabbitMQ的使用(Golang使用amqp包)

代码部分参考 upup小亮的博客

代码只是简单的操作,主要是熟悉流程。对于如何创建Queue和绑定Exchange之类的操作有个了解。

Simple(简单收发模式,只有一个Queue)

Simple运行机制与WorkQueue相似,只是一个Consumer与多个Consumer的区别。多个Consumer之间存在竞争关系,所以工作队列是创建多个Consumer,多个竞争只有一个可以获取消息消费。消费成功后ack消息删除。
演示代码放到一起了:

WorkQueue 工作队列

生产者

// simple and work queue
func main2() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
		return
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
		return
	}
	// 通道关闭
	defer ch.Close()

	// 创建存储队列
	queue, err := ch.QueueDeclare(
		"hello", // 队列名称
		false, // 持久化设置,可以为true根据需求选择
		false, // 自动删除,没有用户连接删除queue一般不选用
		false, //独占
		false, //等待服务器确认
		nil)   //参数
	if err != nil {
		fmt.Println(err)
		log.Fatalf("无法声明队列:%s", err)
		return
	}

	var body string
	// 发送信息
	for i := 0; i < 10; i++ {
		fmt.Println(i)
		body = "Hello RabbitMQ" + string(i)
		err = ch.Publish(
			"",
			queue.Name,
			false, // 必须发送到消息队列
			false, // 不等待服务器确认
			amqp.Publishing{
				ContentType: "text/plain",
				Body:        []byte(body),
			})
		if err != nil {
			log.Fatalf("消息生产失败:%s", err)
			continue
		}
	}
}

消费者

	// create conn
	// 如果同时运行两个这样的consumer代码,就是工作队列。只有一个consumer就是simple
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
		return
	}
	defer conn.Close()

	// create channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
		return
	}
	defer ch.Close()
	// create queue
	queue, err := ch.QueueDeclare(
		"hello",
		false,
		false,
		false,
		false,
		nil)
	if err != nil {
		log.Fatalf("无法创建queue:%s", err)
		return
	}
	
	// 消费信息

	msgs, err := ch.Consume(
		queue.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	if err != nil {
		log.Fatalf("无法消费信息:%s", err)
		return
	}
	for msg := range msgs {
		log.Println(string(msg.Body))
	}
	return

pub/sub 发布订阅模式

发布订阅模式可以创建两个Queue,绑定到同一个Exchange中
生产者这边只需要跟交换机对接,而交换机类型为fanout

func main() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
	}
	defer ch.Close()

	// create exchange
	ex := ch.ExchangeDeclare(
		"exchange1", // 交换机名称
		"fanout",    // 交换机类型
		true,        // 是否持久化
		false,       // 是否自动删除
		false,       // 是否内部使用
		false,       // 是否等待服务器响应
		nil,         // 其他属性
	)
	fmt.Println(ex)

	body := "Hello RabbitMQ for Pub/Sub"
	err = ch.Publish(
		"exchange1",
		"", // routing key 可以为空,因为fanout不看routing key
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})
	if err != nil {
		log.Fatalf("err %s:", err)
	}
	log.Println(body)
}

消费者:创建交换机,类型为fanout,创建队列,绑定交换机(创建多个consumer绑定同一个queue和同一个交换机。这样发送一个消息,所有的consumer都能收到。== 发布订阅模型)

	// Pub/Sub
	// Create conn
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// channel create
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// exchange create
	ex := ch.ExchangeDeclare(
		"exchange1",
		"fanout",
		true,
		false,
		false,
		false,
		nil)

	fmt.Println(ex)

	// queue create
	queue, err := ch.QueueDeclare(
		"hello",
		false,
		false,
		false,
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}
	err = ch.QueueBind(
		queue.Name,
		"",
		"exchange1",
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}

	msgs, err := ch.Consume(
		queue.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}
	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages. To exit press CTRL+C")
	<-make(chan struct{}) // 阻塞主goroutine
}

Routing 模式(对特定的队列投递消息)

生产者

func main() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
	}
	defer ch.Close()

	// create exchange
	ex := ch.ExchangeDeclare(
		"exchange1", // 交换机名称
		"direct",    // 交换机类型
		true,        // 是否持久化
		false,       // 是否自动删除
		false,       // 是否内部使用
		false,       // 是否等待服务器响应
		nil,         // 其他属性
	)
	fmt.Println(ex)
	body := "Hello RabbitMQ for direct routing"
		// 发布消息到交换机,并指定路由键
	err = ch.Publish(
		"logs_direct", // 交换机名称
		"routing_key", // 路由键
		false,         // 是否等待服务器响应
		false,         // 是否立即将消息写入磁盘
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		},
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	log.Printf("Sent message: %s", message)

消费者

func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_direct", // 交换机名称
		"direct",      // 交换机类型
		true,          // 是否持久化
		false,         // 是否自动删除
		false,         // 是否内部使用
		false,         // 是否等待服务器响应
		nil,           // 其他属性
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}

	// 声明一个临时队列
	q, err := ch.QueueDeclare(
		"",    // 队列名称,留空表示由RabbitMQ自动生成,因为定义了key所以队列名可以是随意的,毕竟是依靠key来进行匹配的
		false, // 是否持久化
		false, // 是否自动删除(当没有任何消费者连接时)
		true,  // 是否排他队列(仅限于当前连接)
		false, // 是否等待服务器响应
		nil,   // 其他属性
	)
	// 将队列绑定到交换机上,并指定要接收的路由键
	err = ch.QueueBind(
		q.Name,        // 队列名称
		"routing_key",      // 路由键
		"logs_direct", // 交换机名称
		false,         // 是否等待服务器响应
		nil,           // 其他属性
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}

	// 订阅消息
	msgs, err := ch.Consume(
		q.Name, // 队列名称
		"",     // 消费者标识符,留空表示由RabbitMQ自动生成
		true,   // 是否自动应答
		false,  // 是否独占模式(仅限于当前连接)
		false,  // 是否等待服务器响应
		false,  // 其他属性
		nil,    // 其他属性
	)
	failOnError(err, "Failed to register a consumer")

	// 接收消息的goroutine
	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages. To exit press CTRL+C")
	<-make(chan struct{}) // 阻塞主goroutine

topic

func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换机名称
		"topic",      // 交换机类型
		true,         // 是否持久化
		false,        // 是否自动删除
		false,        // 是否内部使用
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 定义要发送的消息的路由键和内容
	routingKey := "example.key.das"
	message := "Hello, RabbitMQ!"

	// 发布消息到交换机,并指定路由键
	err = ch.Publish(
		"logs_topic", // 交换机名称
		routingKey,   // 路由键
		false,        // 是否等待服务器响应
		false,        // 是否立即发送
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(message),
		},
	)
	if err != nil{
		log.Fatalf(err)
	}

	log.Printf("Sent message: %s", message)
}

消费者

func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换机名称
		"topic",      // 交换机类型
		true,         // 是否持久化
		false,        // 是否自动删除
		false,        // 是否内部使用
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 声明一个临时队列
	q, err := ch.QueueDeclare(
		"",    // 队列名称,留空表示由RabbitMQ自动生成
		false, // 是否持久化
		false, // 是否自动删除(当没有任何消费者连接时)
		true,  // 是否排他队列(仅限于当前连接)
		false, // 是否等待服务器响应
		nil,   // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 将队列绑定到交换机上,并指定要接收的路由键
	err = ch.QueueBind(
		q.Name,       // 队列名称
		"example.#",  // 路由键,可以使用通配符*匹配一个单词
		"logs_topic", // 交换机名称
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}
	// 创建一个消费者通道
	msgs, err := ch.Consume(
		q.Name, // 队列名称
		"",     // 消费者标识符,留空表示由RabbitMQ自动生成
		true,   // 是否自动应答
		false,  // 是否排他消费者
		false,  // 是否阻塞
		false,  // 是否等待服务器响应
		nil,    // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}
	// 接收和处理消息
	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages...")
	// 阻塞
	<-forever
}

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

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

相关文章

基于开源IM即时通讯框架MobileIMSDK:RainbowChat v11.5版已发布

关于MobileIMSDK MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架&#xff0c;超轻量级、高度提炼&#xff0c;一套API优雅支持UDP 、TCP 、WebSocket 三种协议&#xff0c;支持iOS、Android、H5、小程序、Uniapp、标准Java平台&#xff0c;服务端基于Netty编写。 工…

09 SQL进阶 -- SQL高级处理 -- 窗口函数等

1. 窗口函数 1.1 窗口函数概念及基本的使用方法 窗口函数也称为 OLAP 函数。OLAP 是 OnLine AnalyticalProcessing 的简称,意思是对数据库数据进行实时分析处理。 为了便于理解,称之为窗口函数。常规的 SELECT 语句都是对整张表进行查询,而窗口函数可以让我们有选择的去某…

创建一个javascript公共方法的npm包,js-tool-big-box,发布到npm上,一劳永逸

前端javascript的公共方法太多了&#xff0c;时间日期的&#xff0c;数值的&#xff0c;字符串的&#xff0c;搞复制的&#xff0c;搞网络请求的&#xff0c;搞数据转换的&#xff0c;几乎就是每个新项目&#xff0c;有的拷一拷&#xff0c;没有的继续写&#xff0c;放个utils目…

每日一题---OJ题: 用队列实现栈

片头 嗨! 小伙伴们,大家好! 今天我们一起来看看这道OJ题---用队列实现栈,话不多说,我们马上开始~ emmm,题目看似有点长,我们一起来分析分析! 我们都知道,栈的特点是后进先出(Last In First Out,简称 LIFO ),队列的特点是先进先出(First In First Out 简称 FIFO),明明两者的性…

【记录】Python|Selenium 下载 PDF 不预览不弹窗(2024年)

版本&#xff1a; Chrome 124Python 12Selenium 4.19.0 版本与我有差异不要紧&#xff0c;只要别差异太大比如 Chrome 用 57 之前的版本了&#xff0c;就可以看本文。 如果你从前完全没使用过、没安装过Selenium&#xff0c;可以参考这篇博客《【记录】Python3&#xff5c;Sele…

求π的近似值(C语言)

一、N-S流程图&#xff1b; 二、运行结果&#xff1b; 三、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> # include <math.h>int main() {//初始化变量值&#xff1b;int symbol 1;double denominator 1.0, sum 0, term 1.0;//循…

Excel文件解析(Java)

一、概述 在应用程序的开发过程中&#xff0c;经常需要使用 Excel文件来进行数据的导入或导出。所以&#xff0c;在通过Java语言实现此类需求的时候&#xff0c;往往会面临着Excel文件的解析(导入&#xff09;或生成&#xff08;导出)。 在Java技术生态圈中&#xff0c…

JVM运行时内存溢出以及解决办法

JVM有哪些运行时数据区 JVM运行时数据区有程序计数器、本地方法栈虚拟机栈、堆、元数据区、直接内存。 其中只有程序计数器不是内存溢出&#xff0c;其他的都有可能会产生内存溢出。 栈内存溢出 当方法的调用深度过深&#xff0c;可能会导致栈内存溢出。 一般是发生在递归调…

Elasticsearch:如何将 MongoDB 数据引入 Elastic Cloud

作者&#xff1a;Hemendra Singh Lodhi Elastic Cloud 是由 Elastic 提供的基于云的托管服务。Elastic Cloud 允许客户在亚马逊网络服务 (AWS)、谷歌云平台 (GCP) 和微软 Azure 上部署、管理和扩展他们的 Elasticsearch 集群。 MongoDB 是一种流行的 NoSQL 文档导向数据库&am…

IDEA最好用插件推荐

1 背景 俗话说&#xff1a;“工欲善其事必先利其器”&#xff0c;本问介绍几款强大实用的 IDEA 插件&#xff0c;助力大家开发。 希望大家做一个聪明又努力的人&#xff0c;而不只是一个努力的人。 以下插件大都可以通过 IDEA 自带的插件管理中心安装&#xff0c;如果搜不到可以…

算法|最大堆、最小堆和堆排序的实现(JavaScript)

一些概念 堆&#xff1a;特殊的完全二叉树&#xff0c;具有特定性质的完全二叉树。大根堆&#xff1a;父节点 > 子节点小根堆&#xff1a;父节点 < 子节点 二叉堆也属于完全二叉树&#xff0c;所以可以用数组表示。 若下标从1开始&#xff0c;左节点为 2*i &#xff0…

类和对象-封装-设计案例1-立方体类

#include<bits/stdc.h> using namespace std; class Cube{public://设置长void setL(int l){m_Ll;} //获取长int getL(){return m_L;}//设置宽 void setW(int w){m_Ww;}//获取宽 int getW(){return m_W;}//设置高 void setH(int h){m_Hh;}//获取高int getH(){return m_H;…

【机器学习300问】72、神经网络的隐藏层数量和各层神经元节点数如何影响模型的表现?

评估深度学习的模型的性能依旧可以用偏差和方差来衡量。它们反映了模型在预测过程中与理想情况的偏离程度&#xff0c;以及模型对数据扰动的敏感性。我们简单回顾一下什么是模型的偏差和方差&#xff1f; 一、深度学习模型的偏差和方差 偏差&#xff1a;衡量模型预测结果的期望…

JAVAEE—UDP协议TCP协议/三次握手四次挥手

文章目录 UDP协议UDP协议的段格式UDP的传输过程校验和无连接 TCP协议TCP报文的格式段有连接可靠性确认应答超时重传如果ACK丢了呢&#xff1f; 序号和确认序号 连接的构建和断开连接的构建&#xff08;三次握手&#xff09;三次握手的作用为什么握手是三次&#xff0c;而不是四…

微信小程序的常用API ①

前言&#xff1a;什么是微信小程序的API&#xff1f; &#xff08;1&#xff09;微信小程序的API是由宿主环境提供的。通俗来说API是一种接口函数&#xff0c;把函数封装起来给开发者使用&#xff0c;这样好多功能都无需开发者去实现&#xff0c;直接调用即可。 &#xff08;…

工业电脑在ESOP工作站行业应用

ESOP工作站行业应用 项目背景 E-SOP是实现作业指导书电子化&#xff0c;并统一管理和集中控制的一套管理信息平台。信迈科技的ESOP终端是一款体积小巧功能齐全的高性价比工业电脑&#xff0c;上层通过网络与MES系统连接&#xff0c;下层连接显示器展示作业指导书。ESOP控制终…

Covalent Network(CQT)宣布推出面向 Cronos 生态的捐赠计划与 API 积分,为 Web3 创新赋能

为了促进 Web3 领域的创新&#xff0c;Covalent Network&#xff08;CQT&#xff09;宣布将其捐赠计划向 Cronos 生态系统中的开发者拓展。这一战略性举措&#xff0c;旨在通过向 Cronos 网络中基于 Covalent Network&#xff08;CQT&#xff09;API 构建的项目提供支持和资源&…

OpenHarmony实战开发-如何使用Navigation实现多设备适配。

介绍 在应用开发时&#xff0c;一个应用需要适配多终端的设备&#xff0c;使用Navigation的mode属性来实现一套代码&#xff0c;多终端适配。 效果图预览 使用说明 将程序运行在折叠屏手机或者平板上观看适配效果。 实现思路 本例涉及的关键特性和实现方案如下&#xff1a…

高版本Android studio 使用Markdown无法预览(已解决)

目录 概述 解决方法 概述 本人升级Android studio 当前版本为Android Studio Jellyfish | 2023.3.1 RC 2导致Markdown无法预览。 我尝试了很多网上的方法都无法Markdown解决预览问题&#xff0c;包括升级插件、安装各种和Markdown相关的插件及使用“Choose Boot Java Runtim…

Linux 操作系统编译器、静态库、动态库

1、编辑器 1.1、vim的安装 指令&#xff1a;sudo apt-get install vim 1.2 vim的使用 格式&#xff1a;vim 文件名 如果文件存在&#xff0c;只打开&#xff0c;文件不存在&#xff0c;创建并打开 vim的4中模式&#xff1a; 命令模式&#xff0c;插入模式&#xff0c;地行模…
最新文章