c# 虚拟机加密软件
本文重点介绍C#中环组的开发。 任何呼叫中心的有效性不仅取决于运营商的行为,还取决于呼叫中心的技术背景。 学习完本指南后,您将能够创建振铃组分机(即“虚拟软件电话”),该分机可用于根据预定义的振铃组策略将传入呼叫转移到选定的呼叫中心座席之一。 下载源代码: Softphone-RingGroup-RingOneByOne.zip 目录- 主题的重要性
- 有关呼叫中心的技术背景的更多信息
- 环组开发简介
- 开发/第1部分:构建软件电话
- 开发/第2部分:构建其他功能
- 开发/第3部分:使用“一对一”策略构建环网组
- 测试
- 结论
- 进一步的阅读和参考
呼叫中心或呼叫中心是一个集中式办公室,用于通过电话接收或发送大量请求[1]。 换句话说:“呼叫中心是一个物理场所,通常由一定数量的计算机自动化来由组织来处理客户和其他电话” [2]。 „
前面定义的一部分通常表示,成功的呼叫中心是基于人工和某种技术设备的合作。 因此,适当的IT基础架构至关重要。据专家介绍,
精心挑选员工是成功建立呼叫中心的第一步。 但是大多数技能都是可以学习的。 在第一线支持,服务台和服务台培训期间 ,讲师经常提请操作员注意以下几点:- 要仁慈有礼
- 主动
- 首先把事情放在首位–让优先事项推动举措
- 双赢思考–对您的客户有利的对您也有利,等等。
这些是成功呼叫中心必不可少的非常重要的人为因素。 另外,某些
管理工具,例如绩效评估和对员工的定期反馈,奖励,积极强化等,是有效呼叫中心不可或缺的要素。 但是如果没有适当的技术,所有人类知识都是徒劳的。 无论是小型呼叫中心还是大型呼叫中心,要获得更多利润并建立客户忠诚度,员工都必须使用先进的硬件和软件设备 。在外包的印度呼叫中心和大型的美国呼叫中心中常见的是
图1 )? 是的,有很多东西,但最重要的是核心技术。 在这两个地方,运营商都使用VoIP台式电话,软电话,头戴式耳机,以及:具有典型呼叫中心功能(例如,呼叫转移,振铃组,呼叫记录,呼叫排队,电话会议等)的高级PBX系统,这使某些任务更加复杂。由于自动化,操作简单。呼叫中心有能力
同时处理大量电话并将其转发给负责处理这些电话的人。 但是,在当今快速发展的世界中,VoIP技术,高性能计算机和智能软件应用程序几乎是必不可少的。 今天的呼叫中心使用的是比老式的巨大“盒子”更多的最新设备 ,而老式的“盒子”却有很多电线伸出来。我们可以将呼叫中心分为两种主要类型,如下所示[1]:
- 虚拟呼叫中心:在这种呼叫中心模型中,运营商通常向在其自己的数据中心托管呼叫中心电话设备的供应商支付月费或年费。 代理商通过传统的PSTN电话线或VoIP(互联网协议语音)连接到供应商的设备。
- 基于前提的呼叫中心:此模型与先前的模型相反。 在这种情况下,呼叫中心建在PBX(专用分支交换)中,该交换机由呼叫中心运营商自己拥有,托管和维护。 该PBX提供高级功能,例如呼叫排队,ACD(自动呼叫分配),IVR(交互式语音响应)或SBR(基于技能的路由)–这是一种呼叫辅助策略,用于将传入呼叫分配给最合适的座席,而不是简单地选择下一个可用的代理)。
如果使用虚拟呼叫中心,则功能是固定的,不能无限扩展它们。 但是,在基于内部的呼叫中心的情况下,您将有机会通过开发所需的功能来改进系统。 实际上,只有想象力(当然还有公司的需求和条件)限制了这种改进。
环组开发简介在本文中,您将看到后一种情况的示例。 确切地说,本指南分步演示了如何创建
环组 。 该铃声组将是安装在集团电话中的“虚拟” 分机 。 此分机基于软件电话 (因此,即已注册到PBX)。 PBX为该分机分配电话号码。 呼叫时,分机尝试将来电转移到其他分机之一(即转移到先前已在PBX上注册的振铃组成员之一)。 振铃组策略确定哪个成员将接受呼叫。恐怕以前的线程包含一些新的(或令人恐惧的)表达式……不管是不是,为了完整起见,让我澄清一下此解决方案的基本要素。
- 什么是振铃组:在现代电信时代,呼叫路由允许在有来电时有多个分机振铃。 它称为环组。 环组通常在PBX中显示为“虚拟分机”。 在配置振铃组时,可以定义哪些电话分机属于该振铃组。 这样,当有“虚拟分机”的来电时,所有所属分机将在振铃组内同时或依次振铃。
- “分机”的含义是什么:在商务电话中,电话分机可能是指与PBX相连的内部电话网络中的电话。 在集团电话内,用户只需拨打分机号码即可直接与任何其他用户联系。 对于呼入电话,拨打公司电话号码后,总机接线员或自动值班人员可以要求首选分机的号码。 如果将外部号码分配给各个分机[5],则也可以通过直接呼入完成呼叫。
- 什么是软件电话:软件电话是一种特殊的计算机软件,可以充当虚拟电话,使人们可以使用其PC或笔记本电脑通过Internet与他人进行通信。
- 什么是呼叫转移:呼叫转移是一种电信机制,允许用户通过使用转移按钮或切换挂机闪光灯并拨所需的位置[6],将现有电话转移到另一个电话或话务台。
- 什么是振铃组策略:振铃组策略确定在有来电时如何通知组成员的方式。 在呼叫转移之前,环网组需要通知组成员需要进行呼叫转移。 要选择在振铃组内接受呼叫的成员,可以使用几种策略。 让我们看一些示例:可以配置为同时呼叫振铃组中的所有分机(即所有成员的电话)。 还可以实现的是,环群分机将以预先设置的顺序一一呼叫成员。 另一种常用的策略是环组的延伸将以随机顺序被调用的情况。 (在该项目中,将演示“一对一”策略。)
基于以上内容,让我们总结一下该项目需要哪些开发:
- 构建能够接收电话的虚拟(或控制台)软件电话
- 构建所需的附加功能,例如呼叫转移和多路呼叫管理
- 用“一对一”的策略建立团队
因此,我使用了以下
3节课 :- Softphone.cs:它介绍了如何使用可以注册到PBX的C#开发软件电话。
- Program.cs:此类负责处理用户事件。 它介绍了如何将软件电话注册到PBX。
- RingGroupCallHandler.cs:此类说明如何创建必要的铃声组策略。 由于此类分开,因此应用程序可以使用其实例同时处理多个传入呼叫。
首先,您将需要一个新的
Visual C#控制台应用程序 ,因为此应用程序实际上是“虚拟软件电话”,所以一个控制台应用程序就足够了。 为了更好地理解,我将本文的其余部分分为三个主要部分。 这三个部分全部介绍了一个类的实现步骤。现在,已经用尽了理论和发展背景,“保持冷静,开始编程!” :)
开发/第1部分:构建软件电话你准备好了吗? 当然! :)好吧,让我们从
Softphone.cs类。 第一步,使用以下行添加所需的内容。 (作为C#.NET VoIP库,已经使用了Ozeki VoIP SIP SDK- 请记住,此代码是使用此SDK的预先编写的VoIP组件编写的。因此,如果要尝试使用它们,则需要安装它的免费试用版。 [7])using System;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK;
现在需要软件电话和电话线对象。
您可以从ISoftPhone和IPhoneLine接口获取它们,如下所示:
ISoftPhone _softphone;
IPhoneLine _phoneLine;
在构造函数中,您需要使用默认参数来初始化此软件电话。
5000和10000参数是指端口范围:第一个数字是最小端口号( minPortRange ),另一个是最大端口号( maxPortRange )。
为了实现对来电的持续监控,您需要订阅IncomingCall事件。
请查看以下上述步骤:
public Softphone()
{
_softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000);
_softphone.IncomingCall += softphone_IncomingCall;
}
通过订阅IncomigCall事件,将通知您是否有来电。
如果电话线的状态发生更改,那么PhoneLineStateChange事件(当然,在订阅后)会通知您。
请查看以下上述步骤:
public event EventHandler<VoIPEventArgs<IPhoneCall>> IncomigCall;
public event EventHandler<RegistrationStateChangedArgs> PhoneLineStateChanged;
通过使用
注册方法,可以将软件电话注册到PBX。 为此,应创建一个需要SIP帐户的电话线。 为了能够为您的软件电话创建SIP帐户,您需要指定以下参数:- registrationRequired:为了能够接收来电,您需要为此参数设置“ true”值。
- displayName:这是要在被叫客户端上显示的名称。
- userName:这是要呼叫此软件电话的其他客户端要拨打的号码。
- authenticationId:这是PBX的标识符(如登录名)。
- registerPassword:这是用于注册到PBX的authenticationId的密码。
- domainHost:这是一个域名,即您要注册的集团电话的IP地址。
- domainPort:这是您要注册的集团电话的端口号。
创建必要的SIP帐户后,您可以创建电话线以能够与PBX通信。 还需要收听电话线状态的变化。 最后,
RegisterPhoneLine方法可用于将电话线注册到软件电话。请查看以下上述步骤:
public void Register(bool registrationRequired, string displayName, string userName, string authenticationId, string registerPassword, string domainHost, int domainPort)
{
try
{
var account = new SIPAccount(registrationRequired, displayName, userName, authenticationId, registerPassword, domainHost, domainPort);
Console.WriteLine("\nCreating SIP account {0}", account);
_phoneLine = _softphone.CreatePhoneLine(account);
Console.WriteLine("Phoneline created.");
_phoneLine.RegistrationStateChanged += _phoneLine_RegistrationStateChanged;
_softphone.RegisterPhoneLine(_phoneLine);
}
catch (Exception ex)
{
Console.WriteLine("Error during SIP registration" + ex.ToString());
}
}
当电话线的注册状态更改时,将调用此方法:
void _phoneLine_RegistrationStateChanged(object sender, RegistrationStateChangedArgs e)
{
var handler = PhoneLineStateChanged;
if (handler != null)
handler(this, e);
}
这将在有来电时被调用:
void softphone_IncomingCall(object sender, VoIPEventArgs<IPhoneCall> e)
{
var handler = IncomigCall;
if (handler != null)
handler(this, e);
}
调用时,可以使用以下代码片段创建调用对象:
public IPhoneCall CreateCall(string member)
{
return _softphone.CreateCallObject(_phoneLine, member);
}
这是Softphone.cs的实现的结尾。
我了解您是否现在想休息一下。
休息一下,让我们继续编码Program.cs !
:) 开发/第2部分:构建其他功能
完成软件电话的创建后,让我们继续使用
Program.cs 。 此类介绍了Softphone对象的用法,处理控制台事件,与用户进行交互以及使用Softphone类提供的机会。 (您将看到,所有内容都是在相互通信的单独方法中处理的。)以下
可以在需要通过调用初始化器方法初始化软件电话的地方看到Main方法。 (代码段末尾的BlockExit方法-不允许应用程序退出。)static List<RingGroupCallHandler> _callHandlers;
static List<String> _members;
static int _memberCount;
static Softphone _softphone;
static void Main(string[] args)
{
_softphone = new Softphone();
ShowHelp();
SipAccountInitialization(_softphone);
_callHandlers = new List<RingGroupCallHandler>();
_softphone.IncomigCall += softphone_IncomigCall;
_softphone.PhoneLineStateChanged += softphone_PhoneLineStateChanged;
BlockExit();
}
现在,有一个新的软件电话可以监视电话线的状态。
您已订阅以获取有关电话线状态已更改的通知。
在下面,您可以看到Softphone_PhoneLineStateChanged方法确定每种状态的处理方式。
当电话线的注册状态更改时,将调用此方法:
static void softphone_PhoneLineStateChanged(object sender, RegistrationStateChangedArgs e)
{
Console.WriteLine("Phone line state changed to: {0}", e.State);
if (e.State == RegState.RegistrationSucceeded)
MemberAdder();
}
关于该类也订阅了电话线路的事件这一事实,当电话线路的状态成功注册时会通知该类,并且它开始向用户询问振铃组:成员编号,他们的电话号码。
这些被存储在字符串列表中:
private static void MemberAdder()
{
_members = new List<String>();
while (_memberCount == 0 || _memberCount == null)
{
Console.Write("\nThe number of the RingGroup members: ");
try
{
_memberCount = Int32.Parse(Console.ReadLine());
}
catch
{
Console.WriteLine("Wrong input!");
}
}
for (int i = 0; i < _memberCount; i++)
{
Console.Write("{0}. member's phone number: ", i + 1);
string member = Console.ReadLine();
_members.Add(member);
}
Console.WriteLine("\nWaiting for incoming calls...");
}
当Program.cs通过控制台窗口与用户通信时;
值得为用户提供简短的介绍信息:
static void ShowHelp()
{
Console.WriteLine("Hello! This is a brief introduction to this project.");
Console.WriteLine("This project is about to introduce the creation of a RingGroup (with the one by one strategy), which works on the following way:");
Console.WriteLine("1., the application asks the user about the number of the RingGroup members");
Console.WriteLine("2., also asks the user, to enter the members' phone numbers");
Console.WriteLine("3., waits for an incoming call");
Console.WriteLine("4., starts to ring the first RingGroup on the list of members");
Console.WriteLine("5., if the member is busy or cannot be reached, it starts to ring an other member");
Console.WriteLine("6., if the call is being answered, it transfers the call and stops ringing the other members");
Console.WriteLine("-------------------------------------------------------------------------------");
Console.WriteLine();
}
如果有传入呼叫,则将通知用户,应用程序将为该呼叫创建一个新的RingGroupCallHandler对象,订阅其Completed方法,然后将该对象添加到处理程序列表中。
此后,它调用对象的Start方法:
static void softphone_IncomigCall(object sender, VoIPEventArgs<IPhoneCall> e)
{
Console.WriteLine("\nIncoming call from \"{0}\"!\n", e.Item.DialInfo.Dialed);
var callHandler = new RingGroupCallHandler(e.Item, _softphone, _members);
callHandler.Completed += callHandler_Completed;
lock (_callHandlers)
_callHandlers.Add(callHandler);
callHandler.Start();
}
当传入呼叫不再可用时(例如,由于已转移或呼叫者完成了呼叫等), 应从呼叫处理程序列表中删除RingGroupCallHandler对象:
static void callHandler_Completed(object sender, EventArgs e)
{
lock (_callHandlers)
_callHandlers.Remove((RingGroupCallHandler)sender);
}
为了能够使用先前创建的软件电话进行通信,需要设置一个SIP帐户 。
因此,应用程序应要求用户输入有效的SIP帐户详细信息,然后尝试注册到PBX。
有关详细说明,请查看以下代码库中的注释:
static void SipAccountInitialization(Softphone softphone)
{
var registrationRequired = true;
Console.WriteLine("\nPlease set up Your SIP account:\n");
// Asks, if a registration is required to the PBX. The default value is true.
Console.Write("Please set if the registration is required (true/false) (default: true): ");
var regRequired = Read("Registration required", false);
if (regRequired.ToLower() == "false" || regRequired.ToLower() == "no" ||
regRequired.ToLower() == "n")
{
registrationRequired = false;
}
else
{
Console.WriteLine("Registration set to required.");
}
// The SIP account needs and authentication ID, and some names as well.
Console.Write("Please set Your authentication ID: ");
var authenticationId = Read("Authentication ID", true);
// If the user only presses the Enter button, the username will be the same as the authentication ID
Console.Write("Please set Your username (default: " + authenticationId + "): ");
var userName = Read("Username", false);
if (string.IsNullOrEmpty(userName))
userName = authenticationId;
// If the user only presses the Enter button, the display name will be the same as the authentication ID
Console.Write("Please set Your name to be displayed (default: " + authenticationId + "): ");
var displayName = Read("Display name", false);
if (string.IsNullOrEmpty(displayName))
displayName = authenticationId;
// The registration password needs to be entered.
Console.Write("Please set Your registration password: ");
var registerPassword = Read("Password", true);
// Domain name as a string, for example an IP adress.
Console.Write("Please set the domain name: ");
var domainHost = Read("Domain name", true);
// Port number with the as 5060 default value.
Console.Write("Please set the port number (default: 5060): ");
int domainPort;
string port = Read("Port", false);
if (string.IsNullOrEmpty(port))
{
domainPort = 5060;
}
else
{
domainPort = Int32.Parse(port);
}
Console.WriteLine("\nCreating SIP account and trying to register...\n");
softphone.Register(registrationRequired, displayName, userName, authenticationId, registerPassword, domainHost, domainPort);
}
之后,需要一种帮助程序方法来读取输入:
private static string Read(string inputName, bool readWhileEmpty)
{
while (true)
{
string input = Console.ReadLine();
if (!readWhileEmpty)
{
return input;
}
if (!string.IsNullOrEmpty(input))
{
return input;
}
Console.WriteLine(inputName + " cannot be empty!");
Console.WriteLine(inputName + ": ");
}
}
最后,您需要使用BlockExit方法。
这不会让应用程序退出:
private static void BlockExit()
{
while (true)
{
Thread.Sleep(10);
}
}
是的,现在我们也可以使用Softphone.cs和Program.cs 。
如果需要,请在开始最后一个主要部分之前稍作休息!
:) 开发/第3部分:运用“一对一”策略建立团队
现在是最后一个主要部分,展示了
RingGroupCallHandler类。 此类负责等待传入的呼叫,开始根据首选的振铃组策略呼叫振铃组的成员,并将呼叫转移到适当的成员。的
有来电时应调用Start方法。 Start方法接受呼叫,然后通过调用StartOutgoingCalls方法开始通知振铃组成员。IPhoneCall _incomingCall;
Softphone _softPhone;
List<IPhoneCall> _calls;
ist<String> _members;
object _sync;
public RingGroupCallHandler(IPhoneCall incomingCall, Softphone softPhone, List<String> members)
{
_sync = new object();
_incomingCall = incomingCall;
_softPhone = softPhone;
_members = members;
_calls = new List<IPhoneCall>();
}
public event EventHandler<VoIPEventArgs<CallError>> CallErrorOccured;
public event EventHandler Completed;
public void Start()
{
_incomingCall.Answer();
_incomingCall.CallStateChanged += call_CallStateChanged;
StartOutgoingCalls();
}
该StartOutgoingCalls方法创建呼叫对象到每个构件和CallSequencer方法将被调用。
需要订阅CallStateChanged事件。
事件发生时,应用程序将调用OutgoingCallStateChanged方法。
(当接听来电时将使用它,并且应将其转移到振铃组中的座席。)
void StartOutgoingCalls()
{
foreach (var member in _members)
{
var call = _softPhone.CreateCall(member);
call.CallStateChanged += OutgoingCallStateChanged;
_calls.Add(call);
}
CallSequencer();
}
如果呼叫列表中有呼叫,则CallSequencer方法会逐个振铃成员(从列表中的第一个成员开始)。
如果呼叫列表中没有任何呼叫,则该方法挂断来电:
private void CallSequencer()
{
if (_calls.Count > 0)
{
var call = _calls[0];
Console.WriteLine("Ringing phone number \"{0}\"", call.DialInfo.Dialed);
call.Start();
}
else
{
Console.WriteLine("No available RingGroup member has been found.");
_incomingCall.HangUp();
}
}
OutgoingCallStateChanged方法通过使用AttendedTransfer方法将该呼叫者(即传入呼叫)转移到环组成员之一。
OutgoingCallStateChanged方法还负责从调用列表中删除该调用,然后调用OnCompleted方法。
当被叫忙或无法接通时 ,应用程序将再次调用CallSequencer方法。
void OutgoingCallStateChanged(object sender, CallStateChangedArgs e)
{
var call = (IPhoneCall)sender;
if (e.State == CallState.Answered)
{
Console.WriteLine("\nCall has been accepted by {0}.", call.DialInfo.Dialed);
Console.WriteLine("Call from \"{0}\" is being transferred to \"{1}\".\n", _incomingCall.DialInfo.Dialed, call.DialInfo.Dialed);
_incomingCall.AttendedTransfer(call);
lock (_sync)
{
_calls.Remove(call);
OnCompleted();
}
}
if (e.State == CallState.Busy || e.State == CallState.Error)
{
lock (_sync)
{
if (call != null)
{
Console.WriteLine("Ringing phone number \"{0}\" ends.", call.DialInfo.Dialed);
call.HangUp();
_calls.Remove(call);
CallSequencer();
}
}
}
}
应答呼叫并将其转移到振铃组成员后,将调用OnCompleted方法。
OnCompleted方法调用HangupOutgoingCalls方法,并设置Completed事件。
这表明呼叫转移已成功,或者呼叫方已在转移之前挂断了呼叫。
Completed事件还通知应用程序,呼叫处理程序无事可做。
(当传入呼叫在传输之前完成时,也会调用OnCompleted方法。)
void call_CallStateChanged(object sender, CallStateChangedArgs e)
{
if (e.State.IsCallEnded())
{
OnCompleted();
}
}
void OnCompleted()
{
HangupOutgoingCalls();
var handler = Completed;
if (handler != null)
handler(this, EventArgs.Empty);
}
HangupOutgoingCalls方法用于挂断所有呼出电话并将其从列表中清除:
void HangupOutgoingCalls()
{
foreach (var call in _calls)
{
call.HangUp();
}
_calls.Clear();
}
void call_CallErrorOccured(object sender, VoIPEventArgs<CallError> e)
{
Console.WriteLine("Error occured at {0}: {1}", ((IPhoneCall)sender).DialInfo.Dialed, e.Item);
var handler = CallErrorOccured;
if (handler != null)
handler(this, e);
}
这就是开发的终点!
祝贺您的新申请!
你的工作快结束了。
现在,您只需要使用新的虚拟软件电话来测试振铃组。
测试
让我们运行该应用程序以测试新虚拟虚拟电话的振铃组功能。 请注意
某些SIP帐户 (无论是台式VoIP电话,软件电话还是移动分机)应事先安装在PBX中,以便能够将成员添加到环组中。 例如,我在PBX中注册了三个SIP帐户(1000、2000、3000)。 (两个将成为一个环网组成员,而我将使用第三个来进行测试通话。)按F5后,先前写的
可以在控制台应用程序中看到介绍 。 介绍之后,该应用程序会要求您设置您的SIP帐户 ( 图2 )。现在,您需要配置虚拟软件电话和振铃组本身。 首先,设定
通过输入“ true” ( 图3 )进行注册。 此后,您需要在PBX中创建一个新的SIP帐户,该帐户将用作虚拟软件电话分机以用于振铃组(在我的情况下,这是编号为“ 4000”的SIP帐户)。 现在,通过输入所需数据在新控制台应用程序中提供相同的SIP帐户详细信息 。 域名是集团电话的IP地址, 端口号是它的端口号(通常是5060)。 提供必要的SIP帐户详细信息后,应用程序将尝试注册到PBX 。 如果提供的信息正确,则电话线状态将更改为RegistrationSucceeded 。 之后,应用程序要求您输入环组成员的数量并定义一个序列 。 此顺序确定当某人拨打响铃组的电话号码时哪个响铃(即哪个电话)将响铃。 (应用程序会将来电转接到第一个成员。如果不可用,则呼叫将转接到第二个成员,依此类推。)现在,配置已完成,应用程序正在等待来电 ( 图3 )。我的
测试电话可以在下面看到( 图4 )。 我用“ 3000”打了一个电话。 我拨打了“ 4000”(这是振铃组的电话号码-它可以用作公司的中央电话号码)。 此后,第一个振铃组成员,编号为“ 2000”的分机开始振铃。 但是,此分机忙,因此第二个成员“ 1000”开始响铃。 此分机已接受呼叫,因此来电已从“ 4000”(虚拟软件电话)转移到“ 1000”(台式VoIP电话)。这意味着您的应用程序运行良好! 恭喜您成功! :)
结论在当今瞬息万变的世界中,公司在所有业务领域中也都需要最新的软件和硬件设备,因此在与客户和其他合作伙伴进行交流时也是如此。 呼叫中心应配备有效的解决方案,以帮助实现自动化。 如果使用VoIP技术(和IP PBX),则呼叫转移,振铃组,呼叫记录,呼叫排队,电话会议等只是可以实现的一些重要功能。 在本教程中,我想通过振铃组示例演示如何轻松地自行开发缺少的功能以改善呼叫中心。 我希望你喜欢它!
进一步的阅读和参考为了撰写有关构建环网组的C#教程,我使用了以下知识库:
- http://en.wikipedia.org/wiki/Call_centre
- http://searchcrm.techtarget.com/definition/call-center
- http://www.imdb.com/title/tt1593756/...f_=tt_pv_mi_sm
- http://elderlymedicalalertsystems.co...-alert-systems
- http://en.wikipedia.org/wiki/Extension_%28telephone%29
- http://en.wikipedia.org/wiki/Call_transfer
- http://voip-sip-sdk.com/p_21-downloa...-sdk-voip.html
c# 虚拟机加密软件