Yate 开发向导(整理版)
1.Yate 框架设计
Yate 的设计是为了提供一个可扩展性的电话引擎,试图以最简简洁的代码,在扩展所需功能与性能、稳定性之间达到最佳平衡。
Yate 设计分为三大部分:
(1) 引擎( Engine )
该引擎已 Yate C++ 类为基础,将把所有模块组件连接在一起,上图描述了各组件之间的交互过程。
(2) 模块( Modules )
大部分功能由运行时加载的模块来实现。这些模块以动态链接库为载体,作为插件被引擎或外部程序的特定模块加载。被外部程序特定模块加载时,需能够与引擎或其他模块之间相互交互(通信)。
(3) 消息( Messages )
Yate 模块(包括插件和外部模块)之间的交互是依靠消息实现的。消息提供了一种可扩展,可定制,并且与具体技术无关的交互机制。每个模块在需要得到信息或者需要通知其他模块时只需要创建并向引擎提交消息,引擎负责会将消息传递给合适的目标。
Yate 以 Class Engine 为核心,构建了插件式的管理体系,按照观察者(发布 - 订阅)的设计模式来处理数据流。
Engine 类根据配置文件加载插件,缺省参数的情况下,会加载指定目录下所有的插件。然后运行插件的初始化函数 initialize() 完成插件的初始化。
Class Yate 提供了一些 API (静态函数)用于加载分析配置参数,加载特定模块,和指定目录下的所有模块。
// 加载指定目录下模块 参数 relPath 相对主模块的路径
bool loadPluginDir(const String& relPath);
// 注册插件,只有注册过的插件才能被初始化
bool Register(const Plugin* plugin, bool reg = true);
// 加载指定模块
bool loadPlugin(const char* file, bool local,bool nounload); private
void loadPlugins();// 从插件目录中加载插件 private
void initPlugins();// 初始化插件 private
基于列表的发布 - 订阅示例由客户端、服务和数据源程序组成。可以有多个客户端和多个数据源程序同时运行。客户端订阅服务、接收通知,然后取消订阅。数据源程序向服务发送将与所有当前订户共享的信息。
Class Engine::m_dispatcher::m_handles 维护着订阅者列表,每个订阅者都实现了以下接口用于接收通知。
class MessageHandler
{
virtual bool received(Message& msg) = 0;
}
Class Engine 还提供了一些列的 API (静态成员函数)
// 安装注册订阅者的接口,参数指定订阅的事件类型
static bool install(MessageHandler* handler);
static bool uninstall(MessageHandler* handler);
// 发送事件通知,所有注册了该事件类型的都有机会得到事件内容
//enqueue 为非阻塞函数,即将事件通知加入消息列表中,不关系事件处理的结果
//dispatch 为阻塞函数,即必须等到订阅者处理完事件才能返回。
static bool enqueue(Message* msg);
static bool dispatch(Message* msg);
一般调用 enqueue 把 message 放进了一个消息队列里,再由一个 Dispatcher 类来对消息顺序处理,相当于向所有订阅者发布消息。
插件必须从 Module 类派生,并实现 initialize() 这个虚函数,另外,根据需要实现 Message 的处理类MessageHandler 。在 initialize() 里,按照主题安装 MessageHandler ,如下所示:
Engine::install(new AuthHandler(s_cfg.getIntValue("general","auth_priority",70)));
上例的 AuthHandler 即是从 MessageHandler 中派生而来,它必须实现 received() 虚函数,以处理接收到的message 。如果处理成功,则 received() 应该返回 true ,否则返回 false 。
如果某类 DrModule 需要处理多个事件,首先订阅者必须从 MessageReceiver 派生, Module 就是从MessageReceiver 派生的,可以处理多了事件。另外消息处理类必须由 MessageRelay 派生,当然 MessageRelay,也是从 MessageHandler 的。同样在 Dr-Module::initialize 中,安装如下
TokenDict DrModule::s_messages[] = {
{ "engine.halt", DrModule::Halt },
{ "call.progress", DrModule::Progress },
{ "call.route", DrModule::Route },
{ "chan.text", DrModule::Text },
{ "msg.route", DrModule::ImRoute },
{0,0}
}
DrModule::installRelay(Halt);
DrModule::installRelay(Progress);
DrModule::installRelay(Route,200);
DrModule::installRelay(Text);
DrModule::installRelay(ImRoute);
DrModule::installRelay(ImExecute);
YATE 的架构是典型的发布 - 订阅机制,很好的实现了平台的可扩充性,减少了模块与平台、模块与模块之间的耦合,可以说架构是非常清晰的。比如用户认证的功能, YATE 里有三个模块提供三种方式对用户进行认证:文件方式( Regfile ), Radius 方式,数据库方式。这三种方式都是接收并处理 "user.auth" 、 "user.register" 等消息,其先后顺序根据 install 时定的优先级排序。处理的结果由 message 的 retValue() 带回。
这种处理机制虽然优点很突出,但性能上的缺点也很明显,因为在对消息队列的处理是单线程而且要对所有订阅者进行遍历,效率比较低。
2 Yate 中的消息
在 Yate 中,消息取代函数成为模块间主要的交互方式。这样的好处在于,当一个模块改变时,其他独立的模块不用做任何修改。另外,因为我们能够轻松的跟踪到消息的传递过程,所以调试起来相对容易。
消息包括以下几个组成部分:
(1) 名字( name ) —— 消息类型的标识,允许消息处理器通过名字进行匹配
(2) 返回值( return value ) —— 一个用字符串表示的处理消息之后的返回值
(3) 时间( time ) —— 消息被创建的时间;这对于排队的消息检查等待时长非常重要
(4) 参数( parameters ) —— 可能有多个或 0 个参数组成,每个参数由名称、值对构成。每个处理器都能根据参数进行不同的动作,或者修改参数本身。未定义参数必须忽略。
所有的消息在 YATE 内部是二进制形式的。然而我们可以通过 rmanager 模块提供一个对人可读的形式。
YATE 内部消息传递通过内存共享( memory sharing )的方式,提高系统的性能。其他传递方式如管道或Sockets ,没有内存共享灵活和高效。当被传递到外部模块( external modules )时,消息可被转换成字符串编码的形式,这样所有能处理文本的外部模块都可以处理消息了。 可参考文档 external module ,获取更多详细信息。
消息被消息处理器( MessageHandler )处理。消息处理器接收名字匹配的消息,可以对其中的组成部分进行修改,然后停止处理此消息(释放),或让此消息滑动到下一个操作者。
消息处理器接收消息分发器通知的顺序在其向引擎注册时提供的优先级决定。优先级数字越小,优先级越高。对于相同优先级的消息处理器,调用顺序是不确定的。
调用顺序按以下的规则:
* 同名的消息调用顺序是不会改变的
* 为了避免不确定性,如果消息处理器被移除,并插入一个同等优先级的消息处理器,则他们的顺序由她的的内存地址决定。
2.1 消息系统工作示例
以下是 “call.rotue” 在消息系统重的工作过程例子
当某个电话打来时,消息是这样产生的:
1. Message *m = new Message("call.route");
2. m->addParam("driver","iax");
3. if (e->ies.calling_name)
4. m->addParam("callername",e->ies.calling_name);
5. else
6. m->addParam("callername",e->session->callerid);
7. if (e->ies.called_number)
8. m->addParam("called",e->ies.called_number);
9. else
10. m->addParam("called",e->session->dnid);
然后我们将消息发送给引擎,检查是否有模块( module )接收并处理了,最后必须将消息销毁。
1. if (Engine::dispatch(m))
2. Output("Routing returned: %s",m->retValue().c_str());
3. else
4. Output("Nobody routed the call!");
5. m->destruct();
上面的处理方式是阻塞式的,模块发送消息之后需要等待该消息被发送之后才进行后续的处理。 Yate 中还有一种“发射后忘记”( fire-and-forget )的消息机制,非阻塞式消息机制,这种消息被存储在引擎中的一个队列中,当消息被分发后,由引擎负责释放。这种消息一般都是事关系统全局的重要消息,例如错误报警,如下代码所示:
1. Message *m = new Message("alert");
2. m->addParam("reason","Hardware malfunction");
3. Engine::enqueue(m);
如果我们编写的模块需要处理一个路由请求(极有可能是其他模块产生的),我们首先需要声明一个名为RouteHandler 的类,并从MessageHandler 基类继承。
1. class RouteHandler : public MessageHandler
2. {
3. public:
4. RouteHandler(int prio)
5. : MessageHandler("call.route",prio) { }
6. virtual bool received(Message &msg);
7. };
然后,由于在 received 方法中实现,他是类 MessageHandler 中是纯虚函数,我们必须重载。
1. bool RouteHandler::received(Message &msg)
2. {
3. const char *driver = msg.getValue("driver")
4. Debug(DebugInfo,"Route for driver %s",driver);
5. // don't actually route anything, let message continue
6. return false;
7. }
最后,在插件的 initialized 方法中,安装此消息处理器
1. void RegfilePlugin::initialize()
2. {
3. m_route = new RouteHandler(priority);
4. Engine::install(m_route);
5. }
这样,该插件就能处理“ call.route ”消息了,这个例子中实际上只是接收了消息但没有做任何动作,如果需要什么操作,在 received 方法里实现即可。 Yate 中的几乎所有消息操作者都是按照这样的框架实现的。
2.2 消息流示例
以呼叫进入为例:
路由
当一个通道模块检测到有呼叫进入 (1) ,它便发送 call.route(2) 消息来决定将此呼叫路由到哪个位置。Call.route 消息将被叫号码映射到一个呼叫目标。
连接
当呼叫对象已知以后,呼入通道将其呼叫端点( CallEndPoint )附在 call.execute 消息上 (4) 。接收方应该将它的呼叫端点连接到 call.execute 中携带的呼叫端点上。在等待对端接受呼叫期间应该发送 call.ringing 消息 (6) ,当呼叫被接受时, call.ansered 被发送。
会话期间
在会话期间, chan.dtmf 消息 (8,9) 能在两个方向上发送。
挂机
当呼入通道检测到挂机 (10) ,它将断开其呼叫端点。断开呼叫端点将引发两个通道 chan.disconnected 消息和chan.hangup 消息的(图中未包括)发送。
从消息流的示例我们可以看到, Yate 对呼叫的抽象很清晰,在逻辑上符合人们的思维习惯,比较容易理解。
2.3 消息类型
1. 引擎消息( Engine messages )
engine.start 由引擎发送给普通模块,通知他们 Yate 准备就绪,并已进入主循环
engine.halt
engine.init
engine.busy
engine.help
engine.command
engine.status
engine.timer
2. 通道消息( Channel messages )
chan.attach
chan.connected
chan.disconnected
chan.dtmf 双音多频信号( Dual-tone multi-frequency signaling )
chan.hangup
chan.masquerade
chan.notify
chan.record
chan.rtp
chan.startup
chan.text
chan.connect
chan.locate
chan.control
chan.replaced
3. 呼叫消息( Call messages )
call.answered
call.cdr
call.drop
call.execute
call.progress
call.ringing
call.route
call.preroute
call.update
call.conference
4. 用户消息( User messages )
user.account
user.auth
user.login
user.notify
user.register
user.unregister
5. 资源描述 / 通知消息 (Resource subscribe/notify messages)
resource.subscribe
resource.notify
6. SIP 消息 (SIP messages)
sip.<methodname>
xsip.generate
7. 编解码特定协议消息 (Encode or decode protocol specific messages)
isup.decode
isup.encode?
isup.mangle
8. 网络操作消息( Socket operation messages )
socket.stun
socket.ssl
socket.peel?
9. 集群相关消息( Clustering related messages )
cluster.locate
10. 即时信息相关消息( Instant messaging related messages )
msg.route?
msg.execute?
11. Jabber / XMPP messages
xmpp.generate?
xmpp.iq
12. 其他消息( Miscellaneous messages )
database
monitor.query
monitor.notify
3.Yate 模块创建方法
在实际应用中,可能需要编写自己的模块插入到系统中或者替换掉自己的模块,例如,使用商用的SIP 协议栈替代Yate 中开源的SIP 协议栈,所以,编写在Yate 框架下编写自己的模块是使用Yate 平台必备的技能。
上面提到,Yate 可以分为核心和模块,核心提供了系统的基础,帮助API 和消息系统,而模块则使用核心提供的功能实现特定的目标。
Yate 的模块可以分为以下几种类型:
(1) 通道(Channel )
(2) 路由器(Router )
(3) 呼叫记录器(Call Detail Recorder,CDR)
(4) 计费应用(Billing application )
(5) 其他模块( Other modules )
a. 确定模块的类型
模块类型在这里涉及许多概念性的东西。 Yate 的设计中并不区分模块的种类,而是根据模块处理的消息类型来区分模块类型。例如一个通道模块接受 call.execute 消息,并创建一个通道来接受处理它。有此特征的模块我们称之通道模块。另一方面即使如果模块可能接受 call.execute 消 息并处理一个事情,但并不创建一个通道 / 终端 , 则它不是一个通道模块。 CDRBuilder 就是这样的模块。如果你还不清楚,稍等,接下来的例子会说明清楚。
b. 程序员眼中 Yate 消息
消息应正确派发到注册了并在监听该消息的模块中。模块可以指定接受消息的优先级。如果一模块监听的call.execute 消息优先级为 50 ,其他模块也 在监听 call.execute 消息,但优先级值大于 50 ,则该模块应该先于其他模块获取 call.execute 消息。一旦接收到该消息,模块可向派发 器返回 true 或 fale ,并附带一些额外信息。如果返回 true ,则派发器停止向后续的模块发送消息。返回 false ,则允许消息按照优先级继续派发到 其他模块中。
Yate 消息不会同 Windows 消息相混淆,因为他只在 Yate 系统范围内发送而没有使用操作系统机制发送消息。此外, Yate 消息构造是以字符串定义的,而 OS 消息使用的是数值。
c. TelEngine 命名空间
所有的 Yate API 都什么在 TelEngine 命名空间中,命名空间是 C++ 的一个概念,用于减少程序中变量名和函数名重名冲突。因此,要使用命名空间 TelEngine ,如不想写 TelEngine::blahblah, 你可以在程序前面加上:
using namespace TelEngine;
3.1 创建 Yate 模块
我们现在开始写我们的第一个模块,接受 call.execute 消息,并将其呼叫和被叫的号码输出到控制台中。我们设模块名为 mymodule1.ln 。在这个模块中我们需要讨论以下几个类。
*Module
*Message
*MessageReceiver
*String
同样还得介绍 call.route
前面提到,所有的 Yate 模块需要从 Module 类继承。 Module 本身从另外一些类继承。
1. class YATE_API Module : public Plugin, public Mutex, public MessageReceiver, public DebugEnabler
2. ...
注意 Plugin 类提供了 Yate 加载模块的支持。
模块定义类如下:
第二步:创建 Module 类的静态变量
当 Yate 模块加载器(在引擎中)加载模块时,它会寻找 Plugin 类型的静态变量,名为 __Plugin 。因此我们需要定义一个我们创建的模块类( mymoudle1 ,从 Plugin 继承)的一个实例的静态变量。 Yate 提供了一个帮助宏来实现这个定义:
INIT_PLUGIN(MyModule);
第三步:实现 initialize 函数
你需特别注意到我们在 MyModule 声明定义的 initialize 方法。 initialize 函数在 Plugin 类是虚函数,在 Yate 加载模块时会被调用。 MyModule 需要重载它,做一些模块初始化工作。通常在这里注册模块想监听的消息。我们可这样写代码:
Output 是 Yate 提供的输出到控制台的函数。需要的是,不应该过多的使用 Output , Yate 另外提供许多 API 可以分级别的将调试信息显示到控制台上,比 Output 更加灵活实用。
第四步:添加消息接收代码
模块通常需要接受一个或多个消息。模块接受到一个消息并执行程序特定的工作。 Module 类派生与MessageReceiver , MessageReceiver 通过虚函数 received() 提供了接受消息能力。我们可按自己爱好添加代码接受消息,像这样:
Engine 是 Yate API 中的一个类。 Yate 在启动时(不是模块启动时)创建一个 Engine 类的静态对象。这个类启动整个 Yate 的服务器 / 服务。这个类函数全是静态成员函数。 Install() 同样是静态函数,这也就是我们为什么Engine::install 这样调用的原因。 install 向 yate 提供了我们的 MessageRelay 类对象,指定了我们想要监听消息的消息 id 、监听优先级等。第三个参数 CallRoute 是枚举值,它仅关联消息和消息 ID 。枚举值 CallRoute 为Private+1 ,在类 Module 中定义,用于指定(说明)该消息在模块中没有任何 RelayID 。当你监听到多个消息,你需要通过消息 ID 识别监听到的消息,当然也有其他方式实现。注意在 MessageRelay 的最后一个参数值为 55 ,这说明我们想监听的消息优先级别值为 55 。因此如果其他模块也在监听同样的消息且优先级更高,将先接收 到这个消息并且如果他返回非 ture, 我们的这个模块才能接受到这个消息。 MessageRelay 被 Yate 用例发送消息,它的构造函数拥有一个 MessageReceiver 类的对象用户消息的通知。我们 MyModule 继承于 Module ,而 Module 继承于MessageReceiver ,因此,我们在 MyModule 可提供这个对象( MessageReceiver )。
第五步:重载 received
MessageReceiver 类的 received 函数在消息被发送到将已经在引擎注册监听该消息的模块时被 Yate 的消息分发器调用。 Received 是一个虚函数,在 MessageReceiver 类中定义。注意它的返回值是一个布尔值,指示了该消息是否继续发送给下一个操作者。这里我们选择 false ,让消息继续传给下一个操作者,使正确的路由模块对其进行路由。 这里( received )我们可以添加自己的逻辑代码。在 received 中我们可写一些代码,在 call.route 消息来临时,输出呼叫者和被呼叫者名。
上述代码使用了 Yate API 提供的 String 类。从名可知这个类提供了字符串的相关操作。 Message 继承与NamedList ,这是 YATE 提供的另一个类,用于更好管 理字符串与字符串直接的映射链表。类 NamedList 的函数getValue() ,我们先获取被叫号码 ( 熟知的 DNID) ,然后获取呼叫者的号码,作为 电信运营商的 ANI/CLI (被叫者ID/ 呼叫者 ID )。 c_str 为 String 的成员函数,返回字符串存储的数据,类型为 const char*.
最后,完整代码如下
1. #include <yatengine.h>
2. #include <yatephone.h>
3. #include <stdio.h>
4. #include <stdlib.h>
5. using namespace TelEngine;
6. class MyModule : public Module
7. {
8. public:
9. MyModule()
10. : Module("MyModule1","misc"), m_init(false) {}
11. virtual void initialize();
12. virtual bool received(Message &msg,int id);
13. private:
14. enum
15. {
16. CallRoute = Private + 1
17. } PrivateRelayID;
18. bool m_init;
19. };
20. bool MyModule::received(Message &msg,int id)
21. {
22. String called,caller;
23. called = msg.getValue("called");
24. caller = msg.getValue("caller");
25. Output("mymodule1: call.route called
26. caller %s, and called %s",caller.c_str(),called.c_str());
27. return false;
28. }
29. void MyModule::initialize()
30. {
31. Output("Initializing Module mymodule1");
32. if(!m_init) {
33. Engine::install(new MessageRelay("call.route",this,CallRoute,55));
34. m_init = true;
35. }
36. }
37. INIT_PLUGIN(MyModule);
38. /* vi: set ts=8 sw=4 sts=4 noet: */