.Net RabbitMQ(消息队列)

news2024/11/25 0:57:20

文章目录

  • 一.RabbitMQ 介绍以及工作模式
    • 1.RabbitMQ的介绍:
    • 2.RabbitMQ的工作模式:
  • 二.RabbitMQ安装
    • 1.安装Erlang语言环境
    • 2.安装RabbitMQ
  • 三.在.Net中使用RabbitMQ
    • 1.HelloWorld模式
    • 2.工作队列模式
    • 3.发布订阅模式
    • 4.Routing路由模式和Topics通配符模式


一.RabbitMQ 介绍以及工作模式

1.RabbitMQ的介绍:

RabbitMQ 是一个开源的消息代理软件,它实现了高级消息队列协议(AMQP)的标准,同时也支持其他消息协议,如 MQTT。它最初由 Rabbit Technologies 公司开发,后来成为了 Pivotal Software 的一部分,现在则是由 VMware 管理。

以下是 RabbitMQ 的一些关键特点和功能:

消息队列: RabbitMQ 允许应用程序之间通过发送和接收消息进行通信。它作为一个中间件,负责接收、存储和路由消息,以确保消息能够可靠地传递到目标应用程序。

灵活的消息路由: RabbitMQ 支持不同类型的交换机(exchange),如 direct、topic、fanout 和 headers,每种类型的交换机都有不同的消息路由方式,使得消息的分发更加灵活高效。

持久化: RabbitMQ 支持将消息持久化到磁盘,以确保即使在代理重启后,消息也不会丢失。它还允许将队列和交换机设置为持久化,以提高系统的可靠性。

集群和高可用性: RabbitMQ 支持构建具有高可用性和可伸缩性的集群。通过将多个 RabbitMQ 节点组成集群,可以实现消息的负载均衡和故障转移,从而提高系统的可靠性和性能。

消息确认机制: RabbitMQ 支持消息确认机制,即生产者可以选择在消息发送到队列后等待消费者对消息的确认,以确保消息被成功处理。这种机制可以保证消息不会丢失,并且可以防止消息重复处理。

插件系统: RabbitMQ 提供了丰富的插件系统,可以通过插件扩展其功能,如集成其他消息协议、实现认证和授权、监控和管理等。

多语言客户端: RabbitMQ 提供了多种编程语言的客户端库,如 Java、Python、Ruby、C# 等,使得开发者可以方便地与 RabbitMQ 进行交互。

总的来说,RabbitMQ 是一个功能强大、可靠性高的消息代理软件,广泛应用于各种分布式系统和应用场景,如微服务架构、实时数据处理、任务调度等。

2.RabbitMQ的工作模式:

RabbitMQ 的工作模式主要涉及以下几个关键组件:生产者、交换机(Exchange)、队列(Queue)和消费者。通过这些组件,RabbitMQ 实现了消息的传输和路由。

生产者(Producer): 生产者是消息的发送者,它负责将消息发送到 RabbitMQ 中。生产者通常将消息发送到一个特定的交换机,然后由交换机将消息路由到一个或多个队列中。

交换机(Exchange): 交换机是消息的路由中心,负责接收从生产者发送的消息,并根据指定的路由规则将消息路由到一个或多个队列中。RabbitMQ 提供了不同类型的交换机,如 direct、topic、fanout 和 headers,每种类型的交换机都有不同的路由规则。

队列(Queue): 队列是消息的存储容器,它负责存储从交换机接收到的消息,以便消费者可以按顺序处理消息。每个队列都有一个唯一的名称,并且可以有多个消费者订阅同一个队列。

消费者(Consumer): 消费者是消息的接收者,它负责从队列中获取消息并进行处理。消费者通常会订阅一个或多个队列,并在收到消息后执行相应的业务逻辑。

RabbitMQ 的工作流程可以简单描述为:

