信也语音服务架构揭秘

前言

电话销售与贷后联络是公司业务比较重要的一环,而为了实现语音呼叫的功能,公司内部是有一套自研的语音系统来支撑的,那么大家是否好奇语音系统是如何实现的呢?

随着互联网的兴起,VoIP(网络电话)也随之发展起来,VoIP是通过互联网来传输语音和视频的,比如微信的语音和视频通话就是通过VoIP来实现的,所以VoIP的成本和价格低廉,也可以实现很多传统电话无法实现的功能,通过转换网关也可以与PSTN(传统电话网络)对接,而公司内部的语音系统就是基于VoIP来实现的。

语音服务架构说明

一、相关协议

由于语音基础服务是基于VoIP技术来实现的,所以会话控制、媒体传输这些都需要遵循一个标准的协议来进行的。

会话的控制

通话时通过会话初始协议 (Session Initiation Protocal, SIP , RFC 3261)来控制会话的。

SIP是一个应用层的信令控制协议,主要目的是在 IP 网络中建立、修改和释放多媒体会话的应用层协议。其主要的应用包括但不局限于语音、消息、视频、呼叫控制等。会话的参与者可以通过组播(multicast)、网状单播(unicast)或两者的混合体进行通信。
SIP的业务模式是一个点对点协议,其中有两个要素——SIP用户代理(User Agent, UA)和SIP网络服务器。

媒体流的传输

通话时的媒体流(语音流、视频流等)是通过实时传输协议 (Real-time Transport Protocol,RTP )来进行传输的。

RTP是一个网络传输协议,它是由IETF的多媒体传输工作小组1996年在RFC 1889中公布的。是一个网络传输协议,它是由IETF的多媒体传输工作小组1996年在RFC 1889中公布的。

RTP协议详细说明了在互联网上传递音频和视频的标准数据包格式。它一开始被设计为一个多播协议,但后来被用在很多单播应用中。RTP协议常用于流媒体系统(配合RTSP协议),视频会议和一键通(Push to Talk)系统(配合H.323或SIP),使它成为IP电话产业的技术基础。RTP协议和RTP控制协议RTCP一起使用,而且它是创建在UDP协议上的。

SIP信令构成

  1. SIP信令可以基于TCP传输,也可以基于UDP传输
  2. SIP信令可以分为请求(图左:Request-Line)和响应(图右:Status-Line),由起始行来确定
  3. Message Header部分用于存放路由信息以及会话中需要处理的信息
  4. Messge Body(payload)是存放媒体信息和一些文本以及XML信息的

  1. 请求信令包含一个Method字段用于确定请求的类型,不同的请求有不同的职能,如INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, REGISTER等等。
  2. 响应信令包含一个Status-Code字段,用于标识响应码,不同的响应码也有不同的含义,如:100, 180, 183, 200, 400, 407, 487, 500, 503等等。

  1. Record-Route、Via、Contact包含了通话的路由以及通话传输的路径,可以让响应找到返回的路径。
  2. From、To包含了通话的主叫和被叫信息
  3. X-开头的一些数据是一些自定义参数,可以用于业务传参

Message Body中承载的SDP标记了媒体的信息,而通过RTP承载的媒体流就会根据这个信息来传输。

媒体信息:

  • 服务器地址:IPv4 xx.xx.xx.xx(图中方框位置)
  • 媒体类型:audio(语音)
  • 媒体端口:12116
  • 媒体协议:RTP/AVP
  • 语音编码:G.711 PCMU

语音通话信令流程

  1. 215向217发起INVITE请求,表示要拨打一通电话
  2. 217向215回复一个响应码为100的响应,表示我已经接收到请求,正在处理,请等待
  3. 217向215回复一个响应码为183的响应,表示我已经开始响铃了
  4. 这里183是携带SDP的,与INVITE中的SDP相互协商出媒体信息,语音流已经可以开始传输了,而语音流是通过RTP来传输的
  5. 217向215回复一个响应码为200的响应,表示我已经接起电话了
  6. 215向217发起一个ACK的请求,表示此次INVITE请求处理完成了
  7. 217向215发起一个BYE请求,表示我挂断电话了(哪一方先挂断,该请求由哪一方发起)
  8. 215向217回复一个响应码为200的响应,表示我知道了,此时双方都挂断了,通话结束

