ActiveMQ客户端原理及源码分析[通俗易懂]

ActiveMQ客户端原理及源码分析[通俗易懂]对于服务端,传输连接器用于接收和监听来自客户端的连接;对于客户端,传输连接器用于创建一个到服务端的连接,便于发送消息。

大家好,欢迎来到IT知识分享网。

首先为自己打个广告,我目前在某互联网公司做架构师,已经有5年经验,每天都会写架构师系列的文章,感兴趣的朋友可以关注我和我一起探讨,关注我,免费分享Java基础教程,以及进阶的高级Java架构师教程,全部免费送

主要解决三个问题:

  • 1 客户端通过什么与服务端建立连接?
  • 2 客户端通过什么方式向服务端发送消息?
  • 3 用到的设计模式-策略模式
ActiveMQ客户端原理及源码分析[通俗易懂]

1 客户端通过什么与服务端建立连接?

客户端通过传输连接器(transport connectors)与服务端建立连接。对于服务端,传输连接器用于接收(生产者)和监听(消费者)来自客户端的连接;对于客户端,传输连接器用于创建一个到服务端的连接,便于发送消息

ActiveMQ客户端原理及源码分析[通俗易懂]

图1 传输连接器与客户端和服务端的关系

传输连接器的体现

ActiveMQ软件自带5类传输连接器,具体可以查看conf/activemq.xml配置文件,我这里使用的是5.15.9版本,默认的传输连接器有以下5类:

<!--
 The transport connectors expose ActiveMQ over a given protocol to
 clients and other brokers. For more information, see:
 http://activemq.apache.org/configuring-transports.html
 -->
 <transportConnectors>
 <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
 <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
 <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
 <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
 <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
 <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
 </transportConnectors>

IT知识分享网

此处传输连接器包括name属性和uri属性,当然还包括其他属性,如discoveryUri属性。name为传输连接器的名字,必须唯一;这里重点讲一讲uri属性:

IT知识分享网tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600