生产者将消息发送到一个特定的交换机。
交换机根据指定的路由规则将消息路由到一个或多个队列中。
消费者从队列中获取消息并进行处理。
在这个过程中,RabbitMQ 提供了丰富的路由和消息确认机制,以确保消息能够可靠地传递和处理。

二.RabbitMQ安装

bilibili三分钟RabbitMQ安装教程,亲测有效

1.安装Erlang语言环境

a.RabbitMQ是Erlang语言开发的,所以我们首先去Erlang官网下载Erlang环境

在这里插入图片描述

b.下载完成后,我们直接根据默认的情况安装到电脑上,然后进入windows的“高级系统设置”,设置Erlang环境变量

给他一个变量名称,然后浏览刚才安装的Erlang目录位置,获取变量值

在这里插入图片描述

c.在环境变量的path中,配置刚才配置好的Erlang环境

在这里插入图片描述

2.安装RabbitMQ

a.接着我们进入RabbitMQ官网下载RabbitMQ

在这里插入图片描述
b.RabbitMQ下载安装完毕后,我们进入RabbitMQ的sbin目录中,用cmd终端行打开命令行,分别执行以下命令安装RabbitMQ Cli

a.在cmd中,进入RabbitMQ的sbin目录下,输入 
rabbitmq-plugins enable rabbitmq_management 
命令安装RabbitMQ Ctl

b.输入 
rabbitmqctl status 
命令检验是否安装成功

c.进入服务中,先停止RabbitMQ的服务,然后在RabbitMQ的sbin目录中,启动RabbitMQ.Server.bat文件,最后再重启RabbitMQ服务,就可以进入浏览器访问http://localhost:15672/进入RabbitMQ服务端了

1)停止rabbitMQ服务

在这里插入图片描述

2)启动rabbitmq.server.bat文件

在这里插入图片描述

3)重启RabbitMQ服务后,使用浏览器进入http://localhost:15672/
斜体样式
默认的账户名密码都是:guest
在这里插入图片描述

三.在.Net中使用RabbitMQ

bilibili某博主详细解说:RabbitMQ从零到高可用集群

1.HelloWorld模式

HelloWorld模式通常被视为RabbitMQ的点对点模式,生产者将消息放入队列中,消费者直接从队列中读取

a.生产者代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        //创建队列,声明并创建一个队列,如果队列已存在,则使用这个队列
        //第一个参数:队列名称ID
        //第二个参数:是否持久化,false对应不持久化数据,MQ停掉数据就会丢失
        //第三个参数,是否队列私有化,flase达标所有的消费则会都可以访问,true代表只有第一次拥有它的消费者才能一直使用
        //第四个参数:是否自动删除,false代表链接停掉后不自动删除这个队列

        channel.QueueDeclare("hello",true,false,false,null);
        string message = $"Hellow RabbitMQ-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}";
        var body = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish("","hello",null,body);
        Console.WriteLine($"RabbitMQ生产者已发送{message}");
        Console.ReadKey();
    }
}

b.消费者代码:

 var factory = new ConnectionFactory();
 factory.HostName = "localhost";
 factory.Port = 5672;
 factory.UserName = "guest";
 factory.Password = "guest";

 using (var connection = factory.CreateConnection())
 {
     using (var channel = connection.CreateModel())
     {
         //创建一个名为hello的消息队列
         channel.QueueDeclare("hello",true,false,false,null);
         var consumer = new EventingBasicConsumer(channel);
         channel.BasicConsume("hello",false,consumer);

         consumer.Received += (model, e) =>
         {
             var body = e.Body.ToArray();
             var message = Encoding.UTF8.GetString(body);

             Console.WriteLine("RabbitMQ消费者接收到消息:" + message);
         };

         channel.BasicConsume("hello",false,consumer);
         Console.WriteLine("Press [Enter] to exit");
         Console.ReadKey();

     }
 }

c.运行结果:

在这里插入图片描述

2.工作队列模式

工作队列模式,也称任务队列模式,生产者将任务传递到队列中,多个消费者竞争消费队列中的消息,一条消息只对应一个消费者

