FreeSWITCH与Java交互实战:从EslEvent解析到Spring Boot生态整合的全指南

#【Code实战派】技术分享征文挑战赛#

其中:

  • 实时控制:ESL 事件与命令(核心)。
  • 媒体处理:音频流、传真文件(需专用模块)。
  • 状态管理:通道变量、全局状态 API。
  • 持久化数据:CDR 话单、数据库存储。
  • 扩展集成:REST/XML-RPC/Kafka 适配第三方系统。

以下主要介绍核心的事件交互,接口话单交互在写话单的章节已经有所描述,其余数据库、队列为媒介的交互,在后续章节会详细介绍。

📡 一、EslEvent 对象能获取的信息类型

  • 传递内容:系统状态变更(如通话开始/结束)、通道变量、自定义消息(如 CUSTOM 事件)。
  • 典型场景:实时监控通话状态、触发业务流程(如来电弹屏)。
  • 协议/机制:ESL(Event Socket Library)的 plain/json/xml 格式
1. 核心呼叫元数据(最常用)
字段名示例值说明
Caller-Caller-ID-Name"John Doe"主叫名称
Caller-Destination-Number1000被叫号码
Caller-ANI13800138000主叫号码(ANI)
Hangup-CauseNORMAL_CLEARING挂机原因代码
variable_billsec120计费时长(秒)
2. 通道状态信息
{
  "Channel-State": "CS_EXECUTE",  // 通道状态
  "Channel-Call-State": "ACTIVE", // 呼叫状态
  "Answer-State": "answered"      // 应答状态
}
3. SIP摘要信息(非原始信令)
字段名说明
variable_sip_h_X-Header自定义SIP头 (如 X-Campaign-ID)
variable_sip_contact_userContact头中的用户部分
variable_sip_via_proxy经过的SIP代理地址
4. 媒体信息
{
  "variable_rtp_use_codec_name": "PCMA",  // 使用编解码
  "variable_rtp_audio_in_media_port": "16384" // RTP端口
}

🛠️ 二、对应用开发的实用价值

通常情况下,中小型企业,有高性能的DB支撑,没有严格的上下游要求,仅是freeswitch xml配置所能实现的功能,就足以支持业务需求。但是当企业达到一定规模,从业务性能、定时化功能、业务监控等多维度出发,都需要与Java服务进行交互,实现更复杂的业务逻辑。

1. 实时呼叫监控仪表盘
// 监听CHANNEL_CREATE事件构建呼叫看板
event.getEventHeaders().forEach((k,v) -> {
  if(k.startsWith("Caller-")) {
    dashboard.updateCall(k, v); 
  }
});
2. 挂机原因分析(优化IVR)
if("CHANNEL_HANGUP".equals(eventName)){
  String cause = event.getHeader("Hangup-Cause");
  stats.logAbandonment(cause); // 统计用户放弃原因
}
3. 动态路由决策
// 根据主叫号码前缀路由
String ani = event.getHeader("Caller-ANI");
if(ani.startsWith("800")) {
  originateTollFreeCall(ani); 
}
4. 计费系统集成
// 通话结束时获取计费信息
int billsec = Integer.parseInt(event.getHeader("variable_billsec"));
billing.chargeCall(billsec);
5. 自定义业务逻辑触发
// 检测自定义SIP头触发营销动作
if(event.containsHeader("variable_sip_h_X-Promo-Code")){
  promo.activate(event.getHeader("variable_sip_h_X-Promo-Code"));
}

三、FreeSWITCH ESL客户端库

以下主要针对JAVA对接的方式,介绍几种可用的客户端库,能够不用自行根据netty实现,复用轮子,这几种方法使用起来都算简便,具体看各个项目情况进行选用

  • esl-client的Netty 4.x改造版: https://github.com/esl-client/esl-client
  • link.thingscloud/freeswitch-esl:https://github.com/zhouhailin/freeswitch-externals/tree/2.2.0/freeswitch-esl
  • link.thingscloud/freeswitch-esl-spring-boot-starter:https://github.com/zhouhailin/freeswitch-externals/tree/2.2.0/freeswitch-esl-spring-boot-starter