属性uri包括三部分:

  • scheme:tcp协议;用于指明客户端到服务端的底层网络协议。
  • path:0.0.0.0:61616;用于指明服务器的IP地址和端口,此处默认配置为本机,如果ActiveMQ服务端运行在其他主机上则需要修改为相应的的IP地址。端口号是统一默认的,不需要修改。
  • query:maximumConnections=1000&wireFormat.maxFrameSize=104857600;用于指定额外的参数。maximumConnections表示最大连接数,wireFormat.maxFrameSize表示最大帧数,用于限定单条消息的大小,单位为字节(默认为104857600字节,即100MB)。
  • 注意:query部分的参数内容与scheme部分相关紧密,如果有兴趣可以参考官网(http://activemq.apache.org/using-activemq)

传输连接器的不同主要体现在scheme部分,即底层网络协议的不同。上面默认提供的传输连接器(应用层)有openwire协议,amqp协议,stomp协议,mqtt协议和ws协议。下面简单介绍下ActiveMQ支持的几种应用层网络协议。

  • OpenWire协议:是一种跨语言连接协议,允许从不同的平台和语言对ActiveMQ进行本地访问。它是ActiveMQ默认传输协议。OpenWire通过代码生成特定于语言的命令,并对理解核心OpenWire协议的代码进行封送处理。然后,我们可以为低层协议编写特定于语言的扩展,以在不同的语言中提供良好且易于使用的客户机API。
  • AMQP协议:即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
  • STOMP协议:即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
  • MQTT协议:即Message Queuing Telemetry Transport,消息队列遥测传输;是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
  • WS协议:即webScoket,是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。

PS:这些协议都是TCP/UDP/NIO的上层协议,关于底层网络协议这块大家可以自行百度。

传输连接器的实现

下面用Java代码实现客户端上的的传输连接器。

ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = connectionFactory.createConnection();
connection.start();

通过连接工厂创建传输连接器。大概过程如下:

1.根据入参生成URI对象,并生成一个ActiveMQConnectionFactory对象;

IT知识分享网public ActiveMQConnectionFactory(String brokerURL) {
this(createURI(brokerURL));
}
public ActiveMQConnectionFactory(URI brokerURL) {
setBrokerURL(brokerURL.toString());
}
public void setBrokerURL(String brokerURL) {
this.brokerURL = createURI(brokerURL);
 // 其他逻辑
……
}

2.connectionFactory实际调用createActiveMQConnetcion(username,password)方法返回connection对象。

该方法的核心逻辑:

// ① 根据传输工厂生成transport对象
Transport transport = createTransport();
// ② 创建ActiveMQConnection对象
connection = createActiveMQConnection(transport, factoryStats);
// ③ 设置connection相关属性,很多头部信息就是此时设置的
connection.setUserName(userName);
connection.setPassword(password);
configureConnection(connection);
// ④ 启动transport
transport.start();
if (clientID != null) {
connection.setDefaultClientID(clientID);
}
return connection;

这里要注意下面几点:

  • A. transport对象由传输工厂创建,传输工厂的类别根据URI对象的sheme属性确定
protected Transport createTransport() throws JMSException {
 try {
 URI connectBrokerUL = brokerURL;
 String scheme = brokerURL.getScheme();
 if (scheme == null) {
 … // 处理各种情况
 return TransportFactory.connect(connectBrokerUL); //
 } catch (Exception e) {
 throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
 }
 }
public static Transport connect(URI location) throws Exception {
// 策略模式找到传输工厂
TransportFactory tf = findTransportFactory(location);
return tf.doConnect(location);
}
  • B. ActiveMQConnection对象与客户端的唯一ID关联起来
protected ActiveMQConnection createActiveMQConnection(Transport transport, JMSStatsImpl stats) throws Exception {
ActiveMQConnection connection = new ActiveMQConnection(transport, getClientIdGenerator(),
getConnectionIdGenerator(), stats);
return connection;
}

transport对象和connectino对象组成了我们所说的传输连接器

2 客户端通过什么方式向服务端发送消息?

基于connection创建一个会话session对象,基于session对象创建一个消息生产者messageProducer和目的地destination(可以是Queue,也可以是Topic),消息生产者把消息封装成message对象发送到目的地。

Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue destination = session.createQueue("testQueue");
MessageProducer producer = session.createProducer(destination);
Message message = null;
for (int i = 0; i < 100; i++) {
 message = session.createTextMessage("hello world everyboy " + i );
 producer.send(message);
}

注意:

  • 可以创建多个Queue,并用逗号隔开;设置成这个Destination的physicalName属性;
  • 创建messageProducer时,会设置消息头里面的属性,但属性属于messageProducer;

send()方法发送消息。同时传递部分消息属性;

public void send(Message message) throws JMSException {
 this.send(this.getDestination(),
 message,
 this.defaultDeliveryMode,
 this.defaultPriority,
 this.defaultTimeToLive);
 }
不过真正设置消息头信息的部分在下面这个方法
protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, int deliveryMode, int priority, long timeToLive,
 MemoryUsage producerWindow, int sendTimeout, AsyncCallback onComplete) throws JMSException {
 checkClosed();
 if (destination.isTemporary() && connection.isDeleted(destination)) {
 throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
 }
 synchronized (sendMutex) {
 // tell the Broker we are about to start a new transaction
 doStartTransaction();
 TransactionId txid = transactionContext.getTransactionId();
 long sequenceNumber = producer.getMessageSequence();
 //Set the "JMS" header fields on the original message, see 1.1 spec section 3.4.11
 message.setJMSDeliveryMode(deliveryMode);
 long expiration = 0L;
 if (!producer.getDisableMessageTimestamp()) {
 long timeStamp = System.currentTimeMillis();
 message.setJMSTimestamp(timeStamp);
 if (timeToLive > 0) {
 expiration = timeToLive + timeStamp;
 }
 }
 message.setJMSExpiration(expiration);
 message.setJMSPriority(priority);
 message.setJMSRedelivered(false);
 // transform to our own message format here
 ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, connection);
 msg.setDestination(destination);
 msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));
 // Set the message id.
 if (msg != message) {
 message.setJMSMessageID(msg.getMessageId().toString());
 // Make sure the JMS destination is set on the foreign messages too.
 message.setJMSDestination(destination);
 }
 //clear the brokerPath in case we are re-sending this message
 msg.setBrokerPath(null);
 msg.setTransactionId(txid);
 if (connection.isCopyMessageOnSend()) {
 msg = (ActiveMQMessage)msg.copy();
 }
 msg.setConnection(connection);
 msg.onSend();
 msg.setProducerId(msg.getMessageId().getProducerId());
 if (LOG.isTraceEnabled()) {
 LOG.trace(getSessionId() + " sending message: " + msg);
 }
 if (onComplete==null && sendTimeout <= 0 && !msg.isResponseRequired() && !connection.isAlwaysSyncSend() && (!msg.isPersistent() || connection.isUseAsyncSend() || txid != null)) {
 this.connection.asyncSendPacket(msg);
 if (producerWindow != null) {
 // Since we defer lots of the marshaling till we hit the
 // wire, this might not
 // provide and accurate size. We may change over to doing
 // more aggressive marshaling,
 // to get more accurate sizes.. this is more important once
 // users start using producer window
 // flow control.
 int size = msg.getSize();
 producerWindow.increaseUsage(size);
 }
 } else {
 if (sendTimeout > 0 && onComplete==null) {
 this.connection.syncSendPacket(msg,sendTimeout);
 }else {
 this.connection.syncSendPacket(msg, onComplete);
 }
 }
 }
 }

