freeswitch笔记(8)-esl outbound 填坑笔记
github上的esl-client已经N年未更新了,上面有一堆bug,记录一下:
一、内存泄露
org.freeswitch.esl.client.transport.message.EslFrameDecoder 这个类,使用了netty的ByteBuf,对netty有了解的同学应该知道,netty底层大量使用了堆外内存,建议开发人员及时手动释放。
https://github.com/esl-client/esl-client/issues/24 也有记载
参考下图,手动加上释放处理即可

二、线程池优化

org.freeswitch.esl.client.outbound.OutboundChannelInitializer 这个类,每次freeswitch有来电时,会以outbound外联模式,通过tcp连接到esl client,初始化channel。callbackExector是一个单线程池,正常情况下问题倒不大,但是jdk源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
LinkedBlockingQueue默认是一个无界队列:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
有点风险,改成下面这样更安全点:
private ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("outbound-pool-%d").build(); public ExecutorService callbackExecutor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10000), namedThreadFactory);
这个单线程池的用法也顺带研究了下,它真正使用的地方在于org.freeswitch.esl.client.outbound.OutboundClientHandler,用于处理freeswitch发过来的事件
@Override
protected void handleEslEvent(final ChannelHandlerContext ctx, final EslEvent event) {
callbackExecutor.execute(() -> clientHandler.onEslEvent(
new Context(ctx.channel(), OutboundClientHandler.this), event));
}
大家知道Netty本身就有2个线程池:bossGroup,workerGroup,默认大小在io.netty.channel.MultithreadEventLoopGroup中
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
即:核数*2。 既然已经是线程池了,为啥这里esl的事件又单独交给1个单线程池来处理呢? 先来看OutboundChannelInitializer实例化的地方,在org.freeswitch.esl.client.outbound.SocketClient的doStart里
@Override
protected void doStart() {
final ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new OutboundChannelInitializer(clientHandlerFactory)); serverChannel = bootstrap.bind(bindAddress).syncUninterruptibly().channel();
notifyStarted();
log.info("SocketClient waiting for connections on [{}] ...", bindAddress);
}
也就是说,只有outbound tcp server启用时,才会对OutboundChannelInitializer做1次初始化,言外之意,刚才的单线程池实例也只会实例化1次。
试想一下,如果在outbound的处理过程中,一通电话进来,我们订阅了一堆事件,这堆事件发过来后,如果让workerGroup并行处理,事件的处理顺序就得不到保证了,这在电话系统中是很重要的,比如:响铃->接听->挂断。肯定要有顺序的!所以为了保证事件处理的顺序性,强制让所有事件,都交给这个单线程池实例来处理,保证了顺序性。
其实不光是outbound,inbound也是类似机制,保证事件接收时按顺序处理。明白这个原理后,回过头来想想,这个单线程池的callbackExector实例,应该处理成static静态实例更稳妥,这样强制让jvm保证肯定只有一个实例,处理事件绝对有顺序。
另外,在outbound的onConnect事件里,如果尝试跟freeswitch发命令,会发现block住,后面的代码完全无法执行,这也是一个大坑。解决办法:
将onConnect的处理,放在另外1个专用线程池里
class OutboundClientHandler extends AbstractEslClientHandler {
//这是保证事件接收顺序的单线程池
private final ExecutorService onEslEventExecutor;
//这是用于并发处理onConnect的多线程池
private final ExecutorService onConnectExecutor;
public OutboundClientHandler(IClientHandler clientHandler, ExecutorService onEslEventExecutor, ExecutorService onConnectExecutor) {
this.clientHandler = clientHandler;
//构造函数里允许传入
this.onEslEventExecutor = onEslEventExecutor;
this.onConnectExecutor = onConnectExecutor;
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
// Have received a connection from FreeSWITCH server, send connect response
long threadId = Thread.currentThread().getId();
log.debug("Received new connection from server, sending connect message,threadId:" + threadId);
sendApiSingleLineCommand(ctx.channel(), "connect")
.thenAccept(response ->
//这里改为线程池执行
onConnectExecutor.execute(() -> clientHandler.onConnect(
new Context(ctx.channel(), OutboundClientHandler.this),
new EslEvent(response, true)))
)
.exceptionally(throwable -> {
ctx.channel().close();
handleDisconnectionNotice();
return null;
});
}
@Override
protected void handleEslEvent(final ChannelHandlerContext ctx, final EslEvent event) {
//这里仍然用单一线程池处理,保证顺序
onEslEventExecutor.execute(() -> clientHandler.onEslEvent(
new Context(ctx.channel(), OutboundClientHandler.this), event));
}
...
}
然后
public class OutboundChannelInitializer extends ChannelInitializer<SocketChannel> {
private final IClientHandlerFactory clientHandlerFactory;
private static ThreadFactory onEslThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("outbound-onEsl-pool-%d").build();
//专门接收订阅事件的单一线程池(保证顺序)
private static ExecutorService onEslExecutor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100000), onEslThreadFactory);
private static ThreadFactory onConnectThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("outbound-onConnect-pool-%d").build();
//专用于处理新来电onConnect的多线程池
private static ExecutorService onConnectExecutor = new ThreadPoolExecutor(32, 512,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2048), onConnectThreadFactory);
public OutboundChannelInitializer(IClientHandlerFactory clientHandlerFactory) {
this.clientHandlerFactory = clientHandlerFactory;
}
/**
* 重载版本,允许开发人员初始化时,传入自己的线程池
* @param clientHandlerFactory
* @param connExecutor
* @param eslExecutor
*/
public OutboundChannelInitializer(IClientHandlerFactory clientHandlerFactory, ExecutorService connExecutor, ExecutorService eslExecutor) {
this.clientHandlerFactory = clientHandlerFactory;
onEslExecutor = eslExecutor;
onConnectExecutor = connExecutor;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// Add the text line codec combination first
pipeline.addLast("encoder", new StringEncoder());
// Note that outbound mode requires the decoder to treat many 'headers' as body lines
pipeline.addLast("decoder", new EslFrameDecoder(8092, true));
// now the outbound client logic
//将2个线程池,传入实例
pipeline.addLast("clientHandler",
new OutboundClientHandler(clientHandlerFactory.createClientHandler(), onEslExecutor, onConnectExecutor));
}
}
三、源码上的Test示例代码各种错误
https://github.com/esl-client/esl-client/blob/master/src/test/java/OutboundTest.java 这是示例源码
String uuid = eslEvent.getEventHeaders().get("unique-id");
45行,这里应该是"Unique-ID",小写取不到值。
另外82行,outbound的onEslEvent方法,其实永远也不会被触发,因为根本没订阅任何事件,inbound的示例部分也有同样问题。
56行,执行后,实测下来,后面的操作其实都是阻塞的,代码无法向下执行,建议改在新线程里执行(或者参考上面的“线程池优化”分析,修改源码)。
上述这些问题,笔者已经fork了一份代码进行了修改,有兴趣的同学,欢迎fork,地址:https://github.com/yjmyzz/esl-client
freeswitch笔记(8)-esl outbound 填坑笔记的更多相关文章
- 即将上线的YARN服务器面临的一系列填坑笔记
即将上线的YARN服务器面临的一系列填坑笔记 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 折腾了一个星期,终于让开发将数据跑起来了,可通过yarn的webUI界面,发现这里的核心 ...
- 即将上线的Hive服务器面临的一系列填坑笔记
即将上线的Spark服务器面临的一系列填坑笔记 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.18/10/19 16:36:31 WARN metastore.ObjectSt ...
- 即将上线的Spark服务器面临的一系列填坑笔记
即将上线的Spark服务器面临的一系列填坑笔记 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 把kafka和flume倒腾玩了,以为可以轻松一段时间了,没想到使用CDH部署的spa ...
- 即将上线的flume服务器面临的一系列填坑笔记
即将上线的flume服务器面临的一系列填坑笔记 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.flume缺少依赖包导致启动失败! 报错信息如下: 2018-10-17 ...
- 即将上线的Kafka服务器面临的一系列填坑笔记
即将上线的Kafka服务器面临的一系列填坑笔记 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Kafka日志报错:[error] k.m.j.KafkaJMX$ - Fai ...
- [react-native]react-native填坑笔记
填坑笔记 开始入坑RN,从最开始的学起难免有不少乱七八糟的问题,记录在这里. 1. 8081端口占用问题 按照官网教程搭建开发环境并按照下面代码运行时候有报错,显示8081端口的问题 react-na ...
- Win下Jenkins-2.138源码编译及填坑笔记
源码编译篇 1. 安装JDK1.8-181,操作系统添加JDK环境变量.Java -version验证一下. 注:Jenkins2.138版本,JDK必须jkd1.8.0-101以上,不支持Java ...
- H5填坑笔记--持续更新
最近一直在做移动端的页面,发现很多的坑,这里做一下总结,填填坑…… css常见的问题(一) 一.iOS键盘首字母自动大写 IOS的机子,默认英文输入法状态下,首字母是自动大写的,有时候挺烦人的. 在i ...
- 微信微信JS-SDK 6.0.2 填坑笔记
0.为什么以前不需要配置这么麻烦就可以修改分享description 等信息,但是现在不行了. 因为6.0.2版本之前没有做权限验证,所以config都是ok,但这并不意味着你config中的签名是O ...
- Flutter配置签名打包全流程填坑笔记
1.配置包名和版本 找到android-app-src-build.gradle文件在defaultConfig{...}中配置好版本号以及包名 2.生成key 将keytool路径添加进环境变量,c ...
随机推荐
- 【经验】VScode 远程 SSH 连接 Ubuntu 或 TrueNas 出错,Could not establish connection
用VScode常常会碰到以下情况,Could not establish connection. 先介绍一下VScode远程连接和终端SSH连接的区别:终端直接用SSH连接时,只需要开启SSH服务,并 ...
- 【笔记】Python3|使用 PyVis 完成神经网络数据集的可视化
文章目录 版本: 应用实例: 1 神经网络可视化 2 别人的示例和代码 PyVis的应用: 零.官方教程 一.初始化画布`Network` 二.添加结点 添加单个结点`add_node`: 添加一系列 ...
- Java的"伪泛型"变"真泛型"后,会对性能有帮助吗?
泛型存在于Java源代码中,在编译为字节码文件之前都会进行泛型擦除(type erasure),因此,Java的泛型完全由Javac等编译器在编译期提供支持,可以理解为Java的一颗语法糖,这种方式实 ...
- P1514 [NOIP 2010 提高组] 引水入城 题解
题意:P1514 [NOIP 2010 提高组] 引水入城有点复杂,自己看吧. 思路 这里提供一个好像没见过的纯 DP 做法,不需要神秘的证明以及任何脑子,直接顺着思路做即可. 首先判断正确性就是从第 ...
- Vue3 学习-初识体验-helloworld
在数据分析中有一个最重要的一环就是数据可视化, 数据报表的开发. 从我从业这几年的经历上看, 经历了从业务系统导表格数据, 到Excel+PPT, 再是开源报表工具, 再是主流商业BI产品(低/零代码 ...
- 赛前十天——递归(easy)
*理论上,递归与循环是等价的,即任何循环都可以重写为递归形式 eg: package javaPractice; public class Contest { public stati ...
- kafka怎么查看topic的消费进度offset
下载开源的 kafka 界面客户端 KafkaKing:https://github.com/Bronya0/Kafka-King 在成功下载该客户端后,进行连接操作.连接完毕后,切换到 topic ...
- .NET 开源工业视觉系统 OpenIVS 快速搭建自动化检测平台
前言 随着工业4.0和智能制造的发展,工业视觉在质检.定位.识别等场景中发挥着越来越重要的作用.然而,开发一个完整的工业视觉系统往往需要集成相机控制.图像采集.图像处理.AI推理.PLC通信等多个模块 ...
- 50道常见Redis面试题,干货汇总
哪些大厂在使用Redis?github.twitter.微博.Stack Overflow.百度.阿里巴巴.美团和搜狐等都在用,所以今天小编当作搬运工,为大家整理了一份Redis面试题,合计50个 ...
- Winform高级技巧-界面和代码分离的实践案例
办法总比困难多(不会或不想用WPF MVVM模式也可以搞工控自动化和上位机编程)... 正文 活到老就该学到老.但是难道我们不可以偷懒么,老技术指哪打哪,游刃有余,如果已经炉火纯青,那么解决问题又快又 ...