freeswitch笔记(3)-esl入门
题外话:昨天是2020年元宵节,正值"新型肺炎"第二阶段防治关键时期,返沪后按规定自觉在家隔离14天,不出去给社会添乱,真心希望这次疫情快点过去。
废话不多说,继续学习,上篇借助工具大致体验了voip client的使用,这篇学习如何用代码来实现类似的功能。esl全称Event Socket Library, 通过它可以与freeswitch进行交互,esl client支持多种语言,本文将以esl java client为例,演示一些基本用法:
一、两种模式:inbound、outbound
freeswitch(以下简单fs)启动后,内置了一个tcp server,默认会监听8021端口,通过esl,java 应用可以监听该端口,获取fs的各种事件通知,这种模式称为inbound模式。
如上图,inbound模式下:java应用引用esl java client的jar包后(注:esl java client底层是依赖netty实现的),连接到fs(fs内置了mod_event_socket模块,会在本地默认监听2081端口),连接成功后,如果有来电,fs会触发各种事件,透过已经连上的通道,通知java应用,java应用可以针对特定事件做些处理(有必要的话,还可以发送命令给fs),当然连接成功后,java应用也可以直接向fs发送命令,比如对外呼叫某个号码。
如果反过来,java应用起1个端口,自己充当tcp server,fs连接java应用,就称为outbound模式,如下图:
java应用利用esl java client在本机监听某个端口,相当于启动了一个tcp server(底层仍然是基于nettty实现),当fs收到来电时,会连接java应用的tcp server(注:需要修改fs的配置,否则fs不知道tcp server的ip\port这些连接信息),然后java应用可以根据自身业务做些处理,发送命令给fs(比如:给客人放段音乐或转接到特定目标),通话结束后(比如:主叫方挂断,或被叫方拒接),fs会断开连接,直到下次再有来电。
tips:inbound/outbound 是站在fs的角度来看的,外部应用连进来,就是inbound;fs连出去,就是outbound。 二种模式基本上都可以完成大多数业务功能,如何选取看各自特点,比如:如果要监控所有来电情况或实现客人自助语音服务,inbound相对更方便(可以很轻松获取所有事件)。对于来电后的人工客服分配,outbound则更简单(比如:客人来电拨打某个对外暴露公用客服号码比如400电话时,fs把客人来电通过tcp connect最终给到java app,java应用按一定分配规则 ,比如哪个客服最空闲,把来电bridge到该客服分机即可)
二、inbound 代码示例
2.1 pom依赖
<dependency> <groupId>org.freeswitch.esl.client</groupId> <artifactId>org.freeswitch.esl.client</artifactId> <version>0.9.2</version> </dependency>
2.2 演示代码
下面的代码,演示了连接到fs后,利用client直接发起外呼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | package com.cnblogs.yjmyzz.freeswitch.esl; 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 InboundApp { public static void main(String[] args) throws InterruptedException { Client client = new Client(); try { //连接freeswitch client.connect( "localhost" , 8021 , "ClueCon" , 10 ); client.addEventListener( new IEslEventListener() { @Override public void eventReceived(EslEvent event) { String eventName = event.getEventName(); //这里仅演示了CHANNEL_开头的几个常用事件 if (eventName.startsWith( "CHANNEL_" )) { String calleeNumber = event.getEventHeaders().get( "Caller-Callee-ID-Number" ); String callerNumber = event.getEventHeaders().get( "Caller-Caller-ID-Number" ); switch (eventName) { case "CHANNEL_CREATE" : System.out.println( "发起呼叫, 主叫:" + callerNumber + " , 被叫:" + calleeNumber); break ; case "CHANNEL_BRIDGE" : System.out.println( "用户转接, 主叫:" + callerNumber + " , 被叫:" + calleeNumber); break ; case "CHANNEL_ANSWER" : System.out.println( "用户应答, 主叫:" + 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( "用户挂断, 主叫:" + callerNumber + " , 被叫:" + calleeNumber + " , response:" + response + " ,hangup cause:" + hangupCause); break ; default : break ; } } } @Override public void backgroundJobResultReceived(EslEvent event) { String jobUuid = event.getEventHeaders().get( "Job-UUID" ); System.out.println( "异步回调:" + jobUuid); } }); client.setEventSubscriptions( "plain" , "all" ); //这里必须检查,防止网络抖动时,连接断开 if (client.canSend()) { System.out.println( "连接成功,准备发起呼叫..." ); //(异步)向1000用户发起呼叫,用户接通后,播放音乐/tmp/demo1.wav String callResult = client.sendAsyncApiCommand( "originate" , "user/1000 &playback(/tmp/demo.wav)" ); System.out.println( "api uuid:" + callResult); } } catch (InboundConnectionFailure inboundConnectionFailure) { System.out.println( "连接失败!" ); inboundConnectionFailure.printStackTrace(); } } } |
参考输出结果类似如下:
1 2 3 4 5 6 | 连接成功,准备发起呼叫... api uuid:54ae7272-62c1-4d1f-87a1-aab2080538dc 发起呼叫, 主叫:0000000000 , 被叫:1000 用户应答, 主叫:0000000000 , 被叫:1000 异步回调:54ae7272-62c1-4d1f-87a1-aab2080538dc 用户挂断, 主叫:1000 , 被叫:0000000000 , response:null ,hangup cause:NORMAL_CLEARING |
代码稍微解释一下:
a) 18行,连接fs的用户名、密码、端口,可以在freeswitch安装目录下的conf/autoload_configs/event_socket.conf.xml 找到

1 <configuration name="event_socket.conf" description="Socket Client"> 2 <settings> 3 <param name="nat-map" value="false"/> 4 <param name="listen-ip" value="0.0.0.0"/> 5 <param name="listen-port" value="8021"/> 6 <param name="password" value="ClueCon"/> 7 <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> 8 <!--<param name="stop-on-bind-error" value="true"/>--> 9 </settings> 10 </configuration>
强烈建议,把第4行listen-ip改成0.0.0.0(或具体的本机ip地址),默认的::是ipv6格式,很多情况会导致esl client连接失败,改成0.0.0.0相当于强制使用ipv4.
2024-01-13 更新:新版本的FS中,光这么改了,也可能仍然连接不上,可以尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | < configuration name="event_socket.conf" description="Socket Client"> < settings > < param name="nat-map" value="false"/> <!-- <param name="listen-ip" value="::"/> --> <!-- 这里换成你的IP地址 --> < param name="listen-ip" value="172.168.1.4"/> < param name="listen-port" value="8021"/> < param name="password" value="ClueCon1"/> <!-- <param name="apply-inbound-acl" value="loopback.auto"/> --> <!-- 这里改成lan --> < param name="apply-inbound-acl" value="lan"/> <!--<param name="stop-on-bind-error" value="true"/>--> </ settings > </ configuration > |
同时FreeSWITCH\conf\autoload_configs\acl.conf.xml文件,调整成:
1 2 3 4 5 6 7 8 | ... < list name="lan" default="allow"> <!-- <node type="deny" cidr="192.168.42.0/24"/> --> <!-- <node type="allow" cidr="192.168.42.42/32"/> --> < node type="allow" cidr="172.168.1.0/24"/> </ list > ... |
b) 考虑到网络可能发生抖动,在发送命令前,建议参考60行的做法,先判断canSend()
c) 61行,client.sendAsyncApiCommand 这里以异步方式,发送了一个命令给fs(即:呼叫1000用户,接通后再放段声音)。异步方式下,命令是否发成功当时并不知道,但是这个方法会返回一个uuid的字符串,fs收到后,会在backgroundJobResultReceived回调中,把这个uuid再还回来,参见上面贴出的输出结果。(基于这个机制,可以做些重试处理,比如:先把uuid存下来,如果约定的时间内,uuid异步回调还没回来,可以视为发送失败,再发一次)
重要提示:esl java client 0.9.2这个版本,inbound模式下,长时间使用有内存泄露问题,网上有很多这个介绍及修复办法,建议生产环境使用前,先修改esl client的源码。
三、outbound示例
3.1 修改dialplan配置
出于演示目的,这里修改/usr/local/freeswitch/conf/dialplan/default.xml,在文件开头部分添加一段:
<extension name="socket_400_example"> <condition field="destination_number" expression="^400\d+$"> <action application="socket" data="localhost:8086 async full"/> </condition> </extension>
即:当来电的被叫号码为400开头时,fs将利用socket,连接到localhost:8086
3.2 编写业务逻辑
a) SampleOutboundHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | package com.cnblogs.yjmyzz.freeswitch.esl.outbound; import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler; import org.freeswitch.esl.client.transport.SendMsg; import org.freeswitch.esl.client.transport.event.EslEvent; import org.freeswitch.esl.client.transport.message.EslHeaders; import org.freeswitch.esl.client.transport.message.EslMessage; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandlerContext; import java.util.ArrayList; import java.util.List; /** * @author 菩提树下的杨过 */ public class SampleOutboundHandler extends AbstractOutboundClientHandler { @Override protected void handleConnectResponse(ChannelHandlerContext ctx, EslEvent event) { System.out.println( "Received connect response :" + event); if (event.getEventName().equalsIgnoreCase( "CHANNEL_DATA" )) { // this is the response to the initial connect System.out.println( "======================= incoming channel data =============================" ); System.out.println( "Event-Date-Local: " + event.getEventDateLocal()); System.out.println( "Unique-ID: " + event.getEventHeaders().get( "Unique-ID" )); System.out.println( "Channel-ANI: " + event.getEventHeaders().get( "Channel-ANI" )); System.out.println( "Answer-State: " + event.getEventHeaders().get( "Answer-State" )); System.out.println( "Caller-Destination-Number: " + event.getEventHeaders().get( "Caller-Destination-Number" )); System.out.println( "======================= = = = = = = = = = = = =============================" ); // now bridge the call bridgeCall(ctx.getChannel(), event); } else { throw new IllegalStateException( "Unexpected event after connect: [" + event.getEventName() + ']' ); } } private void bridgeCall(Channel channel, EslEvent event) { List<String> extNums = new ArrayList<>( 2 ); extNums.add( "1000" ); extNums.add( "1010" ); //随机找1个目标(注:这里只是演示目的,真正分配时,应该考虑到客服的忙闲情况,通常应该分给最空闲的客服) String destNumber = extNums.get(( int )Math.abs(System.currentTimeMillis() % 2 )); SendMsg bridgeMsg = new SendMsg(); bridgeMsg.addCallCommand( "execute" ); bridgeMsg.addExecuteAppName( "bridge" ); bridgeMsg.addExecuteAppArg( "user/" + destNumber); //同步发送bridge命令接通 EslMessage response = sendSyncMultiLineCommand(channel, bridgeMsg.getMsgLines()); if (response.getHeaderValue(EslHeaders.Name.REPLY_TEXT).startsWith( "+OK" )) { String originCall = event.getEventHeaders().get( "Caller-Destination-Number" ); System.out.println(originCall + " bridge to " + destNumber + " successful" ); } else { System.out.println( "Call bridge failed: " + response.getHeaderValue(EslHeaders.Name.REPLY_TEXT)); } } @Override protected void handleEslEvent(ChannelHandlerContext ctx, EslEvent event) { System.out.println( "received event:" + event); } @Override protected void handleDisconnectionNotice() { super .handleDisconnectionNotice(); System.out.println( "Received disconnection notice" ); } } |
重点看下bridgeCall这个方法,假设有2个客服号码1000、1010可用,随机挑1个,然后将来电接通到这个号码。
b) AbstractOutboundPipelineFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package com.cnblogs.yjmyzz.freeswitch.esl.outbound; import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler; import org.freeswitch.esl.client.outbound.AbstractOutboundPipelineFactory; /** * @author 菩提树下的杨过 */ public class SamplePipelineFactory extends AbstractOutboundPipelineFactory { @Override protected AbstractOutboundClientHandler makeHandler() { return new SampleOutboundHandler(); } } |
还需要一个工厂类,包装一下。
c)OutboundApp 程序入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package com.cnblogs.yjmyzz.freeswitch.esl.outbound; import org.freeswitch.esl.client.outbound.SocketClient; /** * @author 菩提树下的杨过 */ public class OutboundApp { public static void main(String[] args) throws InterruptedException { new Thread(() -> { SocketClient socketClient = new SocketClient( 8086 , new SamplePipelineFactory()); socketClient.start(); }).start(); while ( true ) { Thread.sleep( 500 ); } } } |
输出结果:
1 2 3 4 5 6 7 8 9 10 | Received connect response :EslEvent: name=[CHANNEL_DATA] headers=5, eventHeaders=169, eventBody=0 lines. ======================= incoming channel data ============================= Event-Date-Local: 2020-02-09 12:02:35 Unique-ID: bd659733-d460-4f0f-8c73-4cd4f1e39f68 Channel-ANI: 1002 Answer-State: ringing Caller-Destination-Number: 4008123123 ======================= = = = = = = = = = = = ============================= 4008123123 bridge to 1010 successful Received disconnection notice |
文中示例代码git地址: https://github.com/yjmyzz/freeswitch-esl-java-client-sample
参考文章:
https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket
https://freeswitch.org/confluence/display/FREESWITCH/Java+ESL+Client
出处:http://yjmyzz.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2015-02-09 Effective java 第2版 - 笔记(01) 单例(Singleton)的枚举(enum)实现
2011-02-09 objective-C 的OOP(下)-类(static)方法、实例方法、overwrite(覆写)、属性(property)
2010-02-09 ruby学习笔记(1)--初识语法