前提: 在 freeswitch 中配置开启event_socket
  • modules.conf中需要编译 event_handlers/mod_event_socket,引入后重新编译
  • 配置 /usr/local/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="lan"/>
    <!--<param name="stop-on-bind-error" value="true"/>-->
  </settings>
</configuration>                 
⚙️ 1. 核心库对比:esl-client(Netty 4.x改造版) vs link.thingscloud/freeswitch-esl
特性esl-client(Netty 4.x改造版)link.thingscloud/freeswitch-esl
技术基础基于官方org.freeswitch.esl.client升级Netty 4.x完全重写,原生Netty 4实现,深度优化线程模型
资源泄漏修复✅ 修复Netty 3.x的线程泄漏问题(需手动合并代码)✅ 原生规避Netty 3缺陷,无资源泄漏风险
集群支持❌ 仅支持单节点连接✅ 动态管理多节点(addServerOption/removeServerOption
连接管理基础连接池,需自行封装内置智能重连、心跳保活、故障自动切换
维护状态社区非官方分支,更新不稳定活跃维护(2024年仍有更新,版本迭代至2.2.0)
性能监控❌ 无✅ 支持事件处理耗时统计(performanceCostTime

关键结论

  • 稳定性优先 → 选link.thingscloud/freeswitch-esl:企业级功能+长期维护。
  • 兼容旧项目 → 可尝试esl-client改造版,但需自行解决集群等扩展需求。
esl-client
public class EslInboundClientExample {

    /**
     * <p>main.</p>
     *
     * @param args an array of {@link java.lang.String} objects.
     */
    public static void main(String[] args) {
        InboundClientOption option = new InboundClientOption();

        option.defaultPassword("ClueCon")
                .addServerOption(new ServerOption("127.0.0.1", 8021));
        option.addEvents("all");

        option.addListener(new IEslEventListener() {
            @Override
            public void eventReceived(String addr, EslEvent event) {
                System.out.println(addr);
                System.out.println(event);
            }

            @Override
            public void backgroundJobResultReceived(String addr, EslEvent event) {
                System.out.println(addr);
                System.out.println(event);
            }
        });

        option.serverConnectionListener(new ServerConnectionListener() {
            @Override
            public void onOpened(ServerOption serverOption) {
                System.out.println("---onOpened--");
            }

            @Override
            public void onClosed(ServerOption serverOption) {
                System.out.println("---onClosed--");
            }
        });

        InboundClient inboundClient = InboundClient.newInstance(option);

        inboundClient.start();


        System.out.println(option.serverAddrOption().first());
        System.out.println(option.serverAddrOption().last());
        System.out.println(option.serverAddrOption().random());


    }

}
link.thingscloud/freeswitch-esl
public class ClientExample {
    private static final Logger L = LoggerFactory.getLogger(ClientExample.class);

    public static void main(String[] args) {
        try {
            if (args.length < 1) {
                System.out.println("Usage: java ClientExample PASSWORD");
                return;
            }

            String password = args[0];

            Client client = new Client();

            client.addEventListener((ctx, event) ->{
                L.info("Received event:{} ====================",event.getEventName());
            });

            client.connect(new InetSocketAddress("127.0.0.1", 8021), password, 10);
            client.setEventSubscriptions(EventFormat.PLAIN, "all");

        } catch (Throwable t) {
            Throwables.propagate(t);
        }
    }
}
🌱 2. Spring生态整合:freeswitch-esl-spring-boot-starter的核心优势

开箱即用配置

# application.yml
link:
  thingscloud:
    freeswitch:
      esl:
        inbound:
          defaultPassword: ClueCon
          performance: false
          performanceCostTime: 200
          servers:
            - host: fs1.example.com
              port: 8021
              password: ClueCon
            - host: 127.0.0.1
              port: 8021              
          events: CHANNEL_CREATE, CHANNEL_DESTROY  # 按需订阅事件,all是订阅所有

接收并处理某个event:

@Slf4j
@Component
@EslEventName(EventNames.HEARTBEAT)
public class HeartbeatEslEventHandler implements EslEventHandler {
    /**
     * {@inheritDoc}
     */
    @Override
    public void handle(String addr, EslEvent event) {
        log.info("HeartbeatEslEventHandler handle addr[{}] EslEvent[{}].", addr, event);
    }
}

一个简单的对话:
在这里插入图片描述

捕获 all 的 ESL EVENT 样例:

2025-08-01 14:40:00.123  INFO 92506 --- [licExecutor-1-8] l.t.f.e.s.b.s.e.HeartbeatEslEventHandler : HeartbeatEslEventHandler handle addr[127.0.0.1:8021] EslEvent[EslEvent: name=[HEARTBEAT] headers=2, eventHeaders=28, eventBody=0 lines.].
2025-08-01 15:24:53.478  WARN 58180 --- [licExecutor-1-4] l.t.f.e.s.b.s.h.DefaultEslEventHandler   : Default esl event handler handle addr[127.0.0.1:8021], event[
#
## message header : 
CONTENT_TYPE=text/event-plain
CONTENT_LENGTH=1781
## event header : 
=
Core-UUID=f2e59381-6fe5-4603-b5f2-063f97340f50
Event-Calling-Line-Number=2408
FreeSWITCH-Hostname=opensips
Caller-Unique-ID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Channel-Progress-Media-Time=0
FreeSWITCH-IPv6=::1
Caller-Caller-ID-Number=0000000000
FreeSWITCH-IPv4=127.0.0.1
Caller-Destination-Number=1000
Caller-Channel-Answered-Time=0
Channel-State=CS_CONSUME_MEDIA
Channel-HIT-Dialplan=false
Caller-Channel-Last-Hold=0
Caller-Callee-ID-Name=Outbound Call
Event-Date-Timestamp=1754033093652047
Channel-State-Number=7
Caller-Callee-ID-Number=1000
Caller-Channel-Name=sofia/internal/1000@127.0.0.1:5061
Presence-Call-Direction=outbound
Unique-ID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Direction=outbound
Event-Name=CHANNEL_STATE
Caller-Profile-Created-Time=1754033093652047
Channel-Call-State=DOWN
Caller-Screen-Bit=true
Caller-Logical-Direction=outbound
Event-Calling-File=switch_channel.c
Caller-Channel-Hold-Accum=0
Call-Direction=outbound
FreeSWITCH-Switchname=opensips
Caller-Channel-Progress-Time=0
Caller-Privacy-Hide-Name=false
Caller-Privacy-Hide-Number=false
Event-Date-Local=2025-08-01 15:24:53
Caller-Channel-Bridged-Time=0
Caller-Source=src/switch_ivr_originate.c
Event-Date-GMT=Fri, 01 Aug 2025 07:24:53 GMT
Answer-State=ringing
Caller-ANI=0000000000
Caller-Channel-Hangup-Time=0
Caller-Channel-Resurrect-Time=0
Caller-Orig-Caller-ID-Number=0000000000
Event-Calling-Function=switch_channel_perform_set_running_state
Caller-Channel-Transfer-Time=0
Caller-Profile-Index=1
Caller-Channel-Created-Time=1754033093652047
Event-Sequence=1150
Channel-Name=sofia/internal/1000@127.0.0.1:5061
Channel-Call-UUID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Context=default
## event body lines : 
#
]
2025-08-01 15:24:56.849  WARN 58180 --- [licExecutor-1-2] l.t.f.e.s.b.s.h.DefaultEslEventHandler   : Default esl event handler handle addr[127.0.0.1:8021], event[
#
## message header : 
CONTENT_TYPE=text/event-plain
CONTENT_LENGTH=1896
## event header : 
=
Core-UUID=f2e59381-6fe5-4603-b5f2-063f97340f50
Event-Calling-Line-Number=301
FreeSWITCH-Hostname=opensips
Caller-Unique-ID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Channel-Progress-Media-Time=0
FreeSWITCH-IPv6=::1
Caller-Caller-ID-Number=0000000000
FreeSWITCH-IPv4=127.0.0.1
Caller-Destination-Number=1000
Original-Channel-Call-State=DOWN
Caller-Channel-Answered-Time=0
Channel-State=CS_CONSUME_MEDIA
Channel-HIT-Dialplan=false
Caller-Channel-Last-Hold=0
Caller-Callee-ID-Name=Outbound Call
Event-Date-Timestamp=1754033097012096
Channel-State-Number=7
Caller-Callee-ID-Number=1000
Caller-Channel-Name=sofia/internal/1000@127.0.0.1:5061
Presence-Call-Direction=outbound
Unique-ID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Direction=outbound
Event-Name=CHANNEL_CALLSTATE
Caller-Profile-Created-Time=1754033093652047
Channel-Call-State=RINGING
Caller-Screen-Bit=true
Caller-Logical-Direction=outbound
Event-Calling-File=switch_channel.c
Caller-Channel-Hold-Accum=0
Call-Direction=outbound
FreeSWITCH-Switchname=opensips
Caller-Channel-Progress-Time=1754033097012096
Caller-Privacy-Hide-Name=false
Caller-Privacy-Hide-Number=false
Event-Date-Local=2025-08-01 15:24:57
Caller-Channel-Bridged-Time=0
Caller-Source=src/switch_ivr_originate.c
Event-Date-GMT=Fri, 01 Aug 2025 07:24:57 GMT
Answer-State=ringing
Caller-ANI=0000000000
Caller-Channel-Hangup-Time=0
Caller-Channel-Resurrect-Time=0
Caller-Network-Addr=127.0.0.1
Channel-Call-State-Number=2
Caller-Orig-Caller-ID-Number=0000000000
Event-Calling-Function=switch_channel_perform_set_callstate
Caller-Channel-Transfer-Time=0
Caller-Profile-Index=1
Caller-Channel-Created-Time=1754033093652047
Event-Sequence=1154
Channel-Name=sofia/internal/1000@127.0.0.1:5061
Channel-Call-UUID=7a49e5b5-db65-4ed0-bc7b-ebe283cdbc1a
Caller-Context=default
## event body lines : 
#
]

企业级特性

  • 动态节点管理:运行时增减FreeSWITCH节点(如集群扩容)。
  • 事件顺序保障:单线程池处理事件,避免并发乱序(关键于呼叫流程如振铃→接听→挂断)。
  • 深度监控:集成Spring Actuator,暴露连接状态/事件延迟指标。

对比原生整合

场景手动集成esl-client使用Starter
多节点配置需编码实现动态注册YAML声明式配置,自动注入InboundClient Bean
事件监听需实现IEslEventListener并管理线程@EslEventListener注解+方法自动路由
资源释放需显式调用close()并捕获异常生命周期托管,Spring Context关闭时自动清理

推荐场景
所有Spring Boot项目 → 必选freeswitch-esl-spring-boot-starter,减少70%样板代码。


⚠️ 3. 旧版Netty 3.x的致命缺陷与规避方案

典型问题(org.freeswitch.esl.client:0.9.2

  • 线程泄漏:未释放Netty的ByteBufEventLoop线程,导致OOM。
  • 无界队列风险LinkedBlockingQueue默认容量Integer.MAX_VALUE,高并发下内存飙升。

临时解决方案(非推荐)

// 显式释放Netty资源(旧版补救)
channel.close().sync();  // 补充官方未实现的清理
executor.shutdownNow();  // 防止单线程池堆积

强烈建议
生产环境直接迁移至link.thingscloud系列库,彻底规避Netty 3隐患。

💎 终极选型决策树

在这里插入图片描述

📊 4. 性能与扩展性实测建议
  1. 压力测试
    • 模拟1k+并发连接,观察EventLoop线程数(Netty 4应稳定在核数*2)。
    • 监控堆外内存(DirectBuffer)是否及时释放。
  2. 灾备验证
    • 主动宕机FS节点,检查客户端重连日志(预期:10秒内切换备份节点)。
  3. 事件顺序性
    • 注入乱序事件(如先发送HANGUPANSWER),验证是否被纠正。

总结:向前兼容与未来演进
  • 存量系统迁移
    替换org.freeswitch.esl.clientlink.thingscloud/freeswitch-esl,彻底解决资源泄漏。
  • 新建项目标准
    • Spring Boot架构 → freeswitch-esl-spring-boot-starter
    • 高可用集群 → freeswitch-esl + 动态节点管理
  • 警惕“半改造”方案
    社区分支(如esl-client Netty 4.x版)缺乏企业级验证,慎用于生产。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c_zyer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值