Java ESL 是一个用于与 FreeSWITCH 进行交互的 Java 开发库,它基于 ESL(Event Socket Library)协议,通过与 FreeSWITCH 的 ESL 服务器建立连接,实现了底层的事件通知和控制。
本文将介绍如何使用 官方提供的Java ESL库实现与FreeSwitch的沟通。
第一:准备工作
在开始使用 Java ESL 前,需要先确保以下几个条件已满足:
1,安装 FreeSWITCH,并启动 ESL 服务器。
2,安装 Java 开发环境,并配置好相关环境变量。
3,下载并导入 Java ESL 开发库。
第二:项目主要依赖库
1,org.freeswitch.esl.client-0.9.2.jar
2,netty-3.2.1.Final.jar for async socket comms
3,slf4j-api-1.6.1.jar for logging
第三:程序实现
1,导入依赖:
<dependency>
<groupId>org.freeswitch.esl.client</groupId>
<artifactId>org.freeswitch.esl.client</artifactId>
<version>0.9.2</version>
</dependency>
2,代码实现
本文主要通过esl inbound方式连接到freeswitch,然后通过IEslEventListener监听各类通道事件。
package org.freeswitch.esl.client.sample.example.inbound;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.freeswitch.esl.client.transport.event.EslEvent;
/**
* @author 长生果
*/
public class InboundExampleApp {
public static void main(String[] args) throws InterruptedException {
Client client = new Client();
try {
//客户端连接FS服务器
client.connect("localhost", 8021, "ClueCon", 10);
//注册监听
client.addEventListener(new InboundEventListener());
//监听各种事件
client.setEventSubscriptions("plain", "all");
} catch (InboundConnectionFailure inboundConnectionFailure) {
System.out.println("连接失败!");
inboundConnectionFailure.printStackTrace();
}
//health-check:客户端断链检测
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(1);
service.scheduleAtFixedRate(() -> {
System.out.println(System.currentTimeMillis() + " " + inboundClient.canSend());
if (!inboundClient.canSend()) {//如果短链,就重新链接
try {
//重连
inboundClient.connect(host, password, timeoutSeconds);
//取消订阅的事件
inboundClient.cancelEventSubscriptions();
//重新订阅freeSwitch事件
inboundClient.setEventSubscriptions(IModEslApi.EventFormat.PLAIN, "all");
} catch (Exception e) {
System.out.println("connect fail");
}
}else{//发送心跳包
//inboundClient.sendSyncApiCommand();
}
}, 1, 300000, TimeUnit.MILLISECONDS);
//异步发送指令
try{
//这里必须检查,防止网络抖动时,连接断开
if (client.canSend()) {
//(异步)向1004用户发起呼叫,用户接通后,后台播放音乐/tmp/demo1.wav
String callResult = client.sendAsyncApiCommand("originate", "user/1004 &playback(test.wav)");
System.out.println("api uuid:" + callResult);
}
}catch(EXception e){
e.printStackTrace();
}
}
}
InboundEventListener主要实现FS的事件处理,具体内容如下:
package org.freeswitch.esl.client.sample.example.inbound;
import org.freeswitch.esl.client.inbound.IEslEventListener;
import org.freeswitch.esl.client.internal.Context;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslHeaders;
import java.util.Map;
/**
*
* 主要是简单demo实现,将来可以再此基础上进行无限扩展
*
* @author 常生果
* @date 2024-08-05
*
* @des 主要处理事件监听程序,将来通过各种方式监听通话等的各种状态
*
* **/
public class InboundEventListener implements IEslEventListener {
public void onEslEvent(Context ctx, EslEvent event){
String eventName = event.getEventName();
String calleeNumber = event.getEventHeaders().get("Caller-Callee-ID-Number");
String callerNumber = event.getEventHeaders().get("Caller-Caller-ID-Number");
switch (eventName) {
case "HEARTBEAT"://心跳事件
break;
case "CHANNEL_CREATE"://创建事件,发起呼叫
System.out.println("CHANNEL_CREATE——发起呼叫, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_BRIDGE"://用户转接,一个呼叫两个端点之间的桥接事件
System.out.println("CHANNEL_BRIDGE——用户转接, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_ANSWER"://呼叫应答事件。
System.out.println("CHANNEL_ANSWER——用户应答, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_HANGUP": {//挂机事件
String response = event.getEventHeaders().get("variable_current_application_response");
String hangupCause = event.getEventHeaders().get("Hangup-Cause");
System.out.println("CHANNEL_HANGUP——用户挂断, 主叫:" + callerNumber + " , 被叫:" + calleeNumber + " , response:" + response + " ,hangup cause:" + hangupCause);
break;
}
case "CHANNEL_HANGUP_COMPLETE": {//挂机完成事件
String response = event.getEventHeaders().get("variable_current_application_response");
String hangupCause = event.getEventHeaders().get("Hangup-Cause");
System.out.println("CHANNEL_HANGUP_COMPLETE——用户挂断, 主叫:" + callerNumber + " , 被叫:" + calleeNumber + " , response:" + response + " ,hangup cause:" + hangupCause);
break;
}
case "PLAYBACK_START"://背景音乐播放
System.out.println("--音乐播放-开始!--");
break;
case "PLAYBACK_STOP"://背景音乐结束
System.out.println("--音乐播放-结束!--");
break;
case "CODEC"://编码协商
System.out.println("--CODEC--");
break;
case "RE_SCHEDULE"://心跳相关
System.out.println("--RE_SCHEDULE--");
break;
case "CALL_UPDATE"://更新呼叫
System.out.println("--CALL_UPDATE--");
break;
case "PRESENCE_IN"://
System.out.println("--PRESENCE_IN--");
break;
case "MESSAGE_QUERY"://
System.out.println("--MESSAGE_QUERY--");
break;
case "MESSAGE_WAITING"://
System.out.println("--MESSAGE_WAITING--");
break;
default:
System.out.println("事件:" + eventName);
Map<String, String> eventHeaders = event.getEventHeaders();
System.out.println("--------eventHeaders:-------");
if(eventHeaders!=null){
eventHeaders.forEach((key,value) ->{
System.out.println(" "+eventName+" Headers "+key+":"+value);
});
}
Map<EslHeaders.Name, String> messageHeaders = event.getMessageHeaders();
System.out.println("--------messageHeaders:-------");
if(messageHeaders!=null){
messageHeaders.forEach((key,value) ->{
System.out.println(" "+eventName+" message "+key+":"+value);
});
}
break;
}
}
}
说明:可以发送同步命令和异步命令两种方式。
1,异步发送指令:sendAsyncApiCommand
2,同步发送指令:sendSyncApiCommand
3,连接fs的用户名、密码、端口,可以在freeswitch安装目录下conf/autoload_configs/event_socket.conf.xml
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<param name="nat-map" value="false"/>
<param name="listen-ip" value="0.0.0.0"/>
<param name="listen-port" value="8021"/>
<param name="password" value="ClueCon"/>
<!--<param name="apply-inbound-acl" value="loopback.auto"/>-->
<!--<param name="stop-on-bind-error" value="true"/>-->
</settings>
</configuration>
4,异步命令是否发成功当时并不知道,但是会返回一个uuid的字符串,fs收到后,会在backgroundJobResultReceived回调中,把这个uuid再还回来,参见上面贴出的输出结果。(基于这个机制,可以做些重试处理,比如:先把uuid存下来,如果约定的时间内,uuid异步回调还没回来,可以视为发送失败,再发一次)
5,inbound模式下,0.9.2这个版本,长时间使用有内存泄露问题,网上有很多这个介绍及修复办法,建议生产环境使用前,先修改esl client的源码。
第四、下载 java esl-client
git cloen https://github.com/esl-client/esl-client.git
因为0.9.2版本有内存泄漏风险,下章将介绍主要泄露问题,以及需要对源码进行修改。
一、内存泄露
org.freeswitch.esl.client.transport.message.EslFrameDecoder 这个类,使用了netty的ByteBuf,对netty有了解的同学应该知道,netty底层大量使用了堆外内存,建议开发人员及时手动释放。
case READ_BODY:
/*
* read the content-length specified
*/
int contentLength = currentMessage.getContentLength();
ByteBuf bodyBytes = buffer.readBytes(contentLength);
if(SetingConf.LOG_ON){
log.debug("read [{}] body bytes", bodyBytes.writerIndex());
}
// most bodies are line based, so split on LF
while (bodyBytes.isReadable()) {
String bodyLine = readLine(bodyBytes, contentLength);
if(SetingConf.LOG_ON){
log.debug("read body line [{}]", bodyLine);
}
currentMessage.addBodyLine(bodyLine);
}
/**
*
*内存泄露的主要地方
*-------------------
*使用后及时释放,否则会引起内存泄露
*一定要手动释放!
*
**/
try {
if (bodyBytes.refCnt() == 1) {
bodyBytes.release();
bodyBytes = null;
}
} catch (Exception ex) {
ex.printStackTrace();
}
// end of message
checkpoint(State.READ_HEADER);
// send message upstream
EslMessage decodedMessage = currentMessage;
currentMessage = null;
out.add(decodedMessage);
break;
如果不及时释放,长时间运行后,后台会报错误:
[2024-8-11 17:31:47.436]-[192.168.1.8]-[ERROR]-[nioEventLoopGroup-2-1]-[io.netty.util.ResourceLeakDetector]-[12456]-[Slf4JLogger.java:171]-[LEAK: ByteBuf.release() was not called before it's garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information.Recent access records: #1:
io.netty.buffer.AdvancedLeakAwareByteBuf.readByte(AdvancedLeakAwareByteBuf.java:400)
org.freeswitch.esl.client.transport.message.EslFrameDecoder.readLine(EslFrameDecoder.java:184)
org.freeswitch.esl.client.transport.message.EslFrameDecoder.decode(EslFrameDecoder.java:143)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489)
io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:367)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:645)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:580)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:497)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:459)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:886)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:748)#2:
io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:604)
io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:849)
io.netty.buffer.WrappedByteBuf.readBytes(WrappedByteBuf.java:616)
io.netty.buffer.AdvancedLeakAwareByteBuf.readBytes(AdvancedLeakAwareByteBuf.java:473)
io.netty.handler.codec.ReplayingDecoderByteBuf.readBytes(ReplayingDecoderByteBuf.java:577)
org.freeswitch.esl.client.transport.message.EslFrameDecoder.decode(EslFrameDecoder.java:139)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489)
io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:367)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:645)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:580)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:497)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:459)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:886)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:748)Created at: io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:331)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:185)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:121)
io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:848)
io.netty.buffer.WrappedByteBuf.readBytes(WrappedByteBuf.java:616)
io.netty.buffer.AdvancedLeakAwareByteBuf.readBytes(AdvancedLeakAwareByteBuf.java:473)
io.netty.handler.codec.ReplayingDecoderByteBuf.readBytes(ReplayingDecoderByteBuf.java:577)
org.freeswitch.esl.client.transport.message.EslFrameDecoder.decode(EslFrameDecoder.java:139)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489)
io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:367)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:645)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:580)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:497)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:459)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:886)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:748): 44 leak records were discarded because they were duplicates: 44 leak records were discarded because the leak record count is targeted to 4. Use system property io.netty.leakDetection.targetRecords to increase the limit.]
2,重连后,要及时关闭channel链接,否则链接一直存在,消耗内存。
重连先调用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();
}
接下来咱们会通过netty 4.x版本重构ESL Java库,使其性能更流畅。