ActiveMQ源碼解析(一)建立連接

作為一個消息中間件,有客戶端和服務端兩部分代碼,這次的源碼解析系列主要從客戶端的代碼入手,分成建立連接、消息發送、消息消費三個部分。趁著我昨天弄明白了源碼編譯的興奮勁頭還沒過去,今天研究一下建立連接的部分。

如果讀起來吃力,代碼部分可以略過,我把主要的功能點給加粗。

通常來說,客戶端使用MQ的API建立時,可以分成兩個步驟:
1. 對于連接的配置,比如服務器IP地址,用戶名和密碼等等
2. 建立連接并啟動
客戶端示例代碼:

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(username,password,url);  
ActiveMQConnection connection = connectionFactory.createConnection();  
connection.start();

可以看到主要的方法是ActiveMQConnectionFactory的構造函數,和createConnection(),以及connection中的start()方法。

ActiveMQConnectionFactory中的createConnection

構造函數比較簡單,直接把傳入的用戶名密碼和url放在變量里

public ActiveMQConnectionFactory(String userName, String password, URI brokerURL) {
     setUserName(userName);
     setPassword(password);
     setBrokerURL(brokerURL.toString());
}

createConnection方法指向了createActiveMQConnection方法,該方法中主要做的事情有三個:
1. 建立Transport和通過Transport建立Connection
2. 配置Connection,建立好的Transport對象會被放到Connection對象中
3. 啟動Transport

//建立Transport和通過Transport建立Connection
Transport transport = createTransport();
connection = createActiveMQConnection(transport, factoryStats);            
//配置
connection.setUserName(userName);            
connection.setPassword(password);            
configureConnection(connection);
//啟動Transport
transport.start();

configureConnection(connection);這個方法的作用是對實例化出的ActiveMQConnetion對象中的參數的一系列配置,代碼有點長就不上了。
對于我們來說其實主要想看的是連接是如何建立起來的,也就是

Transport transport = createTransport();
connection = createActiveMQConnection(transport, factoryStats);      

createTransport();方法中包含了對客戶端傳入的url的初步校驗,主要是驗證URL的合法性,而后調用工廠類TransportFactory.connection(url)來進行連接的建立。