a.生产者代码:

 var factory = new ConnectionFactory();
 factory.HostName = "localhost";
 factory.Port = 5672;
 factory.UserName = "guest";
 factory.Password = "guest";

 using (var connection = factory.CreateConnection() )
 {
     using (var channel = connection.CreateModel())
     {
         channel.QueueDeclare("WorkQueue",true,false,false,null);
         for (int i = 0; i < 10; i++)
         {
             var msg = $"消息队列ID10000{i}发送成功,{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}";
             var body = Encoding.UTF8.GetBytes(msg);
             channel.BasicPublish("","WorkQueue",null,body);
             Console.WriteLine($"已发送:{msg}");
         }
         Console.ReadKey();
     }
 }

b.消费者1代码:

 var factory = new ConnectionFactory();
 factory.HostName = "localhost";
 factory.Port = 5672;
 factory.UserName = "guest";
 factory.Password = "guest";

 using (var connection = factory.CreateConnection())
 {
     using (var channel = connection.CreateModel())
     {
         //创建一个名为hello的消息队列
         channel.QueueDeclare("WorkQueue", true, false, false, null);
         //basicqos,MQ不再对消费者一次发送多条请求,而是一条处理完毕(确然后)再从队列中获取新的
         channel.BasicQos(0, 1, false);
         var consumer = new EventingBasicConsumer(channel);

         channel.BasicConsume("WorkQueue", false, consumer);

         consumer.Received += (model, e) =>
         {
             var body = e.Body.ToArray();
             var message = Encoding.UTF8.GetString(body);
             Thread.Sleep(100);
             Console.WriteLine("RabbitMQ消费者接收到消息:" + message);
             //消息确认:一旦消费者成功处理了任务,它会发送确认消息给 RabbitMQ,告知 RabbitMQ 该消息已经被处理并可以从队列中删除。如果消费者在处理任务时发生错误,消息将会被重新放回队列中,以便其他消费者重新尝试处理。
             channel.BasicAck(e.DeliveryTag,false);
         };

         channel.BasicConsume("WorkQueue", false, consumer);
         Console.WriteLine("Press [Enter] to exit");
         Console.ReadKey();

     }
 }

c.消费者2代码:

与消费者1代码一致

d.运行结果:

在这里插入图片描述

3.发布订阅模式

发布/订阅(Publish/Subscribe)模式是一种消息传递模式,用于将消息广播给多个消费者。在这种模式下,生产者将消息发送到一个交换机(Exchange)中,而不是直接发送到队列中。交换机则将消息广播给与之绑定的多个队列,每个队列都有一个或多个消费者来接收消息。

a.生产者代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        var msg = "20摄氏度";
        var body = Encoding.UTF8.GetBytes(msg);
        channel.BasicPublish("aweather","",null,body);
        Console.WriteLine("天气发送成功:"+msg);
    }
}

b.消费者1代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        channel.ExchangeDeclare("aweather", ExchangeType.Fanout);
        //声明消息队列
        channel.QueueDeclare("HuBei", true, false, false, null);
        channel.QueueBind("HuBei", "aweather", "");
        var consumer = new EventingBasicConsumer(channel);

        consumer.Received += (model, e) =>
        {
            var msg = Encoding.UTF8.GetString(e.Body.ToArray());
            Console.WriteLine("湖北收到天气信息:" + msg);
            channel.BasicAck(e.DeliveryTag, false);
        };

        channel.BasicConsume("HuBei", false, consumer);
        Console.WriteLine("Press [Enter] to exit");
        Console.ReadKey();

    }
}

