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直接发起外呼。
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();
} }
}
参考输出结果类似如下:
连接成功,准备发起呼叫...
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中,光这么改了,也可能仍然连接不上,可以尝试:
<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文件,调整成:
...
<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
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
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 程序入口
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);
}
} }
输出结果:
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
freeswitch笔记(3)-esl入门的更多相关文章
- Hadoop学习笔记(1) ——菜鸟入门
Hadoop学习笔记(1) ——菜鸟入门 Hadoop是什么?先问一下百度吧: [百度百科]一个分布式系统基础架构,由Apache基金会所开发.用户可以在不了解分布式底层细节的情况下,开发分布式程序. ...
- iOS学习笔记-地图MapKit入门
代码地址如下:http://www.demodashi.com/demo/11682.html 这篇文章还是翻译自raywenderlich,用Objective-C改写了代码.没有逐字翻译,如有错漏 ...
- jQuery:自学笔记(1)——基础入门
jQuery:自学笔记(1)——基础入门 认识JQuery 1.jQuery概述 jQuery是一个快速.小巧 .功能丰富的JavaScript函数库.它可以实现“写的少,做的多”的目标. jQuer ...
- tensorflow学习笔记二:入门基础 好教程 可用
http://www.cnblogs.com/denny402/p/5852083.html tensorflow学习笔记二:入门基础 TensorFlow用张量这种数据结构来表示所有的数据.用一 ...
- Redis 笔记 01:入门篇
Redis 笔记 01:入门篇 ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ...
- SQLite:自学笔记(1)——快速入门
SQLite的安装和入门 了解 啥是SQLite? SQLite是一种轻巧迷你的关系型数据库管理系统.它的特点如下: 不需要一个单独的服务器进程或操作的系统(无服务器的). SQLite 不需要配置, ...
- Gradle 笔记——Java构建入门
Gradle是一个通用的构建工具,通过它的构建脚本你可以构建任何你想要实现的东西,不过前提是你需要先写好构建脚本的代码.而大部分的项目,它们的构建流程基本是一样的,我们不必为每一个工程都编写它的构建代 ...
- 004_Gradle 笔记——Java构建入门
Gradle是一个通用的构建工具,通过它的构建脚本你可以构建任何你想要实现的东西,不过前提是你需要先写好构建脚本的代码.而大部分的项目,它 们的构建流程基本是一样的,我们不必为每一个工程都编写它的构建 ...
- freeswitch编译java esl
一.背景假设源代码路径为/home/freeswitch 二.编译安装libesl.a1. cd /home/freeswitch(源代码的根目录) 执行./configure,以便生成必要的Make ...
- 《Maven实战》笔记-1-Maven使用入门
<Maven实战>徐晓斌 2011 机械工业出版社 一.介绍 1.名词 artifact:插件 极限编程XP 2.构建脚本: maven——pom.xml(Project Object ...
随机推荐
- 为什么重写equals一定也要重写hashCode方法?
简要回答 这个是针对set和map这类使用hash值的对象来说的 只重写equals方法,不重写hashCode方法: 有这样一个场景有两个Person对象,可是如果没有重写hashCode方法只重写 ...
- RPC实战与核心原理之优雅启动
优雅启动:如何避免流量打到没有启动完成的节点? 回顾 优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能"安全"地切走流量,不再调用自己,从而做到对业务无损.其中实 ...
- 关于#pragma comment的小认识
#pragma 指令 #pragma为预处理指令,作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主 ...
- WinDbg 分析 .NET Dump 线程锁问题
在定位 .NET 应用程序中的高 CPU 占用问题时,WinDbg 是非常强大的工具之一,尤其配合 SOS 扩展使用可以快速锁定"忙线程"或死锁等问题. 本文将基于一次实际的分析流 ...
- MySQL 根据时间排序失败
问题背景:MySQL数据库中,如果使用datetime,那其实只是精确到了秒.如果基于它排序并分页查询,若同一秒的数据超过一页,则多次查询得到的结果集可能会出现不一样的灵异事件.SQL: SELECT ...
- TINYINT[M]、INT[M]和BIGINT[M]中M值的意义
TINYINT[(M)] [UNSIGNED] [ZEROFILL] A very small integer. The signed range is -128 to 127. The unsign ...
- 玩转AI新声态 | 玩转TTS/ASR/YuanQI 打造自己的AI助手
前言 halo, 各位佬友这是我24年写的整理一下发出来, 可能有点老了, ai发展这么快...... 本次带来的是腾讯云玩转AI新声态语音产品应用实践,利用 TTS / ASR / 元器智能体 打造 ...
- 无法直连 SSH?一招反向SSH搞定内网到公网的远程连接问题
作者:SkyXZ CSDN:SkyXZ--CSDN博客 博客园:SkyXZ - 博客园 在校园网或者是家里的内网中,我们常常会遇到一个头疼的问题:两台设备明明都接入了网络,但当我离开内网之后却无法再远 ...
- 阿里微服务解决方案-Alibaba Cloud之集成Nacos(服务注册与发现)(三)
一.集成 Nacos(服务注册与发现) 1.1 下载 Nacos Nacos下载地址 1.2 下载后解压到本地 1.3 启动 Nacos 启动成功界面 输入 http://127.0.0.1:8848 ...
- X6在数栈指标管理中的应用
一.需求背景 产品成立之初,产品的需求是需要对各种指标进行公式运算,组合成一个新的复合指标,供后续使用.当时产品提出的形式是有两种: 一种是直接让用户输入,不作任何其他操作,但这种方式带来的问题一 ...