我們客戶端在建立連接的時候,有可能有TCP、UDP等等協議,AMQ實現了簡單工廠類FactoryFinder,在TransportFactory.connection(url)方法中,先是通過FactoryFinder根據用戶輸入的url(比如tcp://192.168.0.1)來找到使用的協議工廠TcpTransportFactory,然后使用TcpTransportFactory中的類來進行連接的建立。這個過程從代碼上來看有點曲折:
1. TransportFactory的connect()調用findTransportFactory方法
2. findTransportFactory調用FactoryFinder類的newInstance方法
3. newInstance調用ObjectFactory類的create方法
4. ObejctFactory是一個接口類,實現類是StandaloneObjectFactory,其中的create方法調用自身的loadClass方法
5. loadClass方法中最終找到正確的類,返回至TransportFactory中
6. 如果是tcp連接,最終得到的就是一個實例化的TcpTransportFactory類

public abstact class TransportFactory {
……
    private static final FactoryFinder TRANSPORT_FACTORY_FINDER = new FactoryFinder("META-INF/services/org/apache/activemq/transport/");

    public static Transport connect(URI location) throws Exception {
        TransportFactory tf = findTransportFactory(location);
        return tf.doConnect(location);
    }

    public static TransportFactory findTransportFactory(URI location) throws IOException {
        //拆分url
        String scheme = location.getScheme();
        if (scheme == null) {
            throw new IOException("Transport not scheme specified: [" + location + "]");
        }
        TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
        if (tf == null) {
            // 調用FactoryFinder找到正確的TransportFactory
            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;
    }
……
}
public class FactoryFinder {
……
    //通過ObjectFactory來找到正確的TransportFactory
    public Object newInstance(String key) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException {
        return objectFactory.create(path+key);
    }
……
}

ObjectFactory的設計也是很有趣的。AMQ在代碼中的說法是之所以這么實現是因為這樣如果用戶想自己寫一個ObjectFactory,也可以支持。

    /**
     * The strategy that the FactoryFinder uses to find load and instantiate Objects
     * can be changed out by calling the
     * {@link org.apache.activemq.util.FactoryFinder#setObjectFactory(org.apache.activemq.util.FactoryFinder.ObjectFactory)}
     * method with a custom implementation of ObjectFactory.
     *
     * The default ObjectFactory is typically changed out when running in a specialized container
     * environment where service discovery needs to be done via the container system.  For example,
     * in an OSGi scenario.
     */
    public interface ObjectFactory {
        /**
         * @param path the full service path
         * @return
         */
        public Object create(String path) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException;

    }

Anyway,我們現在通過這么曲折的過程得到了一個實例化的TcpTransportFactory對象,下一步應該是調用doConnect(url)方法進行連接的建立了。因為TcpTransportFactory繼承了TransportFactory類,doConnect方法仍然是在TransportFactory中的:

    public Transport doConnect(URI location) throws Exception {
        try {
            //把url里關于Transport的配置提取出來,WireFormat基本都可以看成是url的配置。
            //如果使用Openwire(默認協議),那么WireFormat就是openwire的相關配置。
            //見http://activemq.apache.org/configuring-wire-formats.html
            Map<String, String> options = new HashMap<String, String>(URISupport.parseParameters(location));
            if( !options.containsKey("wireFormat.host") ) {
                options.put("wireFormat.host", location.getHost());
            }
            WireFormat wf = createWireFormat(options);

            //建立socket連接
            Transport transport = createTransport(location, wf);

            //裝飾者模式,在連接上包裝上MutexTransportFilter、WireFormatNegotiator、InactivityMonitor、ResponseCorrelator四個功能
            Transport rc = configure(transport, wf, options);

            //remove auto
            IntrospectionSupport.extractProperties(options, "auto.");

            if (!options.isEmpty()) {
                throw new IllegalArgumentException("Invalid connect parameters: " + options);
            }
            return rc;
        } catch (URISyntaxException e) {
            throw IOExceptionSupport.create(e);
        }
    }

這個方法中主要有三個重要功能:
1. 配置wireformat
2. 建立TcpTransport連接
3. 在連接上包裝四大輔助功能
其中配置WireFormat可以不看,建立TcpTransport其實是在調用createTransport(location, wf);時引用了TcpTransport的構造函數,代碼如下:

    public TcpTransport(WireFormat wireFormat, SocketFactory socketFactory, URI remoteLocation,
                        URI localLocation) throws UnknownHostException, IOException {
        this.wireFormat = wireFormat;
        this.socketFactory = socketFactory;
        try {
            this.socket = socketFactory.createSocket();
        } catch (SocketException e) {
            this.socket = null;
        }
        this.remoteLocation = remoteLocation;
        this.localLocation = localLocation;
        this.initBuffer = null;
        setDaemon(false);
    }

這里直接調用了socketFactory.createSocket();,使用的是默認的方法,所以無法指定本地網卡建立連接。我看了下其實可以用socketFactory.createSocket(host, port, localHost, localPort)來改寫,改寫后就可以指定本地IP和端口了。

此外,查了下網絡上的資料,四大輔助后續再看:

  1. MutexTransportFilter類實現了對每個請求的同步鎖,同一時間只允許發送一個請求,如果有第二個請求需要等待第一個請求發送完畢才可繼續發送。

  2. WireFormatNegotiator類實現了在客戶端連接broker的時候先發送數據解析相關的協議信息,例如解析版本號,是否使用緩存等信息。

  3. InactivityMonitor類實現了連接成功后啟動心跳檢查機制,客戶端每10秒發送一次心跳信息,服務端每30秒讀一次心跳信息,如果沒有讀到則會斷開連接,心跳檢測是相互的,客戶端也會每30秒讀取服務端發送來的心跳信息,如果沒有讀到也一樣會斷開連接。

  4. ResponseCorrelator類實現了異步請求但需要獲取響應信息否則就會阻塞等待功能。

ActiveMQConnection的Start()

在使用AMQ的過程中,很多用戶問我為什么Connection需要start(),不能在createConnection的時候直接start了嗎?而且不調用start方法其實不影響發送,但是會影響接收。抱著這樣的疑惑,我們來看一下源碼。

    /**
     * Starts (or restarts) a connection's delivery of incoming messages. A call
     * to <CODE>start</CODE> on a connection that has already been started is
     * ignored.
     *
     * @throws JMSException if the JMS provider fails to start message delivery
     *                 due to some internal error.
     * @see javax.jms.Connection#stop()
     */
    @Override
    public void start() throws JMSException {
        checkClosedOrFailed();
        ensureConnectionInfoSent();
        if (started.compareAndSet(false, true)) {
            for (Iterator<ActiveMQSession> i = sessions.iterator(); i.hasNext();) {
                ActiveMQSession session = i.next();
                session.start();
            }
        }
    }

源碼里直接對start方法加了注釋,說明start就是啟動connection可以接收消息的功能。其實源碼里可以很明顯看出來start里包含了幾個步驟:
1. 檢查連接是否關閉或失效
2. 確認客戶端的ConnectionInfo是否被送到服務器
3. 啟動這個Connection中的每一個Session

我好奇的是第二步,看看源碼

    /**
     * Send the ConnectionInfo to the Broker
     *
     * @throws JMSException
     */
    protected void ensureConnectionInfoSent() throws JMSException {
        synchronized(this.ensureConnectionInfoSentMutex) {
            // Can we skip sending the ConnectionInfo packet??
            if (isConnectionInfoSentToBroker || closed.get()) {
                return;
            }
            //TODO shouldn't this check be on userSpecifiedClientID rather than the value of clientID?
            if (info.getClientId() == null || info.getClientId().trim().length() == 0) {
                info.setClientId(clientIdGenerator.generateId());
            }
            syncSendPacket(info.copy(), getConnectResponseTimeout());

            this.isConnectionInfoSentToBroker = true;
            // Add a temp destination advisory consumer so that
            // We know what the valid temporary destinations are on the
            // broker without having to do an RPC to the broker.

            ConsumerId consumerId = new ConsumerId(new SessionId(info.getConnectionId(), -1), consumerIdGenerator.getNextSequenceId());
            if (watchTopicAdvisories) {
                advisoryConsumer = new AdvisoryConsumer(this, consumerId);
            }
        }
    }

從源碼里還能看到討論和待辦……我覺得我已經深入核心了……這個方法里做了兩件事,

  1. 發送ConnectionInfo的數據包到服務端,如果info里沒有用戶自己設定的clientID,還會自動幫忙生成一個。發送的時候調用的是syncSendPacket方法,很明顯是個同步發送,需要服務端同步返回response才算發送成功,我理解這里應該是一個試探連接的步驟。
  2. 建立一個通往臨時目的地的消費者。所以其實每一個ActiveMQConnection的連接中都自動包含了一個消費者。我臨時寫了個客戶端試了下,的確是存在的。
連接建立時的臨時目的地

奇葩的是我就算不調用connection.start()方法,直接發送消息,這個臨時消費者也是存在的,所以肯定是在消息發送的時候的哪個地方調用了connection的start方法。

至于為什么不調用start()方法就無法消費,我看了下session的start方法:

    /**
     * Start this Session.
     *
     * @throws JMSException
     */
    protected void start() throws JMSException {
        started.set(true);
        for (Iterator<ActiveMQMessageConsumer> iter = consumers.iterator(); iter.hasNext();) {
            ActiveMQMessageConsumer c = iter.next();
            c.start();
        }
        executor.start();
    }

原來是在session的start方法里啟動了這個session里的consumer,想想session的建立過程的確是不需要調用session.start方法的,但是我們一般是先調用start方法,而后建立consumer,這個邏輯順序還是有點錯亂……
等下一次研究接收端的源碼時再深入吧。

本次發現的源碼優化點

1. socket建立時,使用不同的createSocket方法,指定本機IP和端口。
2. 項目用到了advisory message,每當agent建立/斷開連接的時候,ActiveMQ.Advisory.Connection中會生成一條消息,這個消息中帶上了ConnectionInfo。項目就是使用這個來即時檢測agent的在線和離線狀態的。因此如果我們改一下ConnectionInfo,加上agent的一些重要信息,比如agent版本號,操作系統類型,真實IP地址等等,會在獲取agent信息的即時性上得到很大的提高。

我真的去試了一下……在ConnectionInfo里添加了一條test=test,然后重新編譯服務端和客戶端的依賴jar包,開啟MQ的logging plugins,并且用客戶端去監聽了一下ActiveMQ.Advisory.Connection,得到了這樣的結果。

服務器上開啟logging后建立連接看到的ConnectionInfo
ConnectionInfo {commandId = 1, 
responseRequired = true, 
connectionId = ID:Air.local-51230-1502000963732-1:1, 
clientId = ID:Air.local-51230-1502000963732-0:1, 
clientIp = tcp://127.0.0.1:51231, 
userName = null, password = *****, 
test = test, 
brokerPath = null, 
brokerMasterConnector = false, 
manageable = true, 
clientMaster = true, 
faultTolerant = true, 
failoverReconnect = false}

成功!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,809評論 18 139
  • 1.OkHttp源碼解析(一):OKHttp初階2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HT...
    隔壁老李頭閱讀 20,941評論 24 176
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,729評論 18 399
  • 06 我有一個閨密叫曾小姐。 曾小姐屬性毒舌,平日里說話的殺傷力特別大,而且還時常嫌棄我,每天不損一下我她的屁股就...
    講故事的余姑娘閱讀 219評論 0 0
  • 雷雷的婚禮十分的冷清,他的家人只來了他媽媽,據說他爸爸已經要跟雷雷斷絕關系了。 但是雷雷絲毫不為所動。他略微憔悴的...
    東方辭閱讀 286評論 0 3