简介:一份基于C#的ActiveMQ消息中间件Demo,面向需要在.NET环境中快速上手消息队列的开发者。项目完整演示了NMS客户端的连接配置、会话创建、生产者/消费者收发消息,并覆盖点对点与发布/订阅两种消息模型,同时包含消息持久化、高可用等关键特性的实践场景。压缩包共194个文件,大小12.71MB,以cs源码、dll依赖、xml配置、exe可执行程序等为主,辅以resx资源文件和sln工程文件,适合对照源码理解ActiveMQ集成细节。已有313人学习,资源内包含带图形界面的Form配置,可直观设置服务器地址、端口及消息收发参数,便于测试和调试ActiveMQ环境。通过该Demo,开发者既能掌握在.NET中使用ActiveMQ的基本流程,也能了解消息中间件的核心原理,为实际项目中的异步通信开发提供参考。 ActiveMQ 这名字在 .NET 圈子里一出现,不少人的第一反应是“这玩意不是 Java 生态的吗,跟我 C# 有什么关系”。说实话我最早也是这么想的,直到在一个上位机项目里被逼着在 C# 客户端和 Java 后端之间倒腾消息,才正儿八经把 ActiveMQ 和 C# 的搭配摸了一遍。这篇就把我搭建 ActiveMQ Demo (C#) 的完整过程、踩过的坑、调参经验一次性写清楚,给准备在 .NET 环境里接 ActiveMQ 的朋友一条能直接走通的路。
这个 Demo 能解决什么问题?简单说就是:让你在 C# 程序里通过 ActiveMQ 完成消息的发布和订阅,实现不同系统之间的异步通信、解耦和削峰。无论你是做上位机需要把设备状态推给后端,还是业务系统里要做消息通知、任务分发,这套东西都能用得上。适合刚接触消息队列的 .NET 开发者,也适合从 Java 转过来想快速上手 C# 客户端的同学。
1. 在 C# 项目里引入 ActiveMQ:我为什么这么选
1.1 消息队列能解决什么实际问题
先说个最直观的场景。我以前做的一个产线数据采集上位机,设备 PLC 每秒钟产生几百条数据,上位机要实时显示,还要同步写数据库,偶尔还要通知后端系统做联动。如果全部用同步接口硬调,上位机界面会卡死,数据库压力也扛不住,后端服务一重启,数据就丢了。
消息队列在这个架构里就是中间的那层“缓冲带”。上位机把数据作为消息发到 ActiveMQ,后端服务按自己的节奏消费,数据库写入可以批量做,处理不过来就先堆在队列里,服务重启之后还能继续消费,不丢数据。C# 这边只管把消息发出去,不用关心接收方到底是谁、什么时候处理完,这就是解耦。
另一个典型场景是系统集成。很多公司的主业务系统是 Java 写的,但你负责的模块是 C# 的,两边要做数据同步。与其两边各写一套 WebService 互相调,不如统一接 ActiveMQ,Java 端发消息,C# 端消费,协议统一、接口稳定,出问题也好排查。
1.2 为什么在 C# 端选择 ActiveMQ 而不是别的队列
现在消息队列选择很多,RabbitMQ、Kafka、RocketMQ 都有各自的拥趸。我选 ActiveMQ 主要看中三点。
第一是协议兼容性好。ActiveMQ 原生支持 OpenWire、AMQP、STOMP、MQTT 多种协议,C# 客户端可以用 Apache.NMS(Native Messaging Service)走 OpenWire 协议,也可以用 AMQP 协议接入。这意味着你手里的 C# 代码既可以直接对接 ActiveMQ,也可以平滑迁移到其他支持 AMQP 的 Broker,比如 RabbitMQ,选型余地大。
第二是部署轻量。ActiveMQ 是 Java 写的,只要机器上有 JRE 就能跑,不像 Kafka 那样要依赖 ZooKeeper,单机启动一个 Broker 就能用,非常适合中小型项目和个人 Demo。我本地开发直接解压运行即可,配置也好改。
第三是功能够用。消息持久化、事务、死信队列、延迟投递、通配符订阅这些高频功能它都有,做生产级应用也不至于捉襟见肘。
2. Demo 环境搭建:从零把 Broker 跑起来
2.1 下载安装与启动 ActiveMQ
我用的版本是 ActiveMQ Classic 5.x(也就是原来的 ActiveMQ 5.x 系列),这个版本稳定、文档多、网上踩坑资料也全,适合做 Demo。ActiveMQ Artemis 是后来的新一代 Broker,性能更强,但 NMS 客户端对 Classic 的支持更成熟,所以这里先以 Classic 为例。
到 Apache 官网下载压缩包后,解压到本地目录,比如D:\activemq。进入bin目录,里面有两个启动脚本:Windows 下用activemq.bat start,Linux 下用./activemq start。启动成功后默认监听 61616 端口(OpenWire 协议),Web 管理控制台跑在 8161 端口。
提示:本地开发时如果 61616 端口被占用,可以修改
conf/activemq.xml里的 transportConnector 端口配置。我习惯把端口改成 61617 避免和别的服务冲突。
启动后访问http://localhost:8161,默认账号密码是admin/admin,登录后能在 Queues 和 Topics 页面看到当前 Broker 上的消息情况,配合 JMX 还能看更细的指标。这个管理控制台在后面对比消费情况、排查消息堆积时非常有用。
2.2 C# 客户端库选型:NMS 还是 AMQP
C# 接 ActiveMQ 有两条主流路线。
第一条是使用 Apache.NMS.ActiveMQ,这是官方提供的 .NET 客户端,走的是 OpenWire 协议。优点是 API 设计贴近 JMS(Java Message Service)规范,你在 Java 里怎么写 ActiveMQ,在 C# 里几乎可以一一对应,社区资料也最多。缺点是这个库更新不太频繁,但胜在稳定,用于生产问题不大。
第二条是使用 AMQP 协议的客户端,比如 RabbitMQ.Client 或 AMQPNetLite 连接 ActiveMQ 的 AMQP 端口(默认 5672)。优点是如果你的系统后期要迁移到 RabbitMQ 等支持 AMQP 的 Broker,代码改动小;缺点是 ActiveMQ 对 AMQP 的支持在某些细节上并不完美,比如消息属性和头部的处理需要注意。
我这个 Demo 用的是 Apache.NMS.ActiveMQ,因为它和 ActiveMQ 配合最顺,调试问题也最容易找到参考。用 NuGet 安装:
Install-Package Apache.NMS.ActiveMQ如果是 .NET Core / .NET 5+ 项目,同样可以用这个包,我实测在 .NET 8 下可以正常运行,底层依赖是 .NET Standard 2.0,兼容性没问题。
3. 核心代码实现:从生产者到消费者完整跑通
3.1 建立连接与创建会话的基本套路
ActiveMQ 的 C# 客户端用法非常固定,核心对象就五个:连接工厂(ConnectionFactory)、连接(IConnection)、会话(ISession)、目的地(IDestination)、消息生产者/消费者(IMessageProducer/IMessageConsumer)。
先看最基础的连接建立代码:
using Apache.NMS; using Apache.NMS.ActiveMQ; var connectionFactory = new ConnectionFactory("tcp://localhost:61616"); using var connection = connectionFactory.CreateConnection(); connection.Start(); using var session = connection.CreateSession(AcknowledgementMode.AutoAcknowledge);这段代码干了三件事:建立到 Broker 的 TCP 连接、启动连接、创建一个自动确认模式的会话。AcknowledgementMode.AutoAcknowledge表示消息消费成功后自动确认,适合大多数场景。
这里有个细节很多人第一次会忽略:connection.Start()一定要调,否则消费者连接上了但收不到消息。我一开始就是忘了这行,调试了半天发现消息发得出去但收不到,后来翻文档才发现 NMS 的设计里连接默认是不启动的,必须手动 Start。
目的地(Destination)分两种:Queue(点对点)和 Topic(发布订阅)。队列消息一个消息只会被一个消费者消费;主题消息会被所有订阅者各收到一份。创建方式是对应的:
// 队列 var queue = session.GetQueue("demo.queue"); // 主题 var topic = session.GetTopic("demo.topic");3.2 生产者发送消息:同步与异步的取舍
生产者的代码很直接,创建一个生产者,然后发送文本消息:
using var producer = session.CreateProducer(queue); producer.DeliveryMode = MsgDeliveryMode.Persistent; var textMessage = producer.CreateTextMessage("Hello ActiveMQ from C#"); producer.Send(textMessage);DeliveryMode有两个取值:Persistent和NonPersistent。Persistent 模式消息会持久化到 Broker,就算 Broker 重启也不会丢,代价是性能稍差;NonPersistent 模式消息只存在内存里,宕机就丢,但吞吐量高。我的经验是:业务数据必须用 Persistent,临时通知类的内容可以用 NonPersistent,性能差一个量级。
发送消息还可以设置消息属性、延迟时间等,比如设置延迟投递:
var scheduledMessage = producer.CreateTextMessage("延迟消息"); scheduledMessage.Properties["AMQ_SCHEDULED_DELAY"] = 5000; // 延迟5秒 producer.Send(scheduledMessage);这个功能在做定时任务分发时很好用,但要注意schedulerSupport需要在activemq.xml中开启才能生效,否则消息会立即投递。
3.3 消费者接收消息:同步和异步两种模式
消费者的写法有两种。一种是同步阻塞方式,适合简单 Demo,直接调用Receive()函数,没消息就卡住等:
using var consumer = session.CreateConsumer(queue); var message = consumer.Receive(TimeSpan.FromSeconds(5)); if (message is ITextMessage textMessage) { Console.WriteLine($"收到消息: {textMessage.Text}"); }另一种是异步监听方式,生产环境推荐用这个,不阻塞主线程:
var consumer = session.CreateConsumer(queue); consumer.Listener += message => { if (message is ITextMessage textMessage) { Console.WriteLine($"收到消息: {textMessage.Text}"); } };异步监听模式要注意线程安全问题。Listener 回调是在 NMS 的线程池里执行的,如果你的消费逻辑里要操作 UI 控件,需要 Invoke 到 UI 线程,否则 WinForms / WPF 里会直接抛异常。
对于 Topic 的消费,还有个持久化订阅的问题。如果消费者先退出再重新订阅 Topic,普通订阅会错过离线期间的消息。想要不丢,需要创建持久化订阅:
var durableConsumer = session.CreateDurableConsumer(topic, "client-id-001", null, false);同时连接工厂这边要设置clientId:
var connectionFactory = new ConnectionFactory("tcp://localhost:61616"); connectionFactory.ClientId = "client-id-001";3.4 完整 Demo 的代码组织
一个完整的 Demo,我通常会写成一个简单的控制台应用,结构如下:
Program.cs:入口,启动时根据参数决定是生产者还是消费者Producer.cs:封装消息发送逻辑Consumer.cs:封装消息接收逻辑appsettings.json:配置 Broker 地址、队列/主题名称
// Program.cs Console.WriteLine("请输入角色: 1-生产者 2-消费者"); var role = Console.ReadLine(); if (role == "1") { var producer = new Producer("tcp://localhost:61616"); while (true) { Console.WriteLine("输入消息内容(exit退出):"); var input = Console.ReadLine(); if (input == "exit") break; producer.SendText("demo.queue", input); } } else { var consumer = new Consumer("tcp://localhost:61616"); consumer.ConsumeQueue("demo.queue", msg => Console.WriteLine($"收到: {msg}")); Console.ReadLine(); }这样组织的好处是代码清晰,后续想扩展成 WinForms 或者 WPF 上位机版本,只需要把控制台输入换成界面按钮就行,核心的消息收发逻辑不用大改。
4. 进阶功能与性能调优:Demo 之外的实战细节
4.1 与 Spring Boot 项目协同:跨语言消息互通
热搜词里出现了“springboot整合activemq”,说明不少人的实际场景是 Java 后端和 C# 客户端共存。我在一个项目里就遇到过:Java 后端负责业务逻辑,C# 做的 WinForms 上位机负责展示和操作,两边通过 ActiveMQ 通信。
Java 端用 Spring Boot 整合 ActiveMQ 发布消息到demo.queue,C# 消费者这边不需要做任何特殊处理,因为发送到 Queue 的消息对协议层来说是透明的。反过来 C# 生产者发消息,Java 端用@JmsListener就能订阅。唯一要注意的是消息格式约定,比如统一用 TextMessage+JSON 字符串,或者用 BytesMessage 传二进制数据,两端约定好属性名和序列化规则。
跨语言协作时最容易出的问题就是消息体类型不匹配。Spring Boot 默认用 SimpleMessageConverter 把对象序列化成字节流,这个格式 C# 端直接当 TextMessage 读会读到乱码。解决办法是 Java 端生产消息时用convertAndSend(String)或者自己控制消息体为纯 JSON 字符串,C# 端也只处理 TextMessage,两边都别玩花活。
4.2 JMX 监控与 TopicSubscriptionViewMBean 的用法
热搜词里“activemq jmx 查询 topicsubscriptionviewmbean”也是个高频问题,这属于线上排查的进阶操作。ActiveMQ 启动时默认开启了 JMX(Java Management Extensions),可以通过 JConsole 或者 JMX 客户端查看 Broker 内部状态。
TopicSubscriptionViewMBean这个 MBean 在org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Topic,destinationName=demo.topic,endpoint=Consumer,clientId=xxx,consumerId=xxx路径下。通过它你可以查到某个 Topic 的订阅者状况:是否有活跃订阅者、消息积压了多少、消费速率是多少。
在 C# 里查询 JMX 需要额外引入System.Management或者通过连接到 JMX 的 RMI 端口(默认 1099),用 javax.management API 的桥接方式。实操中我更推荐直接用 JConsole 图形化看,省时省力:
- 在
conf/activemq.xml中确认useJmx="true"(默认开启) - 运行 JConsole,连接到本地 Java 进程(即 activemq 进程)
- 进入 MBean 面板,展开
org.apache.activemq节点 - 找到对应的 TopicSubscriptionViewMBean,查看
PendingQueueSize、DequeuedCount、DispatchedCount等属性
这个操作在排查“消息发出去但消费者没收到”这类问题时非常关键。你可以一眼看出消息是卡在 Broker 上没有被消费,还是已经被消费了但显示端没刷新。
4.3 几个关键调优参数
性能调优在 Demo 阶段用不上,但提前了解一下能避免上线后手足无措。C# 客户端这边有几个参数值得关注。
PrefetchSize(预取大小)决定了消费者一次从 Broker 拉多少消息到本地缓存。默认值是 1000,对于高吞吐场景很友好,但如果你的消费逻辑比较重,一次性拉太多反而容易造成消息堆积在客户端内存里。可以设置更小的值:
var prefetchPolicy = new ConnectionPrefetchPolicy { QueuePrefetch = 100, TopicPrefetch = 50 }; connectionFactory.PrefetchPolicy = prefetchPolicy;DispatchAsync(异步分发)设置是否异步分发消息给消费者。默认是 false,在部分场景下改成 true 能提升吞吐量,但消息到达的时序性会略受影响。
消费者这边的ReceiveTimeout也很重要。用consumer.Receive()不带参数会无限期阻塞,程序退出时可能出现线程挂住的情况。我一般给个 3-5 秒的超时,配合循环判断退出标志。
4.4 事务会话与消息确认机制
如果你追求“不丢消息”,事务会话是必须掌握的。事务的意义在于:一批消息要么全部成功,要么全部回滚。
using var session = connection.CreateSession(AcknowledgementMode.Transactional); using var producer = session.CreateProducer(queue); try { producer.Send(producer.CreateTextMessage("第一条")); producer.Send(producer.CreateTextMessage("第二条")); session.Commit(); // 提交事务 } catch { session.Rollback(); // 出错回滚,消息不会投递 throw; }对于消费者,事务会话表示在Commit()之前,你消费到的消息不会被确认,如果消费者崩溃,这些消息会被重新投递给其他消费者。这在实现“至少一次”投递语义时很实用。
不过事务会显著降低吞吐量,而且要注意事务的作用范围是整个 Session,不是单条消息。如果一个 Session 上的消费者处理某条消息报错了,回滚之后这条消息会再次被投递,可能造成死循环,代码里要做好防御。
5. 高频问题排查:我把踩过的坑都列在这
5.1 消息发出去但消费者收不到
步骤一:先看管理控制台的 Queues 页面,确认消息是否已经进入队列。如果队列里消息数为 0,说明生产者的消息根本没有到达 Broker,检查连接地址和网络。如果队列里有消息但消费者收不到,说明消费者注册有问题。
常见原因有三种。一是忘记调connection.Start(),前面提到过。二是消费者在生产者发送之前还没有注册到 Broker,Queue 消息不会补发给不在线的消费者,Topic 则更严格,非持久化订阅者只能收到订阅期间发送的消息。三是消费者线程被阻塞了,比如 UI 线程里同步调用Receive()导致消息处理不过来。
5.2 消费者重复消费消息
如果消费者处理消息后异常崩溃,或者事务回滚,消息会被重新投递,这是消息队列的常见特性。确认机制没设置好也会导致重复消费。比如AcknowledgementMode.ClientAcknowledge模式下,你必须手动调用message.Acknowledge(),否则消息永远不会确认,重启后会重新投递。
处理重复消费的最佳实践是在消费端做幂等:记录消息 ID,处理过就不再处理。ActiveMQ 的消息带有NMSCorrelationID和NMSMessageId,可以把NMSMessageId存到本地缓存或者数据库里做去重。
5.3 连接不稳定或经常断开
首先检查网络和防火墙,61616 端口是否开放。其次 NMS 客户端默认有心跳(KeepAlive)机制,避免 Broker 在长时间没有消息交互时误判连接失效。如果连接被防火墙切断,可以通过设置连接的KeepAliveInterval和RequestTimeout来优化:
var connectionFactory = new ConnectionFactory("tcp://localhost:61616?keepAlive=true&keepAliveInterval=30000");生产环境我还会在程序里加一个重连机制,用 while 循环包裹连接建立逻辑,检测到ConnectionClosedException时自动重连,并让消费者重新注册监听,避免服务因一次性网络抖动就掉线。
5.4 消息堆积但不消费
消息堆积最常见的原因是消费者处理速度跟不上生产速度。用管理控制台或者 JMX 看消费者是否在线、是否有大量消息处于 Pending 状态。解决办法是增加消费者数量(并发消费),或者优化单条消息的处理逻辑。
如果是消费者数量固定但消息仍然堆积,检查有没有消费者线程挂死。在 C# 里如果消费回调抛出未处理异常,NMS 的线程可能直接崩掉,外部看起来连接还在但消费停了。所以回调里必须写 try-catch,把异常记录下来再继续。
5.5 序列化导致的接收乱码
C# 端发送字符串消息,Java 端收到乱码,反过来也一样。遇到这类问题先确认两边都用的 TextMessage + UTF-8 编码字符串。ActiveMQ 对字符串消息的默认编码是基于平台编码的,可能会在跨平台时踩坑。稳妥做法是在发送方显式指定 UTF-8:
var textMessage = producer.CreateTextMessage(input); textMessage.Properties["encoding"] = "UTF-8";接收方看到这个属性就按 UTF-8 解码。如果是二进制消息,最好直接在消息里附带格式说明、版本号字段,方便兼容演进。
6. 尾声:一个小技巧
想起来最开始做这个 Demo 时,还发生过一个有趣的问题:WinForms 上位机里消息回调需要更新 UI,我直接在 Listener 事件里写了this.Text = msg,结果程序狂闪退。后来才知道 NMS 的消息线程和 UI 线程不是一回事,得用Invoke或者SynchronizationContext切线程。
这个问题后来成了一个固定笔记:接 ActiveMQ 的上位机项目,消息回调里所有操作 UI 的地方统一走一个TaskScheduler.FromCurrentSynchronizationContext()或者Control.Invoke,千万别图省事直接写界面调用。
还有一次消息队列里出现了一堆死信消息,原因是消费者处理消息时抛异常,事务不断回滚重投递,最后超过最大重投次数进入死信队列(ActiveMQ 默认的死信队列前缀是ActiveMQ.DLQ)。后来我给 Demo 加了一个异常策略:消息处理失败时先检查重投次数,如果超过 3 次就手动发送到单独的error.queue,由人工事后处理,而不是让系统无限重试。
这些都是文档里不会主动告诉你,但一上线就会让你加班的细节。希望这篇基于 C# 的 ActiveMQ Demo 实战记录,能让你少走几步弯路。如果你也在自己的项目里摸索消息队列,欢迎多试多踩,只有实际跑起来,才能感受到消息队列带来的架构红利。
本文还有配套的精品资源,点击获取