注:180的响应码也是表示振铃,但是180不携带SDP信息,这时候是没有建立语音通道的,要等待接听后(200OK)语音通道才会被打通,所以在回复183的时候,才能够听到彩铃音。

二、架构说明

注册服务器

注册服务器是基于开源SIP服务器OpenSIPS进行开发的,主要是进行分机的管理、认证、注册,用于SIP信令、RTP语音流的代理转发,对媒体服务器进行负载均衡。

电销的客服,通过电话终端(webphone/软电话/硬电话)注册到注册服务器上,注册成功后,就可以通过电话终端进行语音电话的呼叫了。

媒体服务器

媒体服务器是基于开源软交换FreeSWITCH进行开发的,主要是进行呼叫逻辑的控制,提供通话相关的各种信息。

当电销坐席发起呼叫时,由注册服务器将呼叫代理转发到媒体服务器,由媒体服务器对呼叫进行控制,可以对通话进行录音,可以将通话记录通过接口返回给业务系统。

SBC网关代理服务器

SBC网关代理服务器是基于开源SIP服务器OpenSIPS进行开发的,可以进行呼叫并发控制,通过SIP协议对接线路,代理转发SIP信令和RTP语音流,并对线路进行负载均衡。

SBC负载服务器可以对接基于SIP协议的线路服务器,若是传统的电话网络,则需要通过转换网关转换成支持SIP对接的线路,当媒体服务器需要拨打外部的电话时,SBC网关代理会根据媒体服务器发送的SIP信令中携带的信息,通过路由负载代理转发到相应的线路中,最终完成电话的呼出。

系统对接

语音服务对外提供了一套API接口,可以与其他系统进行对接,接入语音服务。

三、相关技术点说明

软电话/硬电话

软电话是安装在PC、手机等终端设备上的一种软件电话,软电话与真是的电话界面上是相似的,功能也是相似的,接入到互联网可以注册到SIP服务器上,完成拨打、接听电话的功能。

这里的硬电话与普通的座机是基本上一样的,但是它可以接入到互联网中,注册到SIP服务器上的,配置好后与传统的座机没有任何区别。

WebPhone

WebPhone是基于WebRTC技术实现的,是运行来浏览器上的,相对软电话/硬电话来说,不需要繁琐的安装和配置,只要浏览器支持WebRTC,就可以跨平台即开即用,基本不存在跨平台问题。

WebRTC,名称源自网页即时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。

OpenSIPS

OpenSIPS是一个成熟的开源SIP服务器,除了提供基本的SIP代理及SIP路由功能外,还提供了一些应用级的功能。OpenSIPS的结构非常 灵活,其核心路由功能完全通过脚本来实现,可灵活定制各种路由策略,可灵活应用于语音、视频通信、IM以 及Presence等多种应用。同时OpenSIPS性能上是目前最快的SIP服务器之一,可用于电信级产品构建。

FreeSWITCH

FreeSWITCH 是一个开源的电话交换平台,它具有很强的可伸缩性–从一个简单的软电话客户端到运营商级的软交换设备几乎无所不能。能原生地运行于Windows、Max OS X、Linux、BSD 及 solaris 等诸多32/64位平台。可以用作一个简单的交换引擎、一个PBX,一个媒体网关或媒体支持IVR的服务器等。它支持SIP、H323、Skype、Google Talk等协议,并能很容易地与各种开源的PBX系统如sipXecs、Call Weaver、Bayonne、YATE及Asterisk等通信。 FreeSWITCH 遵循RFC并支持很多高级的SIP特性,如 presence、BLF、SLA以及TCP、TLS和sRTP等。它也可以用作一个SBC进行透明的SIP代理(proxy)以支持其它媒体如T.38等。FreeSWITCH 支持宽带及窄带语音编码,电话会议桥可同时支持8、12、16、24、32及48kHZ的语音。

