代码改变世界
[登录 · 注册]
  • 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

  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文:https://www.cnblogs.com/yjmyzz/p/esl-inbound-client-connection-and-memory-leak-problem.html
走看看 - 开发者的网上家园