c.消费者2代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        channel.ExchangeDeclare("aweather",ExchangeType.Fanout);
        //生命消息队列
        channel.QueueDeclare("HuNan",true,false,false,null);
        channel.QueueBind("HuNan","aweather","");
        var consumer = new EventingBasicConsumer(channel);

        consumer.Received += (model, e) =>
        {
            var msg = Encoding.UTF8.GetString(e.Body.ToArray());
            Console.WriteLine("湖南收到天气信息:"+msg);
            channel.BasicAck(e.DeliveryTag,false);
        };

        channel.BasicConsume("HuNan",false,consumer);
        Console.WriteLine("Press [Enter] to exit");
        Console.ReadKey();

    }
}

d.运行结果:

在这里插入图片描述

4.Routing路由模式和Topics通配符模式

Routing路由模式,是发布订阅模式的延伸,在发布订阅模式的基础上,分配
路由键(Routing Key),消费者根据路由键匹配队列中对用的消息

Topics通配符模式,是Routing路由模式的延伸,*用于匹配一个单词,#用于匹配零个或多个单词,用于替换匹配路由键

注意:一个队列只能匹配一个通配符,但是可以匹配多个路由规则,路由模式和通配符模式可以嵌套使用

a.生产者代码:

var dic = new Dictionary<string, string>();
dic.Add("china.hunan.changsha.20240418","中国湖南长沙2024年04月18日天气数据");
dic.Add("china.hubei.wuhan.20240418", "中国湖北武汉2024年04月18日天气数据");
dic.Add("china.beijing.20240418", "中国北京2024年04月18日天气数据");
dic.Add("us.flld.20240418", "美国弗罗里达2024年04月18日天气数据");

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        foreach (var item in dic)
        {
            channel.BasicPublish("weather",item.Key,null,Encoding.UTF8.GetBytes(item.Value));
        }
        Console.WriteLine("气象信息发送成功");
    }
}

b.消费者1代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";

using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        channel.ExchangeDeclare("weather",ExchangeType.Direct);
        channel.QueueDeclare("us",true,false,false,null);
        //通配符模式
        channel.QueueBind("us","weather", "us.*.20240418");
        //路由模式
        channel.QueueBind("us","weather", "china.beijing.20240418");
        channel.BasicQos(0, 1, false);
        var customer = new EventingBasicConsumer(channel);
        
        customer.Received += (model, e) =>
        {
            var msg = Encoding.UTF8.GetString(e.Body.ToArray());
            Console.WriteLine("美国收到天气信息:"+msg);
            channel.BasicAck(e.DeliveryTag,false);
        };

        channel.BasicConsume("us",false,customer);
        Console.WriteLine("Press [Enter] to exit");
        Console.Read();
    }
}

c.消费者2代码:

var factory = new ConnectionFactory();
factory.HostName = "localhost";
factory.Port = 5672;
factory.UserName = "guest";
factory.Password = "guest";
using (var connection = factory.CreateConnection())
{
    using (var channel = connection.CreateModel())
    {
        channel.ExchangeDeclare("weather",ExchangeType.Direct);
        channel.QueueDeclare("china",true,false,false,null);
        //路由模式
        channel.QueueBind("china","weather", "china.hunan.changsha.20240418");
        channel.QueueBind("china", "weather", "china.hubei.wuhan.20240418");
        channel.QueueBind("china", "weather", "china.beijing.20240418");
        //通配符模式
        //channel.QueueBind("china", "weather", "china.#");
        channel.BasicQos(0,1,false);

        var consumer = new EventingBasicConsumer(channel);
        consumer.Received += (model, e) =>
        {
            var msg = Encoding.UTF8.GetString(e.Body.ToArray());
            Console.WriteLine("中国收到天气消息:"+msg);
            channel.BasicAck(e.DeliveryTag,false);
        };
        channel.BasicConsume("china",false,consumer);
        Console.WriteLine("Press [Enter] to exit");
        Console.Read();
    }
}

d.运行结果:

在这里插入图片描述

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

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

相关文章

114 接口中幂等性的保证