四、通话处理逻辑

当从前端webphone发起呼叫后,会发送INVITE请求到注册服务器(OpenSIPS)上

if (is_method("INVITE")) {
      # 需要进行注册认证
      if (!is_registered("location", "$fu")) {
        send_reply("503", "Attack!");
        exit;
      }
      setflag(AGENT_TO_FS);
      $var(signid) = $(hdr(X-Sign-ID)[0]);
      xlog("call-id:$ci, signid:$var(signid), call from agent[$fU] to freeswitch[$tU], set AGENT_TO_FS");
      # incoming from user lb to freeswitch
      lb_start("100", "pstn", "rs");
      switch ($retcode) {
        case -1:
          xlog("call-id:$ci, 系统错误,无法找到freeswitch");
          send_reply("500", "System Error");
          exit;
        case -2:
          xlog("call-id:$ci, 查找freeswitch失败,freeswitch的并发已满");
          send_reply("500", "Service Full");
          exit;
        case -3:
          xlog("call-id:$ci, 查找freeswitch失败,没有可用的freeswitch");
          send_reply("500", "Service Down");
          exit;
        case -4:
          xlog("call-id:$ci, 查找freeswitch失败,freeswitch未配置pstn资源");
           send_reply("500", "No Resource");
          exit;
      }
      xlog("call-id:$ci, call to freeswitch[$du] success");
    }
  }

webphone发起的呼叫会先通过is_registered进行注册认证,若是已注册的分机号,则会将通话通过lb_start(负载均衡)转发到对应的媒体服务器上,未注册的分机会被认定为盗打。

<extension name="decodeCallOut">
      <condition field="destination_number" expression="^de(.*)$">
        <action application="set" data="sip_h_X-accountcode=${accountcode}" />
        <action application="set" data="sip_h_X-Tag=" />
        <action application="set" data="callType=${sip_h_X-Call-Type}"/>
        <action application="set" data="serviceType=1"/>
        <action application="set" data="call_direction=outbound" />
        <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}" />
        <action application="lua" data="${ivrpath}/callout.lua"/>
      </condition>
</extension>

当电话到达媒体服务器时,媒体服务器(FreeSWITCH)会将每个通话封装成一个session,先通过通过拨叫计划(dialplan)来进行预处理,将电话控制权转交给callout.lua这个脚本,FreeSWITCH会将通话信息解析存放到session中,在lua中可以通过session来控制呼叫流程

local caller = session:getVariable("caller_id_number")
local destination_number = session:getVariable("destination_number")

可以从通道中获取到主被叫的号码

function createCall(callNumber, shareChannelVariable)
    callString = shareChannelVariable .. displayNumber .. "}sofia/gateway/"
    return callString .. gateway .. routeGroupId .. gwPrefix .. callNumber
end

local channel_variable = "{origination_uuid=" .. uuid .. ",park_after_bridge=true,extension_number=" .. caller .. ",call_number=" .. encodeCallee .. ",sip_h_X-Is-Public=" .. isPublic .. ",origination_caller_id_number="

if session:ready() then
    local dialString = createCall(encodeCallee, channel_variable)
    session:setVariable("media_bug_answer_req", "true")
    session:execute("record_session", recordPath)
    session:setVariable("recordPath", recordPathPara)
    showLog("info", "dialString", dialString)
    session:execute("bridge", dialString)
    local legB = freeswitch.Session(uuid)
    if legB:ready() and not legB:answered() then
        showLog("info", "waiting exit", "waiting exit for detect thread")
        legB:sleep("500")
        legB:hangup()
    end
    if legB:answered() then
        legB:hangup()
    end
end
  • 通过ready函数来确保当前通话已经准备完毕,可以进行控制了
  • 通过record_session命令可以对通话进行录音
  • 通过hangup函数可以将电话主动挂断

当一切都准备就绪,就需要真正向线路发起呼叫请求了,通过bridge命令向SBC服务器发起呼叫请求

