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