前言 同样是 面试问题 如何确保接口的 幂等性 幂等是一个 较为抽象的概念, 多次重复访问, 不会导致业务逻辑的异常 这里从增删改查, 几个方面列一下 一般来说, 我们核心需要关注的就是 新增 和 更新 对于 增加元素, 首先针对唯一约束进行校验, 然后再处理新增的相关业…

IDEA中Docker相关操作的使用教程

一、引言 Docker作为当前最流行的容器化技术&#xff0c;极大地简化了应用的部署和管理。而IntelliJ IDEA作为一款强大的集成开发环境&#xff0c;也提供了对Docker的集成支持。本文将介绍如何在IDEA中配置和使用Docker&#xff0c;包括远程访问配置、服务连接、Dockerfile编写…

【Linux冯诺依曼体系结构】

目录 1.冯诺依曼体系结构原理 1.冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 截至目前&#xff0c;我们所认识的计算机&#xff0c;都是有一个个的硬件组件组成 输入单元&#…

HTML学习笔记:(一)基础方法

Html格式 里面文件使用平台为&#xff1a;w3school 1、基础功能&#xff1a; <html><head> <title>这是我的第一个html页面,会显示在浏览器的标题栏中</title> </head> <!--修改背景颜色 --> <body bgcolor"yellow"> …

如何合理利用多个中国大陆小带宽服务器?

我们知道在中国大陆带宽单价非常昂贵&#xff0c;一个1Mbps 带宽的机子一年就得卖好几百人民币&#xff0c;这是不值当的&#xff0c;当然我们可以去低价漂阿里云、腾讯云的轻量服务器&#xff0c;99包年&#xff0c;但是带宽太小很难崩。 所以&#xff0c;我们必须构建一个能够…

钉钉直播回放怎么下载到本地

钉钉直播回放如何下载到本地,本文就给大家解密如何下载到本地 工具我已经给大家打包好了 钉钉直播回放下载软件链接&#xff1a;https://pan.baidu.com/s/1_4NZLfENDxswI2ANsQVvpw?pwd1234 提取码&#xff1a;1234 --来自百度网盘超级会员V10的分享 1.首先解压好我给大家…

使用脚本启动和关闭微服务

使用脚本启动和关闭微服务 一、前言二、启动1、处理每个服务2、编写启动脚本3、其他启动脚本&#xff08;无效&#xff0c;有兴趣可以看看&#xff09;4、启动 三、关闭1、测试拿服务进程id的命令是否正确2、编写关闭脚本3、关闭 一、前言 假如在服务器中部署微服务不使用 doc…

ElasticSearch:基础操作

一、ES的概念及使用场景 ElasticSearch是一个分布式&#xff0c;高性能、高可用、可伸缩、RESTful 风格的搜索和数据分析引擎。通常作为Elastic Stack的核心来使用 我们通过将ES 和 mysql对比来更好的理解 ES&#xff0c;ES和mysql相关的基本概念的对比表格如下&#xff1a; …

从Linux角度具体理解程序翻译过程-----预处理、编译、汇编、链接

前言&#xff1a; 在C语言中&#xff0c;我们知道程序从我们所写的代码到可执行执行的过程中经历了以下过程 1.预处理 2.编译 3.汇编 4.链接 可以通过下图来理解 翻译过程 1.预处理 该过程主要进行以下操作&#xff1a; (1)头文件的包含 (2)define定义符号的替换&#xff…

稀碎从零算法笔记Day52-LeetCode:从双倍数组中还原原数组

题型&#xff1a;数组、贪心 链接&#xff1a;2007. 从双倍数组中还原原数组 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 一个整数数组 original 可以转变成一个 双倍 数组 changed &#xff0c;转变方式为将 original 中每个元素 值乘以 …

EFK架构部署

7.17版本 准备工作 配置域名 cat >> /etc/hosts <<EOF 192.168.199.149 elk149daidaiedu.com 192.168.199.150 elk150daidaiedu.com 192.168.199.155 elk155daidaiedu.com EOF 修改主机名 hostnamectl set-hostname elk155.daidaiedu.com 免密登录 ssh-keyge…

