官网上http://wiki.freeswitch.org/wiki/Mod_fifo关于mod_fifo模块的说明中包含了一些有关此模块的API,及其使用说明。fifo_member是其中之一。这个API分两大类,一类是add,另一类是对应的del。下面是这两类的使用说明:
add <fifo_name> <originate_string> [<simo_count>] [<timeout>] [<lag>] [expires] [taking-calls]
del <fifo_name> <originate_string>
但只有这些,没有各个参数的具体含义。如果只是这样,只能是在非常粗浅的层面来使用这个API。还好有代码,那就通过查看代码看看各个参数的含义都是什么。
fifo_member_api_function
<pre name="code" class="cpp"> if (action && !strcasecmp(action, "add")) { if (argc > 3) { simo_count = atoi(argv[3]); } if (argc > 4) { timeout = atoi(argv[4]); } if (argc > 5) { lag = atoi(argv[5]); } if (argc > 6) { expires = switch_epoch_time_now(NULL) + atoi(argv[6]); } if (argc > 7) { taking_calls = atoi(argv[7]); } if (simo_count < 0) { simo_count = 1; } if (timeout < 0) { timeout = 60; } if (lag < 0) { lag = 5; } if (taking_calls < 1) { taking_calls = 1; } fifo_member_add(fifo_name, originate_string, simo_count, timeout, lag, expires, taking_calls); stream->write_function(stream, "%s", "+OK\n"); } else if (action && !strcasecmp(action, "del")) { fifo_member_del(fifo_name, originate_string); stream->write_function(stream, "%s", "+OK\n"); } else { stream->write_function(stream, "%s", "-ERR Invalid!\n"); goto done; }
函数内这一部分的处理,也支持fifo_member只有add和del这两个子类。fifo_member_del和fifo_member_add分别是处理两个子类的函数。在此之前的处理,解析出了请求子类,存放在action变量内,解析出了fifo的名称,存放在fifo_name变量内,解析出了originate string,存放在了originate_string变量内。这三个参数是这两类子类都必须具有的参数。add子类特有的参数,在判断出是add子类后再做详细解析。
argc = switch_separate_string(mydata, ' ', argv, (sizeof(argv) / sizeof(argv[0]))); if (argc < 3) { stream->write_function(stream, "%s", "-ERR Invalid!\n"); goto done; } action = argv[0]; fifo_name = argv[1]; originate_string = argv[2];
函数开头部分的处理。这一段的处理要将originate_string字串处理后得到另一个字符数组digest。从这里还看不出这么处理的原因是什么。
if (switch_stristr("fifo_outbound_uuid=", originate_string)) { extract_fifo_outbound_uuid(originate_string, digest, sizeof(digest)); } else { switch_md5_string(digest, (void *) originate_string, strlen(originate_string)); }
继续看接下来的处理。利用fifo_name和digest数组组合一个sql语句。fifo_member API最简单的应用之一就是增加一个队列的成员:fifo_member add fifo1 user/1001。可以在fs_cli控制台下输入上述命令行执行增加队列成员的请求。
sql = switch_mprintf("delete from fifo_outbound where fifo_name='%q' and uuid = '%q'", fifo_name, digest); switch_assert(sql); fifo_execute_sql(sql, globals.sql_mutex); free(sql);
将这条实际的命令行的参数代入正在分析的函数,fifo_name的值就应该是“fifo1”,originate_name的值是“user/1001”。如果这样的话,上述sql语句中digest值由“user/1001”通过计算得到,fifo_name的值是队列名称。这条sql语句的意思就是从fifo_outbound表内删除fifo_name字段是“fifo1”、uuid字段是字串“user/1001”处理后的值的记录。可以理解为,这是增加操作所以必须确保不会有同名的记录已经存在所以先删除一条这样的记录以防异常。不过,对于fifo_outbound这个表名,我觉得有某种误导的含义,为何后加一个outbound字串?继续看代码。接着是在globals.fifo_hash哈希表内查找名为fifo_name的fifo_node_t类型的变量。如果不存在,那么就创建一个。
switch_mutex_lock(globals.mutex); if (!(node = switch_core_hash_find(globals.fifo_hash, fifo_name))) { node = create_node(fifo_name, 0, globals.sql_mutex); node->ready = 1; } switch_mutex_unlock(globals.mutex);
这与实验结果吻合,如果未在fifo.conf.xml配置文件中增加名为fifo1的队列,通过fifo_member add指令仍然可以动态增加一个名为fifo1的队列。现在不想深入到create_node函数内部实现的细节,因为这个函数与其他命令行参数貌似没有关联。得到一个fifo_node_t类型的变量后,再处理一次fifo_name。这次处理的目的是:得到一个fifo_name的副本。但这个副本经历过一些处理:如果原始fifo_name字串中包含“@”字符,那么就删除“@”及其之后的字符。
name_dup = strdup(fifo_name); if ((p = strchr(name_dup, '@'))) { *p = '\0'; }
接下来是执行一条插入语句。uuid字段是处理originate_name字串后得到的值。其他均取自命令行的参数。需要注意,此时并未用到之前处理得到的name_dup值。
sql = switch_mprintf("insert into fifo_outbound " "(uuid, fifo_name, originate_string, simo_count, use_count, timeout, " "lag, next_avail, expires, static, outbound_call_count, outbound_fail_count, hostname, taking_calls, active_time, inactive_time) " "values ('%q','%q','%q',%d,%d,%d,%d,%d,%ld,0,0,0,'%q',%d,%ld,0)", digest, fifo_name, originate_string, simo_count, 0, timeout, lag, 0, (long) expires, globals.hostname, taking_calls, (long)switch_epoch_time_now(NULL)); switch_assert(sql); fifo_execute_sql(sql, globals.sql_mutex); free(sql); free(name_dup);
此时,再回想之前的那条删除语句。删除条件是fifo_name字段和uuid字段。uuid字段来自originate_name命令行参数。目前的理解是,originate_name命令行参数是需要向队列内加入的座席。也即是说,之前那条删除语句是为了删除与此次增加语句有冲突的已存在的座席记录。最后一段处理的目的是得到表内同一个队列内的座席数。并将这个数据赋给fifo_node_t类型变量的member_count属性。以及设置has_outbound属性。如果有座席属于这个队列,那么这个队列的has_outbound属性为1。
cbt.buf = outbound_count; cbt.len = sizeof(outbound_count); sql = switch_mprintf("select count(*) from fifo_outbound where fifo_name = '%q'", fifo_name); fifo_execute_sql_callback(globals.sql_mutex, sql, sql2str_callback, &cbt); node->member_count = atoi(outbound_count); if (node->member_count > 0) { node->has_outbound = 1; } else { node->has_outbound = 0; } switch_safe_free(sql);这就是fifo_member_add函数的全部内容。目的就是向fifo_outbound表内增加一条记录。实质是为队列增加一个座席。增加一个座席对应向fifo_outbound表内增加一条记录。几乎表内的所有字段与命令行参数都是一一对应的。只有uuid字段不是这样,它的值通过处理originate_name命令行参数得到。到现在为止,仍然不知道这些参数有何意义。
fifo_member_del
这个函数的处理与fifo_member_add函数几乎一模一样。唯一不同之处在于,没有向表内插入记录的步骤。
看过来fifo_member_add和fifo_member_del函数后,对于那些命令行参数含义仍然是一无所知。由于FreeSWITCH代码的注释几乎没有,所以也没法快速地定位到实现特定功能的具体函数。试着在代码中查找调用select语句的地方。发现了一个find_consumers函数。函数名称暗示着函数意图找寻一个consumer,记得官网上有过consumer概念的解释。也许通过观察这个函数可以知道一些信息。
find_consumers
粗略看了一眼这个函数,很长。又想了想,要不先查查谁调用了这个函数。一查发现,只有一处调用了这个函数:node_thread_run。
node_thread_run
一看名字就知道这是一个线程主函数。这句说明它执行了一个有条件的无限循环,这个标志变量指示系统是否在运行。
while (globals.node_thread_running == 1) {
循环内的第一部分是循环遍历globals.nodes列表变量。这个变量之前分析看到过,那是在create_node函数内。当创建完一个fifo_node_t类型的变量后,在create_node函数内会将fifo_node_t类型的变量插入到globals.nodes列表变量的头部。因此,globals.nodes列表内存储的是fifo_node_t类型的值。一个fifo_node_t类型的变量代表着一个fifo队列。遍历列表有两个目的。第一个目的是,找到那些ready属性等于0的fifo_node_t变量,将它们从列表内取出删除。从此出可以看出,fifo_node_t的ready属性用以指明是否可以被删除(当值是0时可以从globals.nodes链表内删除)。第二个目的处理包括了调用find_consumers函数。进入第二部分处理的条件r如下:
if (this_node->has_outbound && !this_node->busy && this_node->outbound_priority == cur_priority) {
现在还不知道这些属性代表什么意思。所以就去查看申明fifo_node类型的地方,这里也没有任何注释。没有注释或者注释太少,是FreeSWTICH这个项目最大的一个软肋。因此,就想着去create_node函数内看看创建fifo_node_t变量时都赋了什么值。如果fifo_outbound表内有一条记录的fifo_name字段是待创建队列对象的名称,那么待创建队列对象的has_outbound属性为1,否则为0。所谓的有一条记录的意思就是已经有一个座席属于这个队列。
sql = switch_mprintf("select count(*) from fifo_outbound where fifo_name = '%q'", name); fifo_execute_sql_callback(mutex, sql, sql2str_callback, &cbt); node->member_count = atoi(outbound_count); if (node->member_count > 0) { node->has_outbound = 1; } else { node->has_outbound = 0; }
因此,has_outbound属性可以理解成队列内是否有座席的标志。创建时busy属性并未明确赋上任何值。outbound_priority属性也是如此。我突然想到在哪看到过priority的介绍。这是从官网上截取的片段:
Each FIFO can have 10 priority slots (default priority is 5). Priority 1 has higher priority than 10. With the FIFO slot, you can put a caller into one of the ten FIFO slots with: <action application="set" data="fifo_priority=1" />
因为priority是从1到10,所以在nod_thread_run函数内会有这么一句:
if (this_node->outbound_priority == 0) this_node->outbound_priority = 5;
将其赋值为5,缺省值。那么,outbound_priority属性代表的就是priority slot。现在再回过头看看第二部分处理的条件:这个fifo队列必须已经有一个座席,这个队列的busy属性大于0(应该是希望队列不忙),队列的outbound_priority属性值与cur_priority值相等。前两个条件都很好理解。outbound_priority属性代表的是fifo队列的priority slot概念。cur_priority的值,初始是1。在遍历完globals.nodes列表后会再次处理cur_priority的值。
if (++cur_priority > 10) { cur_priority = 1; }
现在应该很清楚了,每次遍历globals.nodes列表时,第二部分处理针对某个特定的priority slots值。值从1到10,然后再回复到1,依此循环。第二部分处理的条件基本清楚了,接下来了解处理的具体内容。
ppl_waiting = node_caller_count(this_node); consumer_total = this_node->consumer_count; idle_consumers = node_idle_consumers(this_node);
首先是计算出这三个值。node_caller_count函数计算出fifo队列内部几个队列的大小总数。但这些内部队列的用途是什么还一无所知。
fifo队列
fifo_node_t的队列存储的是switch_event_t对象。在找寻这些内部队列用途时有一个数字引起了注意,那就是队列的大小。这个值在编译期间就已经确定,它的值由宏MAX_PRI决定。
#define MAX_PRI 10
定义成10并不是随意而为。它让人想起了priority slots的取值。priority slots的值从1至10,恰好也是10。也许它们是相关的。接着看到了fifo_queue_push这个函数。有两处调用了这个函数,一处是在fifo_add_outbound函数内。
static uint32_t fifo_add_outbound(const char *node_name, const char *url, uint32_t priority) { fifo_node_t *node; switch_event_t *call_event; uint32_t i = 0; if (priority >= MAX_PRI) { priority = MAX_PRI - 1; } if (!node_name) return 0; switch_mutex_lock(globals.mutex); if (!(node = switch_core_hash_find(globals.fifo_hash, node_name))) { node = create_node(node_name, 0, globals.sql_mutex); } switch_thread_rwlock_rdlock(node->rwlock); switch_mutex_unlock(globals.mutex); switch_event_create(&call_event, SWITCH_EVENT_CHANNEL_DATA); switch_event_add_header_string(call_event, SWITCH_STACK_BOTTOM, "dial-url", url); fifo_queue_push(node->fifo_list[priority], call_event); call_event = NULL; i = fifo_queue_size(node->fifo_list[priority]); switch_thread_rwlock_unlock(node->rwlock); return i; }
函数的最后一个参数是priority,在函数体内调用fifo_queue_push函数时用到了这个参数:node->fifo_list[priority]。如果这里的priority与之前分析时看到的priority是同一个概念,那么刚才的假设就是成立的。fifo_node_t类型的内部队列数组大小与priority slots的取值是对应的。fifo_node_t类型的内部队列数组与priority slots的10个等级一一对应。那么fifo_add_outbound函数就可以解释为:依据参数node_name找到fifo_node_t类型变量,然后创建一个switch_event_t变量,最后向找到的fifo_node_t变量内部队列内存入刚创建的switch_event_t类型变量。fifo_node_t变量内部队列有十个,具体哪个队列由最后一个参数priority决定。
暂且不考虑另一处调用fifo_add_outbound函数的地方。先看看哪会调用fifo_add_outbound。只有fifo_add_outbound_function函数内会调用fifo_add_outbound函数。fifo_add_outbound_function是个回调,这可以从mod_fifo模块的加载函数内分析得出。以下是mod_fifo模块加载函数mod_fifo_load内的一句:
SWITCH_ADD_API(commands_api_interface, "fifo_add_outbound", "Add outbound members to a fifo", fifo_add_outbound_function, "<node> <url> [<priority>]");
这句的意图是向核心注册一个API。但这个API的解释性内容“Add outbound members to a filo”似乎在告诉我们这个API的目的仍然是向队列内加入一个座席。似乎这十个内部队列的作用是保存这个队列所有座席。之前的查找表明还有另一处调用fifo_queue_push的地方,那是在fifo_function函数内。
fifo_function函数是一个APP的回调函数。
SWITCH_ADD_APP(app_interface, "fifo", "Park with FIFO", FIFO_DESC, fifo_function, FIFO_USAGE, SAF_NONE);
fifo_function函数内调用fifo_queue_push的语句是这样:
fifo_queue_push(node->fifo_list[p], call_event);
p的值通过这些语句得到:
if ((pri = switch_channel_get_variable(channel, "fifo_priority"))) { p = atoi(pri); }
即,在XML中调用fifo APP前设置了fifo_priority参数,通过上述代码可以得到设置的fifo_priority值,最后将其转换成整型值。也就是说,这里的p值也是fifo_node_t类型内部队列数组的索引值。但第二个参数call_event略有不同。
switch_event_create(&call_event, SWITCH_EVENT_CHANNEL_DATA); switch_event_add_header_string(call_event, SWITCH_STACK_BOTTOM, "dial-url", url);
switch_event_create(&call_event, SWITCH_EVENT_CHANNEL_DATA); switch_channel_event_set_data(channel, call_event);
二者的创建语句完全一致,差别在各自的第二句代码。前者是增加了一个dial-url头域,后者是调用switch_channel_event_set_data函数。查看switch_channel_event_set_data函数内部及其嵌套调用的其他函数,没有发现任何与dial-url头域直接相关的代码。
现在有个疑问,无论是fifo_add_outbound_function函数还是fifo_function函数内,提交给fifo_queue_push函数的第二个参数call_event的作用是什么?上述两个创建switch_event_t对象中的第一个看上去只是创建了这样一个对象,然后是为其加上一个dial-url数据。但另一个好像很复杂。还得继续分析是哪两种情况触发了这两个创建。
fifo_add_outbound函数是上述两个创建对象的地点之一。突然觉得这么看源码有些枯燥也可能没有效率。因此想通过日志信息了解实际的代码是如何运转的。因此,运行fs_cli,打开mod_fifo模块的详细日志开关:fifo debug 9。其实,不知道9是不是输出最多信息的日志等级。但9应该足够大了。日志打开后,屏幕上不停地出现如下的日志:
2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 1 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 2 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 3 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 4 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 5 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:2025 fifo1 waiting 0 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:2025 FIFO1 waiting 0 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:2025 [email protected] waiting 0 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 6 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 7 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 8 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 9 2014-06-06 15:26:09.989207 [DEBUG] mod_fifo.c:1968 Trying priority: 10
每个队列都有十个priority slot,只前的分析也提到了node_thread_run函数内会遍历所有队列的内部十个priority slot。“Trying priority:”字串恰好是node_thread_run函数内一句日志输出语句内容。在测试通话呼入需要队列分配座席时,出现了如下的日志:
2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 1 2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 2 2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 3 2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 4 2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 5 2014-06-06 15:26:10.989182 [DEBUG] mod_fifo.c:2025 fifo1 waiting 1 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:10.989182 [CRIT] mod_fifo.c:886 sql: select uuid, fifo_name, originate_string, simo_count, use_count, timeout, lag, next_avail, expires, static, outbound_call_count, outbound_fail_count, hostname from fifo_outbound where taking_calls = 1 and (fifo_name = 'fifo1') and ((use_count+ring_count) < simo_count) and (next_avail = 0 or next_avail <= 1402068370) order by next_avail, outbound_fail_count, outbound_call_count 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:2025 FIFO1 waiting 0 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:2025 [email protected] waiting 0 consumer_total 0 idle_consumers 0 ring_consumers 0 pri 5 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 6 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 7 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 8 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 9 2014-06-06 15:26:11.989182 [DEBUG] mod_fifo.c:1968 Trying priority: 10 2014-06-06 15:26:12.009238 [DEBUG] mod_fifo.c:1547 fifo1 dialing: [leg_timeout=60,fifo_outbound_uuid=36ad351325dc8c9c60ba748f7a93fb1b,fifo_name=fifo1]user@2002,{ignore_early_media=true,outbound_redirect_fatal=true,leg_timeout=60,fifo_outbound_uuid=96f4de3e27b0e2f72d166c02348c4e04,fifo_name=fifo1}{fifo_member_wait=nowait}user/2000:_:{ignore_early_media=true,outbound_redirect_fatal=true,leg_timeout=60,fifo_outbound_uuid=cfc1ce8d5b26c0d9bc328eafc0192f1a,fifo_name=fifo1}{fifo_member_wait=nowait}user/2001:_:{ignore_early_media=true,outbound_redirect_fatal=true,leg_timeout=60,fifo_outbound_uuid=2cd0b330d16751c2985f840a1cf38365,fifo_name=fifo1}{fifo_member_wait=nowait}user/2002:_:{ignore_early_media=true,outbound_redirect_fatal=true,leg_timeout=60,fifo_outbound_uuid=3cadbefcb93cb085bcd8f6dfae286448,fifo_name=fifo1}user/2002
这里出现了一条sql语句,这条sql语句是在find_consumers函数内构建的。这也符合代码执行流,node_thread_run函数会调用find_consumers。然后与通常相比又多了一条日志记录:“fifo 1 dialing:”。在代码中查找这条日志出现的地方,是在ringall_thread_run函数内。在find_consumers函数内某一个分支下,会创建一个以ringall_thread_run为线程主函数的线程。这些日志可以帮助梳理出这样一个流程:
1、node_thread_run是个线程主函数。它会一直循环检测所有队列的内部十个priority slot,当某个队列的数据满足要求时,将调用find_consumers函数。
2、find_consumers函数首先是构建了一条select SQL语句。执行完这条SQL语句后,创建一个以ringall_thread_run为线程主函数的线程(在我的测试环境下执行了这个分支的代码,代码里还有另一个分支)。
这些日志信息显示的代码执行流正好与之前的假设符合。也就是说之前的分析方向是正确的。但这些日志并没有显示何时向priority slot内放入了数据。因此,在创建事件对象的fifo_add_outbound函数内加了一条日志输出语句。测试后没发现这条新加的日志。因此在另一个创建事件对象的fifo_function函数内又加了一条日志输出语句。再次测试后看到了fifo_function函数内这条日志。也就是说,核心需要一个座席时会调用mod_fifo模块的fifo_function函数。在mod_fifo_load函数内下面这条语句说明fifo_function函数app interface的回调函数:
SWITCH_ADD_APP(app_interface, "fifo", "Park with FIFO", FIFO_DESC, fifo_function, FIFO_USAGE, SAF_NONE);
app interface是不是就是dialplan内如出现如下的内容,则调用mod_fifo模块的fifo_function:
<action application="fifo" data="myqueue in /tmp/exit-message.wav /tmp/music-on-hold.wav"/>这个假设也可以通过增加日志输出来验证。在fifo_function函数的前部加了一句日志输出,测试后那这句日志出现了:
2014-10-06 06:14:27.608997 [DEBUG] switch_ivr_play_say.c:1717 done playing file /usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-hold_connect_call.wav EXECUTE sofia/internal/[email protected] fifo(fifo1 in) 2014-10-06 06:14:27.608997 [ERR] mod_fifo.c:2457 fifo API Args:fifo1 in
“fifo API Args:fifo1 in”,这句就是增加的那句日志。之前的一句日志是为了表明正在执行dial plan中调用app的指令,app是fifo,参数是fifo1 in。这些信息与测试环境的dial plan一致。现在验证了dial plan中调用fifo APP时,核心会调用mod_fifo模块的fifo_function回调函数。即,fifo_function是需要继续研究的那个函数,因为它在dial plan中使用fifo APP时被调用,而且它会创建一个switch_event_t对象并放入队列的内部priority slot队列内。
fifo_function
fifo_function函数的前部处理显示dial plan中调用fifo APP时,最少需传入两个参数。第一个参数是队列名称,第二个参数是方向。方向取值只有两种:in或者out。这两个参数之间用空格分隔。第一个参数也是个字串,但可以不只传递一个队列名。这个字串可以包括多个队列名,队列名间以逗号分隔。更进一步,逗号分隔的单个字串中感叹号前的字串是队列名,感叹号后的是一个整型数值。类似于这样:
fifo1!1,fifo2!2,fifo3!2 in
取出了这些队列名后,在globals.fifo_hash哈希表中查找是否存在同名的fifo_node_t对象,如果不存在则新建一个。
接着依据方向取值决定传入的其他参数含义。如果方向值是“out”,第四个参数是announce,第五个参数是music on hold。如果方向值是“out”,第三个参数是announce,第四个参数是music on hold。然后是依据方向取值分别进行相应处理。此时,最关注的是取值为“in”的情形,因此只考察这部分代码。实际处理前,为那些调用fifo APP前设置的通道变量保留一份副本。暂不考虑针对这些通道变量的处理。处理完这些通道变量后,立刻就来到了之前看到过的创建switch_event_t对象的代码。对象创建完毕后,调用switch_channel_event_set_data函数将通道已有的一些属性赋给switch_event_t对象。接着将这个新创建的switch_event_t对象放入fifo_node_t对象的priority slot对列内。
switch_event_create(&call_event, SWITCH_EVENT_CHANNEL_DATA); switch_channel_event_set_data(channel, call_event);然后调用fifo_caller_add函数,代码如下:
fifo_caller_add(node, session);session对象是fifo_function的参数之一,由核心提供给mod_fifo模块,它代表着这通呼入通话。
static void fifo_caller_add(fifo_node_t *node, switch_core_session_t *session) { char *sql; switch_channel_t *channel = switch_core_session_get_channel(session); sql = switch_mprintf("insert into fifo_callers (fifo_name,uuid,caller_caller_id_name,caller_caller_id_number,timestamp) " "values ('%q','%q','%q','%q',%ld)", node->name, switch_core_session_get_uuid(session), switch_str_nil(switch_channel_get_variable(channel, "caller_id_name")), switch_str_nil(switch_channel_get_variable(channel, "caller_id_number")), switch_epoch_time_now(NULL)); fifo_execute_sql(sql, globals.sql_mutex); switch_safe_free(sql); }
代码显示fifo_caller_add函数的本质是向fifo_callers表内插入一条记录。做完这些后,计算当前这个priority slot队列的大小。然后为当前通道设置fifo_priority通道变量,值是这个priority slot队列的大小。
然后设置一些通道变量,例如fifo_status,以及取出一些通道变量。暂不详细查看这些处理,等看到具体动作执行时使用了某些数据再回头查看。第一个动作就是播放MOH的过程。播放MOH的前提是。MOH是作为fifo APP的输入参数出现,这在fifo_function函数的前部处理已经看到过。在播放MOH过程中客户可以按退出键停止播放,退出键由通道变量设置决定,通道变量是fifo_caller_exit_key。播放MOH是在一个循环内执行,也就是说可能会播放多次。退出这个循环有几个出口。循环后会依据状态决定是否挂断通话,也有可能播放announce文件,它由fifo_announce决定。给人的感觉就是fifo_function的作用是向fifo队列内放入一个数据,然后在结束前播放一些预设的语音文件。分配动作不在这个函数内。分配工作由find_consumers函数负责处理,它也是之前观察过的一个函数,在那将生成一个查找座席的SQL语句,最终将生成一个线程用于处理SQL查询后的结果集。线程函数是ringall_thread_run。
ringall_thread_run
函数前部有两处会提前结束此次分配任务。一是不存在可以接听此次放入队列fifo通话的座席:通过检查fifo_outbound表内的uuid字段,调用check_consumer_outbound_call和check_bridge_call,这两个函数任何一个为真则说明此座席无法接听。二是检查依据队列名是否可以找到一个fifo_node_t类型的变量,如果找不到则直接结束。接着创建两个字串:uuid_list和originate_string。前一个是所有uuid的合集,uuid间用逗号分隔。originate_string字串也是所有originate_string字段的合集。并不是简单的将所有值串接在一块,每条记录的originate_string值都得先进行处理,处理完毕后再串接,分隔符是“:_:”。
然后在这个队列十个priority slot中取出switch_event_t对象。priority slot中存放的是dial plan中使用fifo APP向fifo队列放入通话后生成的switch_event_t对象。这个switch_event_t对象包括了一些来电通话的信息。这里首先选择取出包含“variable_fifo_vip”头域的对象。即优先处理vip等级的来电。ringall_thread_run虽然是个线程函数,但无无限循环处理。基于上述只取一个priority slot内的switch_event_t对象这样一个事实,可以理解为每启动一个ringall_thread_run线程,只负责处理一个来电待分配通话。在node_thread_run线程内下一次循环时再次检测此线程时可以处理其他已放入priority slot内的其他来电待分配通话。同样,在这一阶段处理时如找不到这样的一个switch_event_t对象则提前结束线程。
接着生成一个FIFO EVENT的CUSTOM事件:通知其他对此模块感兴趣的其他方,正在为一个来电通话分配座席。
接着是最重要的一个环节,调用switch_ivr_originate函数让所有空闲座席振铃。在此之前会取出这个来电的一些语音编码信息供拨打座席用。