可以看到,之前讲过的消息headers部分属性(发送模式(DeliveryMode),发送优先级(Priority),消息存活时间(Expiration)等)都在这里设置了。

3 使用到的设计模式

在创建transport对象的时候,首先根据uri的scheme确定xxTransportFactory,然后利用工厂对象创建transport对象。

使用策略模式,根据uri的scheme确定xxTransportFactory

// location为uri对象
TransportFactory tf = findTransportFactory(location);
public static TransportFactory findTransportFactory(URI location) throws IOException {
 String scheme = location.getScheme();
 if (scheme == null) {
 throw new IOException("Transport not scheme specified: [" + location + "]");
 }
 TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
 if (tf == null) {
 // Try to load if from a META-INF property.
 try {
 // ======================这里是重点
 tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
 TRANSPORT_FACTORYS.put(scheme, tf);
 } catch (Throwable e) {
 throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", e);
 }
 }
 return tf;
 }

TRANSPOT_FACTORY_FINDER是FactoryFinder类的一个对象,对象带有一个相对于jar包的相对路径”META-INF/services/org/apache/activemq/transport/”,该路径下包含很多以底层网络协议命名的文件;

各文件内包含一条记录,如tcp文件内的内容为:

class=org.apache.activemq.transport.tcp.TcpTransportFactory

很明显,是该协议对应的传输工厂类的类路径,所以找到该文件就能找到工厂类。然后根据下面的方法找到并创建工厂对象:

public Object newInstance(String key) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
 return objectFactory.create(path+key);
 }

至此,transportFactory对象生成完毕。

以下是分享的部分架构师的学习资料和部分零基础学习Java的视频资料,附带练习题和课堂笔记,需要的朋友可以私信我免费获取

ActiveMQ客户端原理及源码分析[通俗易懂]

ActiveMQ客户端原理及源码分析[通俗易懂]

推荐阅读:

淘宝服务端高并发分布式架构(十四次)演进之路

Spring 中策略模式的 2 个经典应用

Mybatis 使用的 9 种设计模式,真是太有用了

原文:https://zhuanlan.zhihu.com/p/70823939

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/7076.html

(0)
上一篇 2023-01-03 09:53
下一篇 2023-01-03 09:53

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信