# 从浅入深 学习 SpringCloud 微服务架构(一)基础知识

从浅入深 学习 SpringCloud 微服务架构&#xff08;一&#xff09;基础知识 1、系统架构演变&#xff1a; 1&#xff09;单体应用架构。如电商项目。 用户管理、商品管理、订单管理&#xff0c;在一个模块里。 优点&#xff1a;开发简单&#xff0c;快速&#xff0c;适用于…

VScode远程连接虚拟机提示: 无法建立连接:XHR failed.问题解决方案

一问题描述 在vscode下载插件Remote-SSH远程连接虚拟机时提示无法建立连接 二.最大嫌疑原因&#xff1a; 我也是在网上找了许久&#xff0c;发现就是网络原因&#xff0c;具体不知&#xff0c;明明访问别的网页没问题&#xff0c;就是连不上&#xff0c;然后发现下载vscode的…

前端CSS基础4(像素,颜色,字体属性大小复合属性)

前端CSS基础4&#xff08;像素&#xff0c;颜色&#xff0c;字体属性大小复合属性&#xff09; CSS代码编写位置CSS像素CSS颜色CSS常用字体属性和大小字体的复合属性 CSS代码编写位置 在HTML文件的头部使用 <head><style>/* 在这里编写CSS代码 */</style> …

Meta Llama 3强势来袭:迄今最强开源大模型,性能媲美GPT-4

前言 Meta的最新语言模型Llama 3已经发布&#xff0c;标志着在大型语言模型&#xff08;LLM&#xff09;领域的一次重大突破&#xff0c;其性能在行业内与GPT-4相媲美。此次更新不仅提升了模型的处理能力和精确性&#xff0c;还将开源模型的性能推向了一个新的高度。 Huggingf…

从0开始学人工智能测试节选:Spark -- 结构化数据领域中测试人员的万金油技术(二)

Dataframe dataframe 是spark中参考pandas设计出的一套高级API&#xff0c;用户可以像操作pandas一样方便的操作结构化数据。毕竟纯的RDD操作是十分原始且麻烦的。而dataframe的出现可以让熟悉pandas的从业人员能用非常少的成本完成分布式的数据分析工作&#xff0c; 毕竟跟数据…

数仓建模—数仓架构发展史

数仓建模—数仓架构发展史 时代的变迁&#xff0c;生死的轮回&#xff0c;历史长河滔滔&#xff0c;没有什么是永恒的&#xff0c;只有变化才是不变的&#xff0c;技术亦是如此&#xff0c;当你选择互联网的那一刻&#xff0c;你就相当于乘坐了一个滚滚向前的时代列车&#xf…

电视音频中应用的音频放大器

电视机声音的产生原理是将电视信号转化为声音&#xff0c;然后通过扬声器将声音播放出来。当我们打开电视并选择频道时&#xff0c;电视机首先从天线或有线电视信号中获取声音信号。声音信号经过放大器放大之后&#xff0c;就能够通过扬声器发出声音。电视机声音的产生原理和音…

Ubuntu20.04 ISAAC SIM仿真下载使用流程(4.16笔记补充)

机器&#xff1a;华硕天选X2024 显卡&#xff1a;4060Ti ubuntu20.04 安装显卡驱动版本&#xff1a;525.85.05 参考&#xff1a; What Is Isaac Sim? — Omniverse IsaacSim latest documentationIsaac sim Cache 2023.2.3 did not work_isaac cache stopped-CSDN博客 Is…

2024蓝桥杯每日一题(最短路径)

备战2024年蓝桥杯 -- 每日一题 Python大学A组 试题一&#xff1a;奶牛回家 试题二&#xff1a;Dijkstra求最短路 II 试题三&#xff1a;spfa求最短路 试题四&#xff1a;作物杂交 试题一&#xff1a;奶牛回家 【题目描述】 晚餐时间马上就到了&#x…