专栏首页菩提树下的杨过freeswitch笔记(4)-esl inbound模式的重连及内存泄露问题

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 条评论
登录 后参与评论

相关文章

  • 全世界最短IE判定if(!+[1,])的解释

    虽然从司徒先生的博客上看到 全世界最短的IE判定 很长时间了,却一直对于原理没怎么去细看,今天同事(也是一后台程序员,并非前端)又问到这个问题,于是我这个前端外...

    菩提树下的杨过
  • silverlight中顺序/倒序异步加载多张图片

    相册/图片切换广告等很多常用小应用中,服务器返回一组图片的uri,然后silverlight利用WebClient异步加载,如果要严格控制加载顺序的话,可以利用...

    菩提树下的杨过
  • [基础]Javascript中的继承示例代码

    面向对象的语言必须具备四个基本特征: 1.封装能力(即允许将基本数据类型的变量或函数放到一个类里,形成类的成员或方法) 2.聚合能力(即允许类里面再包含类,...

    菩提树下的杨过
  • {防抖}与{节流}

    每次onscroll的时候,先清除掉计时器.如果不清楚,会导致多次触发的时候,其实是把好多次的处理方法放在某个时间点后一起执行。

    Jean
  • R语言爬虫与文本分析

    之前用python做过简单的爬虫与分析,今天尝试一下用R完成相应的功能。首先用R爬取了《了不起的麦瑟尔夫人》豆瓣短评作为语料,然后进行了词云绘制、关键词提取的基...

    三猫
  • 百度APP移动端网络深度优化实践分享(一):DNS优化篇

    网络优化是客户端几大技术方向中公认的一个深度领域,所以百度App给大家带来网络深度优化系列文章。

    JackJiang
  • 模拟退火算法从原理到实战【基础篇】

      模拟退火算法来源于固体退火原理,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达...

    Angel_Kitty
  • 干货 | 国家信息中心杜平谈关于数字化的几点思考

    [ 导读 ]清华大学109周年校庆之际,清华校友总会软件学院分会、大数据系统软件国家工程实验室和清华大学大数据研究中心共同主办了以“软件定义新基建,数据驱动新未...

    数据派THU
  • 全球币圈首个实盘交易大赛?奖池100个BTC+亿万STC!请开始你的表演!

    最近关注到一个刷遍了半个圈内媒体的活动【STC全球币圈首个实盘交易大赛】,奖池足足有100个BTC+亿万STC大奖,在这个行情大寒冬的时期,是哪个项目方出来搞事...

    区块链领域
  • 分布式作业系统 Elastic-Job-Lite 源码分析 —— 运维平台

    本文主要分享 Elastic-Job-Lite 运维平台。内容对应《官方文档 —— 运维平台》。

    芋道源码

扫码关注云+社区

领取腾讯云代金券