和以前版本相比,Asterisk在架构上有了不小的变动,本文基于asterisk 1.8.10.1分析整理。
chan_sip模块属于通道驱动模块。它实现了协议的相关内容,使Asterisk能够和支持SIP协议的其它设备通信。在1.8版本下,还是没有实现S/MIME的内容,有部分代码实现了TCP和TLS,但我没用过。对SIP事务的支持,还是不好。
在chan_sip.c文件的顶部,简要地描述了这个模块实现的功能和一些缺陷,并描述了这个模块的发展计划,这里不重复这些内容。
说明,下面代码引用处的行号来源于asterisk社区doxygen生成的文档,因为代码更新同步原因,可能和您看到的实际代码略有差异
模块初始化
Asterisk是模块化设计的,内核会负责管理外围的模块。内核管理模块信息的回调函数,封装在ast_module_info这个数据结构中。这个数据结构的实例化过程,定义了一个宏,叫做AST_MODULE_INFO,所有的外围模块都会调用这个宏。在chan_sip.c中,实例化的代码是
31889 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Session Initiation Protocol (SIP)",
31890 .load = load_module,
31891 .unload = unload_module,
31892 .reload = reload,
31893 .load_pri = AST_MODPRI_CHANNEL_DRIVER,
31894 .nonoptreq = "res_crypto,chan_local",
31895 );
上面说明了模块的加载函数入口load_module。当内核加载SIP模块时,就会跳转到这里执行。这个函数的原型是int load_module(void)。
load_module首先初始化一系列的容器,1.8版本里,容器采用哈希表,不过这个表不能自动调整大小。容器实例主要有peers、dialogs等,这些在模块范围内是全局的。
接下来,创建调度管事器和POLL模型的IO管理器。接下来,调用reload_config(sip_reloadreason)读取模块的配置文件。
31600 can_parse_xml = sip_is_xml_parsable();
31601 if (reload_config(sip_reloadreason)) { /* Load the configuration from sip.conf */
31602 return AST_MODULE_LOAD_DECLINE;
31603 }
UDP是缺省支持的,如果配置文件里配置了TCP或TLS支持,那么在reload_config里会初始化相应的IO。
接下来,注册通道类型和一系列的回调函数入口,比如说调用ast_rtp_glue_register设置RTP引擎相关的回调函数。最后,调用restart_monitor()来创建SIP监听线程。restart_monitor还在另外两个地方被调用,一个是sip_request_call,一个是sip_reload。
在restart_monitor函数中,调用ast_pthread_create_background创建一个新的线程,线程ID记录在monitor_thread,线程执行体是do_monitor。
26939 /* Start a new monitor */
26940 if (ast_pthread_create_background(&monitor_thread, NULL, do_monitor, NULL) < 0) {
26941 ast_mutex_unlock(&monlock);
26942 ast_log(LOG_ERROR, "Unable to start monitor thread.\n");
26943 return -1;
26944 }
do_monitor首先调用ast_io_add添加一个IO实体,把IO的FD记录在sipsock_read_id,并注册IO回调函数sipsock_read,这个回调是SIP信令的入口。然后进入线程循环体。循环中,首先检查是否需要重新加载模块,然后检查容器中需要挂断的通话(比如说RTP监测超时)。
现在,回头来看reload_config这个函数,这里关注一下TCP和TLS初始化,如果对这部分不关心,可以跳过:
TCP
30000 /* Start TCP server */
30001 if (sip_cfg.tcp_enabled) {
30002 if (ast_sockaddr_isnull(&sip_tcp_desc.local_address)) {
30003 ast_sockaddr_copy(&sip_tcp_desc.local_address, &bindaddr);
30004 }
30005 if (!ast_sockaddr_port(&sip_tcp_desc.local_address)) {
30006 ast_sockaddr_set_port(&sip_tcp_desc.local_address, STANDARD_SIP_PORT);
30007 }
30008 } else {
30009 ast_sockaddr_setnull(&sip_tcp_desc.local_address);
30010 }
30011 ast_tcptls_server_start(&sip_tcp_desc);
30012 if (sip_cfg.tcp_enabled && sip_tcp_desc.accept_fd == -1) {
30013 /* TCP server start failed. Tell the admin */
30014 ast_log(LOG_ERROR, "SIP TCP Server start failed. Not listening on TCP socket.\n");
30015 } else {
30016 ast_debug(2, "SIP TCP server started\n");
30017 }
TLS
30019 /* Start TLS server if needed */
30020 memcpy(sip_tls_desc.tls_cfg, &default_tls_cfg, sizeof(default_tls_cfg));
30021
30022 if (ast_ssl_setup(sip_tls_desc.tls_cfg)) {
30023 if (ast_sockaddr_isnull(&sip_tls_desc.local_address)) {
30024 ast_sockaddr_copy(&sip_tls_desc.local_address, &bindaddr);
30025 ast_sockaddr_set_port(&sip_tls_desc.local_address,
30026 STANDARD_TLS_PORT);
30027 }
30028 if (!ast_sockaddr_port(&sip_tls_desc.local_address)) {
30029 ast_sockaddr_set_port(&sip_tls_desc.local_address,
30030 STANDARD_TLS_PORT);
30031 }
30032 ast_tcptls_server_start(&sip_tls_desc);
30033 if (default_tls_cfg.enabled && sip_tls_desc.accept_fd == -1) {
30034 ast_log(LOG_ERROR, "TLS Server start failed. Not listening on TLS socket.\n");
30035 sip_tls_desc.tls_cfg = NULL;
30036 }
30037 } else if (sip_tls_desc.tls_cfg->enabled) {
30038 sip_tls_desc.tls_cfg = NULL;
30039 ast_log(LOG_WARNING, "SIP TLS server did not load because of errors.\n");
30040 }
30041
这两个处理,都用到了一个关键的结构体ast_tcptls_session_args,实例名字分别是sip_tcp_desc和sip_tls_desc,看一下它们是怎样实例化的。
02213 static struct ast_tcptls_session_args sip_tcp_desc = {
02214 .accept_fd = -1,
02215 .master = AST_PTHREADT_NULL,
02216 .tls_cfg = NULL,
02217 .poll_timeout = -1,
02218 .name = "SIP TCP server",
02219 .accept_fn = ast_tcptls_server_root,
02220 .worker_fn = sip_tcp_worker_fn,
02221 };
02222
02223 /*! \brief The TCP/TLS server definition */
02224 static struct ast_tcptls_session_args sip_tls_desc = {
02225 .accept_fd = -1,
02226 .master = AST_PTHREADT_NULL,
02227 .tls_cfg = &sip_tls_cfg,
02228 .poll_timeout = -1,
02229 .name = "SIP TLS server",
02230 .accept_fn = ast_tcptls_server_root,
02231 .worker_fn = sip_tcp_worker_fn,
02232 };
跟踪一下工作回调函数sip_tcp_worker_fn,会发现,最终调用了handle_request_do函数处理读取的数据,无论传输层用什么协议,数据都是调用这个函数处理的。
内核接口
01612 /*! \brief Definition of this channel for PBX channel registration */
01613 struct ast_channel_tech sip_tech = {
01614 .type = "SIP",
01615 .description = "Session Initiation Protocol (SIP)",
01616 .properties = AST_CHAN_TP_WANTSJITTER | AST_CHAN_TP_CREATESJITTER,
01617 .requester = sip_request_call, /* called with chan unlocked */
01618 .devicestate = sip_devicestate, /* called with chan unlocked (not chan-specific) */
01619 .call = sip_call, /* called with chan locked */
01620 .send_html = sip_sendhtml,
01621 .hangup = sip_hangup, /* called with chan locked */
01622 .answer = sip_answer, /* called with chan locked */
01623 .read = sip_read, /* called with chan locked */
01624 .write = sip_write, /* called with chan locked */
01625 .write_video = sip_write, /* called with chan locked */
01626 .write_text = sip_write,
01627 .indicate = sip_indicate, /* called with chan locked */
01628 .transfer = sip_transfer, /* called with chan locked */
01629 .fixup = sip_fixup, /* called with chan locked */
01630 .send_digit_begin = sip_senddigit_begin, /* called with chan unlocked */
01631 .send_digit_end = sip_senddigit_end,
01632 .bridge = ast_rtp_instance_bridge, /* XXX chan unlocked ? */
01633 .early_bridge = ast_rtp_instance_early_bridge,
01634 .send_text = sip_sendtext, /* called with chan locked */
01635 .func_channel_read = sip_acf_channel_read,
01636 .setoption = sip_setoption,
01637 .queryoption = sip_queryoption,
01638 .get_pvt_uniqueid = sip_get_callid,
01639 };
这些函数,实现了内核的回调接口,在模块初始化时,会把这些函数的入口注册给内核的相关管理器。
入呼处理流程
前面说过,sipsock_read是UDP SIP消息的入口。调用ast_recvfrom(recvfrom函数的封装),从socket上读取数据包。把数据存储在一个sip_request结构体对象的data字段中,然后调用handle_request_do函数处理读取的数据,这里面,很多地方的request泛指了SIP消息,包括response,光看字面,很容易误解。
首先,解析SIP消息:
26367 if (parse_request(req) == -1) { /* Bad packet, can't parse */
26368 ast_str_reset(req->data); /* nulling this out is NOT a good idea here. */
26369 return 1;
26370 }
SIP消息解析完毕之后,匹配SIP的方法,在asterisk里,把response消息也当成一种方法来处理。接着调用find_call检索消息对应的sip_pvt结构,如果原先没有,find_call函数中会创建一个新的,最终把指针返回。紧接着,通过pvt结构中的owner字段,判断消息是否经过权鉴,最后调用handle_incoming函数,对消息进一步的处理:
26371 req->method = find_sip_method(REQ_OFFSET_TO_STR(req, rlPart1));
26372
26373 if (req->debug)
26374 ast_verbose("--- (%d headers %d lines)%s ---\n", req->headers, req->lines, (req->headers + req->lines == 0) ? " Nat keepalive" : "");
26375
26376 if (req->headers < 2) { /* Must have at least two headers */
26377 ast_str_reset(req->data); /* nulling this out is NOT a good idea here. */
26378 return 1;
26379 }
26380 ast_mutex_lock(&netlock);
26381
26382 /* Find the active SIP dialog or create a new one */
26383 p = find_call(req, addr, req->method); /* returns p with a reference only. _NOT_ locked*/
26384 if (p == NULL) {
26385 ast_debug(1, "Invalid SIP message - rejected , no callid, len %zu\n", ast_str_strlen(req->data));
26386 ast_mutex_unlock(&netlock);
26387 return 1;
26388 }
26389
26390 /* Lock both the pvt and the owner if owner is present. This will
26391 * not fail. */
26392 owner_chan_ref = sip_pvt_lock_full(p);
26393
26394 copy_socket_data(&p->socket, &req->socket);
26395 ast_sockaddr_copy(&p->recv, addr);
26396
26397 /* if we have an owner, then this request has been authenticated */
26398 if (p->owner) {
26399 req->authenticated = 1;
26400 }
26401
26402 if (p->do_history) /* This is a request or response, note what it was for */
26403 append_history(p, "Rx", "%s / %s / %s", req->data->str, sip_get_header(req, "CSeq"), REQ_OFFSET_TO_STR(req, rlPart2));
26404
26405 if (handle_incoming(p, req, addr, &recount, &nounlock) == -1) {
26406 /* Request failed */
26407 ast_debug(1, "SIP message could not be handled, bad request: %-70.70s\n", p->callid[0] ? p->callid : "<no callid>");
26408 }
下面看一下handle_incoming函数,它首先对消息的合法性做一些检查,如果是应答消息则走入应答消息处理分枝,否则,继续向下处理:
26064 if (req->method == SIP_RESPONSE)
......
26099 handle_response(p, respid, e + len, req, seqno);
这其中,还检查消息是否为重发消息,如果是,填充重发标识:
else if (p->icseq &&
26143 p->icseq == seqno &&
26144 req->method != SIP_ACK &&
26145 (p->method != SIP_CANCEL || p->alreadygone)) {
26146 /* ignore means "don't do anything with it" but still have to
26147 respond appropriately. We do this if we receive a repeat of
26148 the last sequence number */
26149 req->ignore = 1;
26150 ast_debug(3, "Ignoring SIP message because of retransmit (%s Seqno %u, ours %u)\n", sip_methods[p->method].text, p->icseq, seqno);
26151 }
根据3261规范定义,检查消息的一些通用合法性之后,根据请求的method,调用各自的处理函数:
26199 /* Handle various incoming SIP methods in requests */
26200 switch (p->method) {
26201 case SIP_OPTIONS:
26202 res = handle_request_options(p, req, addr, e);
26203 break;
26204 case SIP_INVITE:
26205 res = handle_request_invite(p, req, debug, seqno, addr, recount, e, nounlock);
......
}
下面,把注意力集中在handle_request_invite函数。
首先,查看消息里的Supported和Require头域,看asterisk能否支持对方所要求的扩展。
23101 if (ast_test_flag(&p->flags[0], SIP_OUTGOING) && p->owner && (p->invitestate != INV_TERMINATED && p->invitestate != INV_CONFIRMED) && ast_channel_state(p->owner) != AST_STATE_UP)
上面这个判断的处理就是检查SIP扩展的。
23150 if (!req->ignore && p->pendinginvite)
这个判断请求的处理情况,是否正在处理。
接下来,检查是否有Replaces头域,如果有,做呼叫转移处理,里面调用到一个叫handle_invite_replaces的函数:
23192 p_replaces = sip_get_header(req, "Replaces");
23193 if (!ast_strlen_zero(p_replaces)) {
23194 /* We have a replaces header */
........
}
接下来的处理,有两种情况,一个是re-invite,一个是初始的invite。这两种情况的处理,有相同的地方,又有差异。代码中先处理不同的部分,再处理相同的部分;先处理re-invite,再处理原始INVITE。
23339 if (!req->ignore) {
23340 int newcall = (p->initreq.headers ? TRUE : FALSE);
23341
23342 if (sip_cancel_destroy(p))
23343 ast_log(LOG_WARNING, "Unable to cancel SIP destruction. Expect bad things.\n");
23344 /* This also counts as a pending invite */
23345 p->pendinginvite = seqno;
23346 check_via(p, req);
23347
23348 copy_request(&p->initreq, req); /* Save this INVITE as the transaction basis */
23349 if (sipdebug)
23350 ast_debug(1, "Initializing initreq for method %s - callid %s\n", sip_methods[req->method].text, p->callid);
23351 if (!p->owner) { /* Not a re-invite */
23352 if (debug)
23353 ast_verbose("Using INVITE request as basis request - %s\n", p->callid);
23354 if (newcall)
23355 append_history(p, "Invite", "New call: %s", p->callid);
23356 parse_ok_contact(p, req);
23357 } else { /* Re-invite on existing call */
......
}}
这一段处理re-invite的情况,主要是关于SDP变化处理什么的。
23417 if (!p->lastinvite && !req->ignore && !p->owner) {
23418 /* This is a new invite */
23419 /* Handle authentication if this is our first invite */
23420 int cc_recall_core_id = -1;
23421 set_pvt_allowed_methods(p, req);
23422 res = check_user_full(p, req, SIP_INVITE, e, XMIT_RELIABLE, addr, &authpeer);
23423 if (res == AUTH_CHALLENGE_SENT) {
23424 p->invitestate = INV_COMPLETED; /* Needs to restart in another INVITE transaction */
23425 goto request_invite_cleanup;
23426 }
......
这一段处理原始INVITE的情况。check_user_full这个函数调用,检查被叫是否是合法的用户,还完成了RPT引擎的初始化。调用栈是:check_user_full-->check_peer_ok-->dialog_initialize_rtp-->ast_rtp_instance_new(还可能是check_user_full直接调用dialog_initialize_rtp)。RTP引擎的处理是Asterisk构架中比较重大的变化,缺省使用asterisk的RTP栈(称之为引擎),但允许用户嵌入自己的RTP栈。
check_user_full返回之后,权鉴通过,设置标识位,然后处理SDP,如果请求中不带SDP,则可能是3PCC的流程:
23450 /* We have a successful authentication, process the SDP portion if there is one */
23451 if (find_sdp(req)) {
23452 if (process_sdp(p, req, SDP_T38_INITIATE)) {
23453 /* Asterisk does not yet support any Content-Encoding methods. Always
23454 * attempt to process the sdp, but return a 415 if a Content-Encoding header
23455 * was present after processing fails. */
23456 if (!ast_strlen_zero(sip_get_header(req, "Content-Encoding"))) {
23457 transmit_response_reliable(p, "415 Unsupported Media type", req);
23458 } else {
23459 /* Unacceptable codecs */
23460 transmit_response_reliable(p, "488 Not acceptable here", req);
23461 }
23462 p->invitestate = INV_COMPLETED;
23463 sip_scheddestroy(p, DEFAULT_TRANS_TIMEOUT);
23464 ast_debug(1, "No compatible codecs for this SIP call.\n");
23465 res = INV_REQ_ERROR;
23466 goto request_invite_cleanup;
23467 }
23468 } else { /* No SDP in invite, call control session */
23469 ast_format_cap_copy(p->jointcaps, p->caps);
23470 ast_debug(2, "No SDP in Invite, third party call control\n");
23471 }
对于原始INVITE来说,很关键的跳转点就是创建新的channel处理:
23554 /* First invitation - create the channel. Allocation
23555 * failures are handled below. */
23556 c = sip_new(p, AST_STATE_DOWN, S_OR(p->peername, NULL), NULL);
sip_new这个函数用于创建一个SIP CHANNEL,这里是入呼的调用,外呼时则是在sip_request_call函数中调用,sip_request_call是内核接口的SIP实现。sip_new返回之后,调用build_route记录SIP消息的路由信息。
回到handle_request_invite函数,re-invite和原始invite的差异处理完之后,处理SST(SIPSession Timer)扩展的支持:
23594 /* Session-Timers */
23595 if ((p->sipoptions & SIP_OPT_TIMER) && !ast_strlen_zero(sip_get_header(req, "Session-Expires")))
......
处理完SST之后,判断自身是还是Attendedtransfer 或call pickup的目标,最后,处理CHANNEL状态,根据状态,选择合适的应答码,交发出SIP应答。
23784 switch(c_state) {
23785 case AST_STATE_DOWN:
23786 ast_debug(2, "%s: New call is still down.... Trying... \n", ast_channel_name(c));
23787 transmit_provisional_response(p, "100 Trying", req, 0);
23788 p->invitestate = INV_PROCEEDING;
23789 ast_setstate(c, AST_STATE_RING);
23790 if (strcmp(p->exten, ast_pickup_ext())) { /* Call to extension -start pbx on this call */
23791 enum ast_pbx_result result;
23792
23793 result = ast_pbx_start(c);
23794
23795 switch(result) {
23796 case AST_PBX_FAILED:
23797 ast_log(LOG_WARNING, "Failed to start PBX :(\n");
23798 p->invitestate = INV_COMPLETED;
23799 transmit_response_reliable(p, "503 Unavailable", req);
23800 break;
23801 case AST_PBX_CALL_LIMIT:
23802 ast_log(LOG_WARNING, "Failed to start PBX (call limit reached) \n");
23803 p->invitestate = INV_COMPLETED;
23804 transmit_response_reliable(p, "480 Temporarily Unavailable", req);
23805 res = AUTH_SESSION_LIMIT;
23806 break;
23807 case AST_PBX_SUCCESS:
23808 /* nothing to do */
23809 break;
23810 }
......
如果是新的呼叫,则调用ast_pbx_start,在这里面启动一个独立的线程,接管这个CHANNEL,新创建的线程入口是pbx_thread函数,在这个函数里调用__ast_pbx_run函数,跳转执行拨号计划。
外呼流程
从SIP角度理解,Asterisk就是一个B2BUA,一路通话,在Asterisk内部需要两个UA,Asterisk负责把两个UA桥接在一块。一个UA对应了一个通道,上面分析了入呼(incoming)通道,接下来我们分析一下外呼(outgoing)通道的情况。
还是从ast_pbx_start说起:
05518 if (increase_call_count(c))
05519 return AST_PBX_CALL_LIMIT;
05520
05521 /* Start a new thread, and get something handling this channel. */
05522 if (ast_pthread_create_detached(&t, NULL, pbx_thread, c)) {
05523 ast_log(LOG_WARNING, "Failed to create new channel thread\n");
05524 decrease_call_count();
05525 return AST_PBX_FAILED;
05526 }
这几行代码检查是否还有空闲处理能力(取决于配置),如果有调用ast_pthread_create_detached启动一个线程,线程入口函数是pbx_thread。当然,这个线程是处理入呼通道的。
跳转到pbx_thread看一下都做了些什么?
05479 static void *pbx_thread(void *data)
05480 {
05481 /* Oh joyeous kernel, we're a new thread, with nothing to do but
05482 answer this channel and get it going.
05483 */
05484 /* NOTE:
05485 The launcher of this function _MUST_ increment 'countcalls'
05486 before invoking the function; it will be decremented when the
05487 PBX has finished running on the channel
05488 */
05489 struct ast_channel *c = data;
05490
05491 /* Associate new PBX thread with a call-id */
05492 struct ast_callid *callid = ast_create_callid();
05493 ast_callid_threadassoc_add(callid);
05494 callid = ast_callid_unref(callid);
05495
05496 __ast_pbx_run(c, NULL);
05497 decrease_call_count();
05498
05499 pthread_exit(NULL);
05500
05501 return NULL;
05502 }
这里调用了一个很关键的函数:__ast_pbx_run。
/* Start by trying whatever the channel is set to */
if (!ast_exists_extension(c, c->context, c->exten, c->priority,
S_COR(c->caller.id.number.valid, c->caller.id.number.str, NULL))) {
/* If not successful fall back to 's' */
ast_verb(2, "Starting %s at %s,%s,%d failed so falling back to exten 's'\n", c->name, c->context, c->exten, c->priority);
/* XXX the original code used the existing priority in the call to
* ast_exists_extension(), and reset it to 1 afterwards.
* I believe the correct thing is to set it to 1 immediately.
*/
set_ext_pri(c, "s", 1);
if (!ast_exists_extension(c, c->context, c->exten, c->priority,
S_COR(c->caller.id.number.valid, c->caller.id.number.str, NULL))) {
/* JK02: And finally back to default if everything else failed */
ast_verb(2, "Starting %s at %s,%s,%d still failed so falling back to context 'default'\n", c->name, c->context, c->exten, c->priority);
ast_copy_string(c->context, "default", sizeof(c->context));
}
}
在这里,首先调用ast_exists_extension查找拨号计划的入口,如果找到了,进入一个for循环,逐条执行拨号计划:
05141 for (;;) {
05142 char dst_exten[256]; /* buffer to accumulate digits */
05143 int pos = 0; /* XXX should check bounds */
05144 int digit = 0;
05145 int invalid = 0;
05146 int timeout = 0;
05147
05148 /* loop on priorities in this context/exten */
05149 while (!(res = ast_spawn_extension(c, ast_channel_context(c), ast_channel_exten(c), ast_channel_priority(c),
05150 S_COR(ast_channel_caller(c)->id.number.valid, ast_channel_caller(c)->id.number.str, NULL),
05151 &found, 1))) {
05152 if (!ast_check_hangup(c)) {
05153 ast_channel_priority_set(c, ast_channel_priority(c) + 1);
05154 continue;
5155 }
........
ast_spawn_extension这个函数调用pbx_extension_helper完成具体的动作:
05051 int ast_spawn_extension(struct ast_channel *c, const char *context, const char *exten, int priority, const char *callerid, int *found, int combined_find_spawn)
05052 {
05053 return pbx_extension_helper(c, NULL, context, exten, priority, NULL, callerid, E_SPAWN, found, combined_find_spawn);
05054 }
pbx_extension_helper调用pbx_find_extension查找系统中的extension:
04342 e = pbx_find_extension(c, con, &q, context, exten, priority, label, callerid, action);
找到之后,调用pbx_exec执行
04390 return pbx_exec(c, app, passdata); /* 0 on success, -1 on failure */
到目前为止,处理的都还是入呼的流程,如果需要外呼,就是在拨号计划中执行到dial()应用,从这个应用跳转到外呼处理。
在app_dial模块加载时,注册了一个应用的回调入口:
03160 res = ast_register_application_xml(app, dial_exec);
而dial_exec则调用了dial_exec_full:
03029 static int dial_exec(struct ast_channel *chan, const char *data)
03030 {
03031 struct ast_flags64 peerflags;
03032
03033 memset(&peerflags, 0, sizeof(peerflags));
03034
03035 return dial_exec_full(chan, data, &peerflags, NULL);
03036 }
在dial_exec_full函数里:首先调用ast_request分配通道资源,最终调用的就是通道模块注册的回调函数,SIP模块就是sip_request_call函数。然后调用ast_call发起呼叫,对应最终调用SIP通道注册的回调函数sip_call。最后,调用wait_for_answer等待被叫方应答。被叫应答后调用ast_bridge_call桥接两个通道。
相关推荐
5. **chan_sip.c**:这是处理SIP通道的核心模块,负责处理SIP消息的接收与发送,如INVITE请求的处理。 6. **ast_waitfor_n**:这个函数用于等待指定数量的事件发生,常用于处理并发事件。 7. **app_queue.c**:...
- 在安装Asterisk之前,需要先安装`libpri`库,`libcurl4-openssl-dev`,因为它们是Asterisk运行所必需的,特别是`libcurl4-openssl-dev`对于加载`chan_sip.so`模块至关重要。 2. **MySQL配置**: - 安装`mysql-...
内容概要:本文详细介绍了全C语言编写的基于外插法的永磁同步电机高频方波注入无传感器Simulink仿真模型。主要内容涵盖方波电压信号的生成与处理,包括开关频率5kHz、注入信号频率2.5kHz的正负辨别、电流误差分离、外插法解析、Atan反正切计算角度等。此外,文章深入讲解了永磁同步电机的Foc磁场定向控制框架的实现,包括Clarke Park变换、iPark控制、Svpwm等关键技术,并实现了转速和转矩的斜坡函数。该算法能够适应低速重载工况,成功加载至额定载荷,并展示了实际角度、估算角度、角度误差、实际转速、估算转速、转速误差和电流波形等关键参数。为了优化大功率应用中的开关频率低问题,文中还讨论了IGBT导通和关断过程的死区时间设置。 适合人群:从事电机控制领域研究和技术开发的专业人士,尤其是对永磁同步电机无传感器控制感兴趣的工程师。 使用场景及目标:适用于需要深入了解和实现永磁同步电机无传感器控制的研究人员和工程师。目标是提供一个高效的仿真模型,帮助理解和优化永磁同步电机的控制算法,特别是在低速重载工况下。 其他说明:该仿真模型已在DSP28335平台上进行了验证,提供了完整的源代码,便于后续独立算法开发和实际应用。
化工分离过程天津大学ppt.7z
内容概要:本文档是一份详细的 ANSYS 斜拉桥建模教程,涵盖了从图纸分析到最终模型建立的全过程。主要内容包括:图纸与建模思路分析、CAD 三维快速建模、Midas 预处理应用、手把手带写命令流、截面实常数讲解、斜拉索规格介绍、拉索实常数定义、板桁结构二期实常数与单主梁模型区别、板单元等效厚度计算、面内与面外厚度的理解以及支座模拟。通过这些步骤,读者能够掌握斜拉桥建模的关键技术和细节。 适合人群:土木工程专业学生、桥梁设计师、结构工程师及其他对斜拉桥建模感兴趣的从业人员。 使用场景及目标:适用于需要深入了解斜拉桥建模流程和技术细节的专业人士,旨在帮助他们掌握 ANSYS 软件的实际操作技能,提升桥梁设计和分析的能力。 其他说明:文档提供了丰富的实例和详细的命令流解析,有助于读者在实践中加深理解和掌握相关技术。
南京大学论文答辩ppt模板毕业论文答辩PPT模板
内容概要:本文详细介绍了科尔摩根AKD伺服驱动器与西门子S7-1200/1500 PLC之间的PROFINET IO通讯配置步骤及其实践经验。主要内容涵盖配置程序V16、GSDML文件的正确使用、设备间‘方言’适配的方法、确保PLC准确识别驱动器的关键参数设置、数据交换区配置对控制精度的影响以及调试过程中可能遇到的问题及解决方案。此外,还分享了一些提高通讯效率的小技巧,如禁用自动IP分配、正确配置网口、优化通讯周期等。文中提到的实际案例表明,该配置方案已在包装生产线成功应用并表现出良好的稳定性。 适合人群:从事工业自动化领域的工程师和技术人员,特别是那些负责PLC与伺服驱动器集成工作的专业人士。 使用场景及目标:帮助工程师们掌握科尔摩根AKD伺服驱动器与西门子S7-1200/1500 PLC之间建立高效稳定的PROFINET IO通讯的具体方法,从而提升生产效率和系统可靠性。 其他说明:文中提供的配置方案已经经过实际项目的验证,能够应对复杂的工业环境挑战,如电网波动和紧急停止等情况。同时提醒使用者关注驱动器固件版本与GSDML文件的兼容性问题。
内容概要:本文介绍了一种利用注意力机制与LSTM构建的多特征风功率预测模型,适用于MATLAB 2021及以上版本。该模型采用三个特征变量(如风速、风向、温度)来预测风功率输出,并展示了真实值与预测值的对比图以及线性拟合图。文中详细解释了注意力机制的作用,即通过动态调整各特征的权重,提高模型对重要信号的捕捉能力。此外,还提供了调参技巧和扩展应用场景,如用于金融数据预测。 适合人群:具有一定MATLAB编程基础的数据科学家、机器学习工程师、风电行业从业者。 使用场景及目标:① 构建多特征时间序列预测模型;② 提高预测精度,特别是对于非平稳时间序列数据;③ 学习注意力机制在深度学习中的应用。 其他说明:模型效果依赖于数据质量和特征选择,建议使用者根据实际情况进行适当调整。
内容概要:本文介绍了一种基于DBSCAN密度聚类的风电-负荷场景生成与削减模型。该模型首先对风电和电负荷的历史数据进行采集,接着利用DBSCAN算法进行数据预处理,去除异常或小概率数据。随后,根据风电的波动性和电负荷的时序性、周期性特点,分别进行风电场景和电负荷场景的提取。相比于传统的K-means方法,DBSCAN方法能够更好地处理异常值和复杂场景,使场景模型更具代表性。实验结果显示,DBSCAN方法不仅提高了计算效率,还将异常场景的识别率从68%提升到了92%,显著提升了后续容量配置的可靠性。 适合人群:从事电力系统优化、数据分析以及机器学习领域的研究人员和技术人员。 使用场景及目标:适用于需要优化风电和电负荷场景生成与削减的微电网规划项目,旨在提高能源分配的效率和可靠性。 其他说明:文中提供了详细的代码示例和参数调整技巧,帮助读者理解和应用DBSCAN密度聚类方法。
实训商业源码-WP9.0模板+插件-毕业设计.zip
内容概要:本文介绍了基于Proteus利用SR锁存器74LS279和与非门74LS02D设计抢答器的方法。文章详细讲解了通过SR锁存器的高、低电平状态实现抢答锁定功能,即S低电平置高电平用于抢答,R低电平置低电平用于清零,确保抢答按键按下后无法再次触发。抢答结果通过数码管显示,其中1、2、3序号分别对应不同的按键,4路抢答器独立显示序号,3路抢答器则通过或逻辑处理并列抢答的情况,如1和2、1和3、2和3序号并列抢答时显示“3”,并用LED指示具体并列情况。电路还集成了声光反馈机制,当抢答有效时,蜂鸣器发出声音提示。此外,文章提到可通过三刀双掷开关切换是否允许并列抢答,以及通过锁存器实现抢答器路数的扩展,并提出加入倒计时功能进一步完善系统。 适合人群:对数字电路设计有一定兴趣的电子爱好者,尤其是希望深入了解锁存器应用及抢答器设计原理的初学者和中级工程师。 使用场景及目标:①学习如何利用SR锁存器实现抢答器的基本功能;②掌握并列抢答情况下的电路处理方法;③理解数码管、LED、蜂鸣器等元件在电路中的应用;④探索电路的可扩展性和改进方向。 阅读建议:读者应结合Proteus软件进行实际电路仿真,动手搭建文中所述电路,逐步理解每个组件的作用及其相互关系,同时关注电路中可能出现的问题及解决方法。
project.rar
内容概要:本文详细介绍了双馈风机(DFIG)低电压穿越(LVRT)技术的研究,重点讨论了MATLAB仿真模型的应用。首先,文章概述了DFIG的基本结构和低电压穿越技术的重要性。接着,深入分析了转子侧变换器的控制策略,包括基于定子电压定向的矢量控制策略,实现了有功和无功解耦,并具备最大功率点跟踪(MPPT)能力。同时,文中提到网侧变换器采用电网电压定向的矢量控制策略,确保直流母线电压稳定和输入功率因数为1。此外,文章还探讨了Crowbar电路的作用,即在低电压穿越过程中保护电路免受过电流或过电压的影响。最后,通过MATLAB仿真模型展示了DFIG在电网电压变化时的响应过程及其稳定性。 适合人群:从事电力工程、风力发电技术研究的专业人士和技术人员。 使用场景及目标:适用于希望深入了解双馈风机低电压穿越技术和MATLAB仿真的研究人员,旨在提高风力发电系统的稳定性和可靠性。 其他说明:本文不仅提供了理论分析,还通过具体的仿真模型验证了相关技术的实际效果,有助于推动新能源领域的发展。
1_6DoF机械臂智能机器人原理课程实验--轨迹插值.pptx
内容概要:本文详细介绍了利用ANSYS Maxwell对开关磁阻电机进行参数化仿真的方法及其应用。主要内容涵盖四个部分:首先是参数化仿真的概念介绍,解释了这一技术如何帮助我们在设计阶段预测并优化电机性能;其次是针对转子内外径、定转子极弧系数、气隙长度和绕组匝数等关键参数的扫描实验,展示了不同参数变化对电机性能(如磁场分布、转矩、效率)的具体影响;再次是灵敏度分析,通过对各参数敏感性的评估,识别出对电机性能有重大影响的因素;最后是效率优化,基于前面的分析结果,采用适当的优化算法寻找最佳参数配置,以提升电机的整体效能。文中还涉及了一些必要的代码片段,用于指导实际操作。 适合人群:从事电机设计与优化工作的工程师和技术人员,尤其是那些希望深入了解ANSYS Maxwell软件特性和应用的人群。 使用场景及目标:适用于需要对开关磁阻电机进行全面性能评估和优化的工作场合,旨在帮助用户掌握ANSYS Maxwell中参数化仿真和效率优化的技术细节,以便应用于实际项目中。 其他说明:文章不仅提供了理论知识,还包括具体的操作指南和实例演示,有助于读者快速上手并应用于实际工作中。
内容概要:文章介绍了蓝牙5.3的发布背景、发展历程、相较于旧版本的性能和功能提升、应用场景以及未来发展趋势。蓝牙技术自1994年提出以来,经过不断迭代,蓝牙5.3在传输延迟、抗干扰性、功耗、安全性等方面实现了重大突破。其应用场景涵盖真无线耳机、物联网设备、智能家居、工业自动化和医疗设备等领域,为用户带来更佳的使用体验。文章最后探讨了蓝牙技术未来在传输速度、功能拓展、应用领域等方面的潜在发展方向。; 适合人群:对蓝牙技术发展感兴趣的科技爱好者、从事蓝牙技术研发的工程师及相关行业的从业者。; 使用场景及目标:①了解蓝牙技术的历史沿革及其各个版本的关键特性;②掌握蓝牙5.3相较于以往版本的具体改进之处;③明确蓝牙5.3在不同设备和场景下的应用价值;④展望蓝牙技术未来可能的发展趋势。; 其他说明:是否升级到蓝牙5.3取决于个人需求和现有设备情况。对于追求极致体验或特定环境下使用蓝牙设备的用户来说,升级蓝牙5.3能带来明显的好处;而对于日常简单使用场景下,若现有设备能满足需求,则无需特意升级。此外,还需考虑设备的兼容性和可升级性。
内容概要:本文详细介绍了基于PLC(可编程逻辑控制器)的乡村恒压供水系统的设计与实现。该系统不仅解决了传统供水系统中存在的水压不稳定问题,还加入了水质监测和智能加热功能,确保了用水的安全性和舒适性。系统采用S7-1200 PLC进行闭环控制,利用PID算法自动调节水泵转速,将压力波动控制在±0.02MPa以内。同时,在水箱中加入了多层滤芯和水质监测设备,如浊度传感器和余氯检测仪,确保水质符合标准。此外,系统还包括了一个智能加热模块,根据不同季节调整水温,满足村民的需求。组态画面设计直观易懂,便于村民操作和监控。 适用人群:从事自动化控制系统设计的技术人员、农村水利设施管理人员以及对PLC应用感兴趣的工程师。 使用场景及目标:适用于需要解决水压不稳定、水质不达标等问题的乡村地区。目标是提供一个稳定、安全、舒适的供水系统,提升居民生活质量。 其他说明:该系统已在鲁中山区成功运行半年,经历了极端天气考验,表现出优异的性能和可靠性。
内容概要:本文探讨了在光伏储能系统中应用多目标粒子群算法(MOPSO)进行经济调度的方法。针对光伏并网系统中存在的经济调度难题,如发电成本与光伏消纳之间的矛盾,提出了利用MOPSO算法解决这一问题的新思路。文中详细介绍了MOPSO算法的工作原理及其在光伏储能调度中的具体实现步骤,包括定义双目标适应度函数、维护Pareto前沿的精英解集合以及处理储能状态约束等问题。此外,还讨论了如何应对光伏预测误差对调度效果的影响,并给出了相应的解决方案。实验结果显示,在适当的成本增长下能够显著降低弃光率。 适用人群:从事新能源电力系统规划与运营的专业人士,特别是关注光伏储能联合调度的研究人员和技术人员。 使用场景及目标:适用于需要优化光伏发电利用率和降低成本的实际工程项目,旨在为相关从业者提供一种有效的调度工具,帮助他们在满足多种性能指标的前提下做出最佳决策。 其他说明:文中提供了部分关键代码片段用于解释算法细节,有助于读者更好地理解和复现研究成果。同时强调了最终的选择仍需由人类专家根据实际情况决定。
内容概要:本文详细剖析了“hello”程序从源代码编写到运行终止的全过程,涵盖预处理、编译、汇编、链接、加载、执行、访存、动态内存分配、信号处理及终止回收等阶段。通过Ubuntu环境下的实例分析,展示了计算机系统如何利用编译器工具链和操作系统管理,将静态代码转化为动态进程,并实现程序资源管理的生命周期。研究揭示了系统设计的层次化与抽象机制,强调了性能、复杂度平衡及容错可靠性的重要性,为计算机系统优化提供启示。 适合人群:计算机专业本科生、研究生及对计算机系统底层原理感兴趣的读者。 使用场景及目标:①帮助读者理解编译器工具链的工作流程;②深入探讨操作系统在进程管理和内存管理中的角色;③学习如何通过分析程序生命周期优化计算机系统性能;④掌握动态内存分配和信号处理机制。 其他说明:本文通过具体的“hello”程序案例,详细解释了每个阶段的技术细节,包括预处理指令、编译过程中的代码转换、汇编生成的机器语言、链接器的符号解析、加载器的内存映射、执行时的进程调度、访存机制、动态内存分配管理以及信号处理机制。文章还附带了详细的附件和参考文献,便于读者进一步学习和研究。
内容概要:本文深入浅出地介绍了用例图的概念、构成元素及其在软件开发中的重要作用。用例图作为一种描述系统功能的视图,是UML的重要组成部分,它通过参与者、用例、边界及关系清晰展示了系统的功能需求。文章详细解释了参与者(外部实体)、用例(功能需求或用户场景)、系统边界和关系(关联、包含、扩展、泛化)的定义和作用。用例图不仅有助于获取精准需求,还能指导测试和系统设计,确保开发过程有序高效。通过实际案例,如电商系统、打车软件等,文章生动地展示了用例图的应用场景,帮助读者更好地理解其在软件开发各阶段的作用。 适合人群:适合软件开发人员、项目经理、需求分析师以及所有参与软件开发流程的相关人员,尤其是初学者和有一定经验的技术人员。 使用场景及目标:① 在需求分析阶段,帮助团队梳理用户需求,确保系统功能明确;② 在系统设计阶段,为架构师和开发人员提供功能模块划分和接口设计的依据;③ 在测试阶段,为测试人员提供详细的测试用例设计参考;④ 提高团队沟通效率,确保各方对系统功能达成一致理解。 其他说明:本文通过丰富的实例和详细的解释,帮助读者掌握用例图的绘制方法和应用技巧,建议读者在实际项目中多加练习,结合具体业务场景灵活运用用例图,以提升软件开发的质量和效率。