if (is_method("INVITE")) {
    # 检查呼叫方向 freeswitch
    if (lb_is_destination("$si", "$sp", "100")) {
      # incoming from freeswitch lb to gw
      setflag(FS_TO_GW);
      xlog("call-id:$ci, call from freeswitch to gateway, set FS_TO_GW");
      $var(signid) = $(hdr(X-Sign-ID)[0]);
      xlog("call-id:$ci, signid:$var(signid), call from $fU, to $tU");
      $var(callee) = $ruri.user;
      # 商户前缀->路由组ID
      $var(prefix) = $(var(callee){s.substr,0,2});
      xlog("call-id:$ci, prefix: $var(prefix), number: $var(callee)");

      lb_start("$(var(prefix){s.int})", "pstn", "rs");
      switch ($retcode) {
        case -1:
          xlog("call-id:$ci, 查找网关失败,系统错误");
          send_reply("500", "System Error");
          exit;
        case -2:
          xlog("call-id:$ci, 查找网关失败,所有网关并发已满");
          send_reply("500", "Service Full");
          exit;
        case -3:
          xlog("call-id:$ci, 查找网关失败,无可用的网关");
          send_reply("500", "Service Down");
          exit;
        case -4:
          xlog("call-id:$ci, 查找网关失败,无pstn网关资源");
          send_reply("500", "No Resource");
          exit;
      }

      # 被叫号码(不带商户前缀)
      $var(callee) = $(var(callee){s.substr,2,0});
      # info的值是 "线路前缀,线路主叫认证,线路domain"
      dp_translate("$(var(prefix){s.int})", "$du/$var(info)");
      xlog("call-id:$ci, select perfix and caller is $var(info)\n");
      # 以","分割info
      # 线路前缀
      $var(gw_prefix) = $(var(info){s.select,0,,});
      # 线路主叫认证
      $var(gw_caller) = $(var(info){s.select,1,,});
      # 线路domain
      $var(dest_uri) = $du;
      $var(dest_domain) = $(var(dest_uri){s.substr,4,0});
      # 拼接request uri
      $ruri = "sip:" + $var(gw_prefix) + $var(callee) + "@" + $var(dest_domain);
      # 修改 to
      uac_replace_to("$ruri");
      xlog("call-id:$ci, new to: $ruri");
      # 修改 from
      if ($var(gw_caller) != "" && $var(gw_caller) != null) {
        if (isflagset(FROM_LOCAL)) {
          uac_replace_from("$var(gw_caller)", "sip:$var(gw_caller)@LocalIpV4");
          xlog("call-id:$ci, new from: sip:$var(gw_caller)@LocalIpV4");
        } else {
          uac_replace_from("$var(gw_caller)", "sip:$var(gw_caller)@NetIpV4");
          xlog("call-id:$ci, new from: sip:$var(gw_caller)@NetIpV4");
        }
      }
      xlog("call-id:$ci, call to gateway success");
    } else {
        xlog("Attack from $si:$sp!!!");
        send_reply("500", "Attack!!");
        exit;
    }

    # account only INVITEs
    do_accounting("log");
  }

当电话到达SBC网关服务器时,会根据预设的信息,通过lb_start去查找真正的线路,最终将电话送达被叫的手机,如果网路和线路都正常,这时被叫的手机将会开始振铃,电话呼叫成功。

语音服务应用

语音服务除了电话营销和贷后联络外,在公司内部还有很多别的应用:

空停检测

系统自动拨打电话,通过分析拨打电话接通之前的声音,比如说:长嘟嘟的回铃音、短嘟嘟的忙音、彩铃、空号、通话中、关机等运营商网络给出来的提示音,来获得被叫的状态,这样可以剔除掉那些空号和停机的号码,提升电销和贷后联络的效率。

智牛语音机器人

语音系统对接ASR(语音转文字)、TTS(文字转语音)、NLP(自然语言理解)的服务,可以实现自动的语音机器人,无需人工的接入,就可以完成电话营销和贷后联络的任务了。

电话告警

语音系统对接TTS,可以自动拨打电话,给用户播放一段文字或预设的语音,可以用于告警和提醒。

作者介绍

Passerby,科技输出团队技术专家。

03-05 15:17