freeswitch筆記(4)-esl inbound模式的重連及內存泄露問題


esl inbound client,內部有一個canSend()方法:

    public boolean canSend() {
        return channel != null && channel.isConnected() && authenticated;
    }

大多數情況下(之所以說大多數情況是因為最末尾還有一個authenticated),都可以用它來檢測網絡是否斷開,如果斷開了,可以自己寫代碼重連(注:0.9.2版本依賴的netty較老,esl client本身也並沒有重連邏輯)。

而且在org.freeswitch.esl.client.inbound.Client#connect()方法里,有一個判斷:

如果之前有連着,先close斷開,接下來看close方法:

這里又做了1次網絡檢測,checkConnected實現如下:

看上去很嚴謹,雙重檢測,感覺重連時只要再調用1次connect就可以了,但是這里有一個陷阱:如果channel連接正常,但是authenticated=false,canSend()就返回false,這時候再去connect,先前的連接並不會釋放,造成連接泄露

為了重現這個問題,我們先准備一段代碼:

import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.transport.event.EslEvent;


public class InboundTest {

    private static class DemoEventListener implements IEslEventListener {

        @Override
        public void eventReceived(EslEvent event) {
            System.out.println("eventReceived:" + event.getEventName());
        }

        @Override
        public void backgroundJobResultReceived(EslEvent event) {
            System.out.println("backgroundJobResultReceived:" + event.getEventName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        String host = "localhost";
        int port = 8021;
        String password = "ClueCon";
        int timeoutSeconds = 10;
        Client inboundClient = new Client();
        try {
            inboundClient.connect(host, port, password, timeoutSeconds);
            inboundClient.addEventListener(new DemoEventListener());
            inboundClient.cancelEventSubscriptions();
            inboundClient.setEventSubscriptions("plain", "all");
        } catch (Exception e) {
            System.out.println("connect fail");
        }

        while (true) {
            System.out.println(System.currentTimeMillis() + " " + inboundClient.canSend());
            if (!inboundClient.canSend()) {
                try {
                    //重連
                    inboundClient = new Client();
                    inboundClient.addEventListener(new DemoEventListener());
                    inboundClient.connect(host, port, password, timeoutSeconds);
                    inboundClient.cancelEventSubscriptions();
                    inboundClient.setEventSubscriptions("plain", "all");
                } catch (Exception e) {
                    System.out.println("connect fail");
                }
            }
            Thread.sleep(200);
        }
    }
}

代碼很簡單,先連上,然后用一個循環不停檢測canSend(),發現"斷開"了,就重連。 

參考上圖,在if條件這行打一個斷點,然后利用調試工具,在斷點處,強制把inboundClient.authenticated改成false(不清楚該調試技巧的同學,可參考之前的舊文idea 高級調試技巧),同時打開一個終端窗口,在程序運行前、斷點修改前、斷點修改並完成connect后,分別用lsof -i:8021觀察下本機的連接情況

如上圖:
1) 程序運行前,只有一個freeswitch在監聽本機的8021端口
2) 啟用成功后,在斷點修改前,java進程13516,建立了1個連接(對應的隨機端口號為58825)
3) 斷點修改后,繼續運行到connect后,還是13516進程,又建立了1個連接(對應的隨機端口號為58857),而之前的舊連接(58825)並沒有釋放,哪怕這里我用new Client()生成了一個全新的實例,舊實例關聯的連接資源仍然在!
4) 繼續這樣操作,會發現每次都會創建1個新鏈接,而原來的鏈接依然存在。

解決方法:重連先調用channel.close()方法,關閉channel,可以在源碼中,加一個方法closeChannel

    /**
     * close netty channel
     *
     * @return
     */
    public ChannelFuture closeChannel() {
        if (channel != null && channel.isOpen()) {
            return channel.close();
        }
        return null;
    }

然后connect開頭那段檢測改成:

        // If already connected, disconnect first
        if (canSend()) {
            close();
        } else {
            //canSend()=false but channel is still opened or connected
            closeChannel();
        }

這里說點題外話,channel類有isOpen、isConnected 二個方法,另外還有close()及disconnect()方法,有啥區別?

isOpen=true時,該channel可write,但是不能read (即:打開,但是沒連網)
isConnected=true,該channel可read/write(即:真正連上了網),換句話說:isOpen=true,未必isConnected=true,但是isConnected=true,isOpen必須為true.

這里我們旨在重連前釋放channel的所有資源,所以用close更徹底點。

 

再來看看內存泄露的問題,這個問題其實已經有網友記錄過了,大致原因是netty底層大量使用了DirectByteBuffer,這是直接在堆外分配的(即:堆外內存),不會被GC自動回收,如果代碼處理不當,多次調用connect()時,就有可能內存泄露。按該網友的建議,改成static靜態實例后,保證只有1個實例就可以了。細節不多說,代碼最后會給出,這里談另一個問題:

這里使用的是newCachedThreadPool方法,查看該方法源碼可知:

線程池的最大線程數是MAX_VALUE,相當於沒有上限,如果異常情況下,線程會一直上漲,直到資源用完, 最好換成明確有上限的寫法。

另外,還有1個細節問題,Client只提供了添加事件監控的方法:

    public void addEventListener(IEslEventListener listener) {
        if (listener != null) {
            eventListeners.add(listener);
        }
    }

但卻沒有提供移除的方法,如果重連時,無意重復調用了該方法,同樣的事件(即:同一個listener重復注冊),就會處理多次,可以新增一個清空方法,每次重連前,最好調用一下:

    /**
     * remove all eslEventlistener
     */
    public void removeAllEventListener() {
        if (eventListeners != null) {
            eventListeners.clear();
        }
    }

以上修改已經提交到github,需要的朋友可參考https://github.com/yjmyzz/esl-client/tree/0.9.x


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM