实现目标:类似 广播的效果 服务器发消息,两个客户端都能收到 全部的消息
P:生产者,也就是要发送消息的程序
C:消费者:消息的接受者,会一直等待消息到来。
queue:消息队列,图中红色部分
而在订阅模型中,多了一个exchange角色,而且过程略有变化:
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
C:消费者,消息的接受者,会一直等待消息到来。
Queue:消息队列,接收消息、缓存消息。
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
Publish/Subscribe发布与订阅模式
发布订阅模式:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收 到消息。
服务器端( 消息生产者 )
package com.joincall.j3c.JoinCallCC.RabbitMQ;
import com.rabbitmq.client.BuiltinExchangeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class RabbitMQHelper extends Thread {
protected static final Logger logger = LoggerFactory.getLogger(RabbitMQHelper.class);
private boolean _Connected=false;//连接 RabbitMQ 成功true 或 失败false
private Channel _RabbitMQ_Ch;
public String _strMsg;
private String _exchangeName="message_exchange"; //交换机名称
//private String _strTaskQueue;//任务队列 不用了 使用多队列
//线程 执行
public void run() {
this.SendMsg(_strMsg);
}
//-----------------------------------订阅模式 多队列广播 --------------------------------------------------------------
//初始化 连接 RabbitMQ
public void InitRabbitMQ(String strIP,String strPost,String strUserName,String strPwd ){
try {
//System.out.println("初始化 连接 RabbitMQ");
//System.out.println("JoinCallCC InitRabbitMQ() 初始化 连接 RabbitMQ " +strIP +" "+strPost +" "+strUserName+" "+strPwd+" "+strTaskQueue +" ");
logger.info("RabbitMQHelper InitRabbitMQ() 初始化 连接 RabbitMQ " +strIP +" "+strPost +" "+strUserName+" "+strPwd+" " +" ");
//创建连接工厂,并设置连接信息
ConnectionFactory f = new ConnectionFactory();
//f.setHost("192.168.1.100");
//f.setPort(5672);//可选,5672是默认端口
//f.setUsername("admin");
//f.setPassword("admin");
f.setHost(strIP);
f.setPort(Integer.parseInt(strPost));//可选,5672是默认端口
f.setUsername(strUserName);
f.setPassword(strPwd);
//与rabbitmq服务器建立连接,rabbitmq服务器端使用的是nio,会复用tcp连接,并开辟多个信道与客户端通信,以减轻服务器端建立连接的开销
Connection c = f.newConnection();
//建立信道
//Channel ch = c.createChannel();
_RabbitMQ_Ch= c.createChannel();
this._Connected=true;
/** public DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal,Map<String, Object> arguments) throws IOException {
return this.exchangeDeclare(exchange, type.getType(), durable, autoDelete, arguments);
String exchange 交换机名称
BuiltinExchangeType type 交换机类型
DIRECT("direct"), 点对点交换机
FANOUT("fanout"), 广播形式的交换机
TOPIC("topic"), 通配符形式的交换机
HEADERS("headers"); 很少用不做学习
boolean durable 是否持久化
boolean autoDelete 是否自动删除
boolean internal 内部 一般设置为false
Map<String, Object> arguments 参数
}*/
this._exchangeName="message_exchange"; //交换机名称
//创建交换机
this._RabbitMQ_Ch.exchangeDeclare(this._exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
//创建队列
String queueA = "task_queue";//队列名称 -----------------------------------------------
String queueB = "agent_msg_log";//队列名称 -----------------------------------------------
/** public com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
queue 队列的名称
durable 是否持久化,当mq重启之后还在
exclusive 是否独占,只有一个消费者监听这个队列
当connection 关闭的时候删除这个队列
autoDelete 是否自动删除,没有消费者的时候删除
arguments 参数
}*/
this._RabbitMQ_Ch.queueDeclare(queueA,true,false,false,null);
this._RabbitMQ_Ch.queueDeclare(queueB,true,false,false,null);
/**queueBind(String queue, String exchange, String routingKey)
* queue 队列名称
* exchange 交换机名称
* routingKey 路由key
* 如果交换机是 fanout就设置为空字符串 **/
// 绑定交换机和队列
this._RabbitMQ_Ch.queueBind(queueA,this._exchangeName,"");
this._RabbitMQ_Ch.queueBind(queueB,this._exchangeName,"");
//发送一个 启动消息
String body = "rabbitMQ 订阅模式 启动成功";
/**public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
exchange 交换机名称,简单模式下的交换机会默认使用 ""
routingKey 路由名称
props 配置信息
body 消息体 */
this._RabbitMQ_Ch.basicPublish(this._exchangeName,"",null,body.getBytes());
logger.info("RabbitMQHelper InitRabbitMQ() 初始化 连接 RabbitMQ 成功! " +"--------------------------");
logger.info("RabbitMQHelper InitRabbitMQ() 队列A "+ queueA +"--------------------------");
logger.info("RabbitMQHelper InitRabbitMQ() 队列B "+ queueB +"--------------------------");
}catch (Exception ex){
//System.out.println("初始化 连接 RabbitMQ 失败");
logger.error("RabbitMQHelper InitRabbitMQ() 初始化 连接 RabbitMQ 失败!" +strIP +" "+strPost +" "+strUserName+" "+strPwd+" "+" "+ ex.toString() +" "+ ex.getMessage());
}
}
//发送
public void SendMsg(String strMsg) {
try {
if(this._RabbitMQ_Ch==null){
//logger.warn("JoinCallCC SendMsg() 没有启动连接RabbitMQ " +strMsg );
return;
}
if(this._Connected!=true){
logger.error("JoinCallCC SendMsg() 向RabbitMQ发送消息失败! 因连接RabbbitMQ失败!" +strMsg );
return;
}
//logger.debug("===RabbitMQHelper SendMsg() RabbitMQ发消息:" +strMsg );
//System.out.println("JoinCallCC SendMsg() RabbitMQ发送消息 " +strMsg );
//发布消息 这里把消息向默认交换机发送.默认交换机隐含与所有队列绑定,routing key即为队列名称
//参数含义:
//-exchange: 交换机名称,空串表示默认交换机"(AMQP default)",不能用 null
//-routingKey: 对于默认交换机,路由键就是目标队列名称
//-props: 其他参数,例如头信息
//-body: 消息内容byte[]数组
//RabbitMQ_Ch.basicPublish("", "task_queue", null, "Hello world!".getBytes());
//_RabbitMQ_Ch.basicPublish("", "task_queue", null, strMsg.getBytes());
//_RabbitMQ_Ch.basicPublish("", this._strTaskQueue, null, strMsg.getBytes());
//订阅模式
this._RabbitMQ_Ch.basicPublish(this._exchangeName,"",null,strMsg.getBytes());
} catch (Exception ex) {
//System.out.println("oinCallCC RabbitMQ_SendMsg() RabbitMQ发送消息 失败 "+strMsg );
logger.error("JoinCallCC SendMsg() 向RabbitMQ发送消息失败! " + ex.toString() + " " + ex.getMessage());
}
}
}
调用方法
String strRabbitMQ_Ip ="192.168.1.100";
String strRabbitMQ_Port = "5672";
String strRabbitMQ_UserName = "admin";
String strRabbitMQ_Pwd ="admin";
//初始化 连接 RabbitMQ ----------------------------
RabbitMQHelper rabbitMQHelper = new RabbitMQHelper();
rabbitMQHelper.InitRabbitMQ(strRabbitMQ_Ip, strRabbitMQ_Port, strRabbitMQ_UserName, strRabbitMQ_Pwd );
发消息
rabbitMQHelper.SendMsg("发消息测试===============");
客户端( 消息消费者 )
package com.JavaRabbitMQClient;
import java.io.IOException;
//import java.sql.Connection;
import com.rabbitmq.client.*;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import com.JavaRabbitMQClient.dbMySql.*;
/**
* Hello world!
*
*/
//消费者
public class App {
protected static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) throws Exception {
{
//System.out.println("Hello World!");
logger.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" );
logger.info("@@@@@@@@@@@@@@@@@ 启动 JavaRabbitMQClient @@@@@@@@@@@@@@@@@@@@@@@" );
logger.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" );
//logger.info("-------------------------------------------------" );
//RabbitMQ 参数
String strRabbitMQ_Ip ="192.168.1.100";//
String strRabbitMQ_Port = "5672";
String strRabbitMQ_UserName = guest";
String strRabbitMQ_Pwd = "guest";
//System.out.println( "pbx "+strPbxIp+" " + strPbxPort +" "+ strApiPwd +"---------- ");
logger.info("RabbitMQ 参数 IP:" + strRabbitMQ_Ip + " 端口:" + strRabbitMQ_Port + " 用户名:" + strRabbitMQ_UserName + " 密码:" + strRabbitMQ_Pwd + " ---------- ");
//连接工厂
ConnectionFactory f = new ConnectionFactory();
//f.setHost("192.168.1.100");//192.168.1.100 192.168.10.220
//f.setPort(5672);//可选,5672是默认端口
//f.setUsername("admin");//guest
//f.setPassword("admin");//admin
f.setHost(strRabbitMQ_Ip);//192.168.1.100 192.168.10.220
f.setPort(Integer.parseInt(strRabbitMQ_Port));//可选,5672是默认端口
f.setUsername(strRabbitMQ_UserName);//admin
f.setPassword(strRabbitMQ_Pwd);//admin
//建立连接
Connection c = f.newConnection();
//建立信道
final Channel ch = c.createChannel();
//声明队列,如果该队列已经创建过,则不会重复创建
String queueName = "agent_msg_log";//队列名称 ------ 坐席消息日志用 ------------------------------------------
ch.queueDeclare(queueName,true,false,false,null);
//System.out.println("等待接收数据");
logger.info("RabbitMQ 声明队列:" + queueName + " 已连接 " + " 等待接收数据 " + " ----------------------------- ");
//收到消息后用来处理消息的回调对象
DeliverCallback callback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String strDataTime=df.format(new Date());
//System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
String msg = new String(message.getBody(), "UTF-8");
//System.out.println(strDataTime+" 收到: "+msg);
//遍历字符串中的字符,每个点使进程暂停一秒
for (int i = 0; i < msg.length(); i++) {
if (msg.charAt(i)=='.') {
try {
//Thread.sleep(1000);//暂停1秒
Thread.sleep(20);
} catch (InterruptedException e) {
}
}
}
recMsgEvent(msg);//处理收到的消息------------------------------------------------------
//System.out.println("处理结束");
//参数1:消息标签,参数2:是否确认多条消息
ch.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
//消费者取消时的回调对象
CancelCallback cancel = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//一次只能接受一条数据
ch.basicQos(1);
//第二个参数为消息回执,消息确认处理完成,为true为自动确认,只要消息发送到消费者即消息处理成功;为false为,手动发送确认回执,服务器才认为这个消息处理成功
ch.basicConsume(queueName, false, callback, cancel);
}
}
public static void recMsgEvent(String msg){
try {
//分析 收到的RabbitMQ消息-----------------
recMsgEvent_Analyse(msg);
}catch (Exception ex)
{
logger.debug("收到消息时出错! recMsgEvent() " +msg);
}
}
}
测试结果
点击交换机名称出现
总结
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
发布订阅模式与工作队列模式的区别
1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
注:在实践中发现如果用工作队列模式(循环模式),如果有两个客户端(消息消费者)则服务器发出的消息先给A客户端,再有消息再给B客户端,交替分发。不能实现广播的效果。
如果两个客户端都要收到全部的消息(广播效果)则需要使用 订阅模式
感谢:https://blog.csdn.net/qq_38063429/article/details/112350952