实现目标:类似 广播的效果 服务器发消息,两个客户端都能收到 全部的消息
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) { try { //System.out.println("Hello World!"); logger.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ); logger.info("@@@@@@@@@@@@@@@@@ 启动 JavaRabbitMQClient @@@@@@@@@@@@@@@@@@@@@@@" ); logger.info("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ); //logger.info("----------------- queue_log4j ---------------------------------" ); Properties prop = readConfigFile("config.properties"); //MySql 参数 MySqlUtil.mysql_open = prop.getProperty("mysql_open"); MySqlUtil.mysql_url = prop.getProperty("mysql_url"); MySqlUtil.mysql_username = prop.getProperty("mysql_username"); MySqlUtil.mysql_password = prop.getProperty("mysql_password"); MySqlUtil.mysql_driver = prop.getProperty("mysql_driver"); logger.info("MySql 参数: " + prop.getProperty("mysql_url") + " -- " + prop.getProperty("mysql_username") + " -- " + prop.getProperty("mysql_password") + " -- " + prop.getProperty("mysql_driver")); if (MySqlUtil.mysql_open.equals("true") == false) { logger.warn("没有启动连接 MySql 数据库 " + MySqlUtil.mysql_open + " " ); }else { try { String str = MySqlUtil.connectionTest();//连接测试 logger.info("连接测试结果: " + str); logger.info("==========================连接 MySql 成功!==========================="); } catch (Exception e) { logger.info("连接 MySql 失败 " + e.toString() + " ==========================="); } } //RabbitMQ 参数 String strRabbitMQ_Open = prop.getProperty("rabbitmq_open");//是否 开启 RabbitMQ String strRabbitMQ_Ip = prop.getProperty("rabbitmq_ip");// String strRabbitMQ_Port = prop.getProperty("rabbitmq_port"); String strRabbitMQ_UserName = prop.getProperty("rabbitmq_username"); String strRabbitMQ_Pwd = prop.getProperty("rabbitmq_pwd"); //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"); //如果中文乱码则换GB2312 或 GBK 试试 String msg = new String(message.getBody(),"GB2312"); //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); }catch (Exception ex) { logger.error("JavaRabbitMQClient 启动时出错! App.main() " +ex.toString()); } } public static void recMsgEvent(String msg){ //DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG < INFO < WARN < ERROR < FATAL try { /* String strLevel = msg.substring(0, 4); if (strLevel.equals("INFO") == true) { logger.info(msg); } else if (strLevel.equals("DEBU") == true) { logger.debug(msg); } else if (strLevel.equals("WARN") == true) { logger.warn(msg); } else if (strLevel.equals("ERRO") == true) { logger.error(msg); } else { logger.info(msg); }*/ //分析 收到的RabbitMQ消息----------------- recMsgEvent_Analyse(msg); }catch (Exception ex) { logger.debug("收到消息时出错! recMsgEvent() " +msg); } } //分析 收到的RabbitMQ消息 public static void recMsgEvent_Analyse(String strMsg){ //是否包含 boolean isInclude = strMsg.contains("<agentMsgLog>"); if(isInclude==true){ //包含 recMsgEvent_AgentMsgLog(strMsg);////收到 坐席状态消息 }else{ //不包含 } } //收到 坐席状态消息 public static void recMsgEvent_AgentMsgLog(String strMsg){ String strMsgJson=strMsg.replace("<agentMsgLog>",""); //String str=getEncoding(strMsg); //logger.error("!!!!!!!!!!!!!!!!!!! " +str +" !!!!!!! "+strMsg); logger.debug("收到 坐席状态消息: " +strMsg); //<agentMsgLog>{"msgType":"AgentSend","dateTime":"2022-08-03 11:49:58","msg":"AgentLogin","agentName":"1008","agentPwd":"1008","extNum":"","description":"坐席登陆"} //<agentMsgLog>{"msgType":"AgentRec","dateTime":"2022-08-03 11:49:58","msg":"AgentLoginOk","agentName":"1008","extNum":"1008","agentGroupId":"6703","description":"坐席登陆成功"} //<agentMsgLog>{"msgType":"PbxApi","dateTime":"2022-08-03 11:49:58","msg":"AgentUnPause","agentName":"1008","extNum":"1008","agentGroupId":"6703","description":"恢复队列服务(取消暂停)","cause":"坐席登陆 恢复队列服务(取消暂停)"} JSONObject jsonObj = JSONObject.parseObject(strMsgJson);// String strMsgType = jsonObj.get("msgType").toString();//消息类型 String strMsgName = jsonObj.get("msg").toString();//消息名称 if (strMsgType.equals("AgentSend") == true) { dbMySql_AgentSend thdAgentSend = new dbMySql_AgentSend(strMsgJson,strMsgType,strMsgName); thdAgentSend.start(); }else if (strMsgType.equals("AgentRec") == true) { //logger.debug(msg); dbMySql_AgentRec thdAgentRec = new dbMySql_AgentRec(strMsgJson,strMsgType,strMsgName); thdAgentRec.start(); } else if (strMsgType.equals("PbxApi") == true) { dbMySql_PbxApi thdPbxApi = new dbMySql_PbxApi(strMsgJson,strMsgType,strMsgName); thdPbxApi.start(); }else if (strMsgType.equals("PbxEvent") == true) { dbMySql_PbxEvent thdPbxEvent = new dbMySql_PbxEvent(strMsgJson,strMsgType,strMsgName); thdPbxEvent.start(); } } //region 读取配置文件 public static Properties readConfigFile(String cfgFile) { try { //System.out.println( JonCallCC.class.getClassLoader().getResource(cfgFile) + "--------------------"); InputStream in = App.class.getClassLoader().getResource(cfgFile).openStream(); Properties prop = new Properties(); prop.load(in); return prop; } catch (IOException e) { e.printStackTrace(); System.out.println( e.getMessage()); return null; } } //endregion public static String getEncoding(String str) { String encode = "GB2312"; try { if (isEncoding(str, encode)) { // 判断是不是GB2312 return encode; } } catch (Exception exception) { } encode = "ISO-8859-1"; try { if (isEncoding(str, encode)) { // 判断是不是ISO-8859-1 return encode; } } catch (Exception exception1) { } encode = "UTF-8"; try { if (isEncoding(str, encode)) { // 判断是不是UTF-8 return encode; } } catch (Exception exception2) { } encode = "GBK"; try { if (isEncoding(str, encode)) { // 判断是不是GBK return encode; } } catch (Exception exception3) { } return "如果都不是,说明输入的内容不属于常见的编码格式"; // 如果都不是,说明输入的内容不属于常见的编码格式。 } public static boolean isEncoding(String str, String encode) { try { if (str.equals(new String(str.getBytes(), encode))) { return true; } } catch (Exception e) { e.printStackTrace(); logger.error("!!!!!! isEncoding" + e.getMessage()); } return false; } }
测试结果
点击交换机名称出现
总结
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
发布订阅模式与工作队列模式的区别
1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
注:在实践中发现如果用工作队列模式(循环模式),如果有两个客户端(消息消费者)则服务器发出的消息先给A客户端,再有消息再给B客户端,交替分发。不能实现广播的效果。
如果两个客户端都要收到全部的消息(广播效果)则需要使用 订阅模式
感谢:https://blog.csdn.net/qq_38063429/article/details/112350952