信也语音服务架构揭秘
前言
电话销售与贷后联络是公司业务比较重要的一环,而为了实现语音呼叫的功能,公司内部是有一套自研的语音系统来支撑的,那么大家是否好奇语音系统是如何实现的呢?
随着互联网的兴起,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信令构成
- SIP信令可以基于TCP传输,也可以基于UDP传输
- SIP信令可以分为请求(图左:Request-Line)和响应(图右:Status-Line),由起始行来确定
- Message Header部分用于存放路由信息以及会话中需要处理的信息
- Messge Body(payload)是存放媒体信息和一些文本以及XML信息的
- 请求信令包含一个Method字段用于确定请求的类型,不同的请求有不同的职能,如INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, REGISTER等等。
- 响应信令包含一个Status-Code字段,用于标识响应码,不同的响应码也有不同的含义,如:100, 180, 183, 200, 400, 407, 487, 500, 503等等。
- Record-Route、Via、Contact包含了通话的路由以及通话传输的路径,可以让响应找到返回的路径。
- From、To包含了通话的主叫和被叫信息
- X-开头的一些数据是一些自定义参数,可以用于业务传参
Message Body中承载的SDP标记了媒体的信息,而通过RTP承载的媒体流就会根据这个信息来传输。
媒体信息:
- 服务器地址:IPv4 xx.xx.xx.xx(图中方框位置)
- 媒体类型:audio(语音)
- 媒体端口:12116
- 媒体协议:RTP/AVP
- 语音编码:G.711 PCMU
语音通话信令流程
- 215向217发起INVITE请求,表示要拨打一通电话
- 217向215回复一个响应码为100的响应,表示我已经接收到请求,正在处理,请等待
- 217向215回复一个响应码为183的响应,表示我已经开始响铃了
- 这里183是携带SDP的,与INVITE中的SDP相互协商出媒体信息,语音流已经可以开始传输了,而语音流是通过RTP来传输的
- 217向215回复一个响应码为200的响应,表示我已经接起电话了
- 215向217发起一个ACK的请求,表示此次INVITE请求处理完成了
- 217向215发起一个BYE请求,表示我挂断电话了(哪一方先挂断,该请求由哪一方发起)
- 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,科技输出团队技术专家。