• 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的ipport这些连接信息),然后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>
    View Code

    强烈建议,把第4行listen-ip改成0.0.0.0(或具体的本机ip地址),默认的::是ipv6格式,很多情况会导致esl client连接失败,改成0.0.0.0相当于强制使用ipv4

    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="^400d+$">
                <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

  • 相关阅读:
    #Laravel 笔记# 多语言化 App::setLocale() 持久化。
    thinkphp 3.2 发送邮件(Phpmailer)
    深度学习的注意力机制
    图像检索引擎vearch安装与测试使用
    word2vector
    GPU环境搭建
    ImportError: libSM.so.6: cannot open shared object file: No such file or dir
    shell中&&和||的用法
    Linux 远程连接sftp与ftp
    mysql-connector-java各版本及与mysql、JDK版本的对应
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/freeswitch-esl-java-client-turorial.html
Copyright © 2020-2023  润新知