FreeSwitch与Java通信ESL

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,导入依赖:

```java

    org.freeswitch.esl.client
    org.freeswitch.esl.client
    0.9.2

```

2,代码实现

本文主要通过esl inbound方式连接到freeswitch,然后通过IEslEventListener监听各类通道事件。

```java
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的事件处理,具体内容如下:

```java
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 eventHeaders = event.getEventHeaders();
                System.out.println("--------eventHeaders:-------");
                if(eventHeaders!=null){
                    eventHeaders.forEach((key,value) ->{
                        System.out.println(" "+eventName+" Headers "+key+":"+value);
                    });
                }
                Map  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

```java

  
    
    
    
    
    
    
  


```

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底层大量使用了堆外内存,建议开发人员及时手动释放。

```java
  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;

```

如果不及时释放,长时间运行后,后台会报错误:

```java
[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

```java
    /**
     * close netty channel
     *
     * @return
     */
    public ChannelFuture closeChannel() {
        if (channel != null && channel.isOpen()) {
            return channel.close();
        }
        return null;
    }
```

然后connect开头那段检测改成:

```java
        // If already connected, disconnect first
        if (canSend()) {
            close();
        } else {
            //canSend()=false but channel is still opened or connected
            closeChannel();
        }
```

接下来咱们会通过netty 4.x版本重构ESL Java库,使其性能更流畅。

文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/6697.html

(0)
LomuLomu
上一篇 2025 年 1 月 17 日
下一篇 2025 年 1 月 17 日

相关推荐

  • Java难绷知识02——抽象类中只能有或者必须有抽象方法吗以及有关抽象类的细节探讨

    Java难绷知识02——抽象类中只能有或者必须有抽象方法吗以及有关抽象类的细节探讨 标题长的像轻小说 首先回答标题抛出的问题——False 显然,有抽象方法的类是抽象类,但是,抽象类中只能有或者必须有抽象方法吗? 抽象类可以包含抽象方法,也可以包含具体方法 如果一个类包含至少一个抽象方法,用abstract关键字修饰,那么这个类必须被声明为抽象类。 抽象类除…

    未分类 2024 年 12 月 31 日
    10500
  • Discord技术架构调研(IM即时通讯技术架构分析)

    一、目标 调研 discord 的整体架构,发掘可为所用的设计思想 二、调研背景 Discord作为目前比较火的一个在线聊天和语音通信平台且具有丰富的功能。另外其 “超级”群 概念号称可支持百万级群聊 以及 永久保留用户聊天记录。探究其相关技术架构与技术实现 三、产品介绍 目前广泛使用的在线聊天和语音通信平台。最初于2015年发布,旨在为游戏社区提供一个交流…

    2025 年 1 月 14 日
    9900
  • MySQL

    阿里云社区https://developer.aliyun.com/mirror 目录 一:数据库 1.1 二: MySQL数据库基本操作 2.1 创建数据库: 2.2 使用某个数据库: 2.3 删除数据库: 2.4 查询支持的存储引擎 2.5 创建表: 2.6 查看表结构: 2.7 查看表结构详细信息: 2.8 删除表: 三:表的操作 3.1 修改表名字:…

    未分类 2025 年 1 月 12 日
    12200
  • python常用模块

    re模块 正则表达式符号: 表达符号 说明 . 匹配所有字符串,除\n以外 – 表示范围[0-9] * 1.匹配前面的子表达式零次或多次,匹配前面的字符0次或多次 2.re.findall(“ab*”,“cabc3abcbbac”)结果:[‘ab’, ‘ab’, ‘a’] + 匹配前面的子表达式一次或多次 ^ 匹配字符串开头 $ 匹配字符串结尾 \ 转义字符…

    未分类 2024 年 12 月 29 日
    17400
  • 通过延时从库+binlog复制,恢复误操作数据

    通过延迟复制与binlog恢复意外删除的数据 一、环境概述 以下是我们操作的数据库环境的详细信息: 数据库版本 实例角色 IP地址 端口 GreatSQL 8.0.32-26 主库 192.168.134.199 5725 GreatSQL 8.0.32-26 从库 192.168.134.199 5726 二、主库设置 在主库上,我们首先需要创建一个复制用…

    2024 年 12 月 24 日
    13100

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信