和以前版本相比,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-...
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
8c71b76fb2ec10cf50fc6b0308d3dcfc_9545878e2b97a84b2e089ece58da9e82
Android逆向过程学习
内容概要:本文详细介绍了基于西门子S7-200 PLC的糖果包装控制系统的设计与实现。首先阐述了PLC在工业自动化领域的优势及其在糖果包装生产线中的重要性。接着深入探讨了系统的硬件连接方式,包括传感器、执行机构与PLC的具体接口配置。随后展示了关键的编程实现部分,如糖果计数、包装执行、送膜控制、称重判断以及热封温度控制等具体梯形图代码片段。此外,还分享了一些实用的经验技巧,如防止信号抖动、PID参数优化、故障诊断方法等。最后总结了该系统的优势,强调其对提高生产效率和产品质量的重要作用。 适合人群:从事工业自动化控制、PLC编程的技术人员,尤其是对小型PLC系统感兴趣的工程师。 使用场景及目标:适用于糖果制造企业,旨在提升包装生产线的自动化程度,确保高效稳定的生产过程,同时降低维护成本并提高产品一致性。 其他说明:文中不仅提供了详细的理论讲解和技术指导,还结合实际案例进行了经验分享,有助于读者更好地理解和掌握相关知识。
内容概要:本文详细介绍了参与西门子杯比赛中关于三部十层电梯系统的博图V15.1程序设计及其WinCC画面展示的内容。文中不仅展示了电梯系统的基本架构,如抢单逻辑、方向决策、状态机管理等核心算法(采用SCL语言编写),还分享了许多实际调试过程中遇到的问题及解决方案,例如未初始化变量导致的异常行为、状态机遗漏空闲状态、WinCC画面动态显示的挑战以及通信配置中的ASCII码解析错误等问题。此外,作者还特别提到一些创意性的设计,如电梯同时到达同一层时楼层显示器变为闪烁爱心的效果,以及节能模式下电梯自动停靠中间楼层的功能。 适合人群:对PLC编程、工业自动化控制、电梯调度算法感兴趣的工程技术人员,尤其是准备参加类似竞赛的学生和技术爱好者。 使用场景及目标:适用于希望深入了解PLC编程实践、掌握电梯群控系统的设计思路和技术要点的人士。通过学习本文可以更好地理解如何利用PLC进行复杂的机电一体化项目的开发,提高解决实际问题的能力。 其他说明:文章风格幽默诙谐,将严肃的技术话题融入轻松的生活化比喻之中,使得原本枯燥的专业知识变得生动有趣。同时,文中提供的经验教训对于从事相关领域的工作者来说非常宝贵,能够帮助他们少走弯路并激发更多创新思维。
慧荣量产工具合集.zip
内容概要:本文详细介绍了永磁同步电机(PMSM)的FOC(磁场定向控制)和SVPWM(空间矢量脉宽调制)算法的仿真模型。首先解释了FOC的基本原理及其核心的坐标变换(Clark变换和Park变换),并给出了相应的Python代码实现。接下来探讨了SVPWM算法的工作机制,包括扇区判断和占空比计算的方法。此外,文章还讨论了电机的PI双闭环控制结构,即速度环和电流环的设计与实现。文中不仅提供了详细的理论背景,还分享了一些实用的编程技巧和注意事项,帮助读者更好地理解和应用这些算法。 适合人群:电气工程专业学生、从事电机控制系统开发的技术人员以及对永磁同步电机控制感兴趣的科研人员。 使用场景及目标:① 学习和掌握永磁同步电机的FOC控制和SVPWM算法的具体实现;② 提供丰富的代码示例和实践经验,便于快速搭建和调试仿真模型;③ 探讨不同参数设置对电机性能的影响,提高系统的稳定性和效率。 其他说明:文章强调了在实际应用中需要注意的一些细节问题,如坐标变换中的系数选择、SVPWM算法中的扇区判断优化以及PI控制器的参数调整等。同时,鼓励读者通过动手实验来加深对各个模块的理解。
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
Android逆向过程学习
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
3dmax插件
# 【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar中文文档.zip】 中包含: 中文文档:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7-javadoc-API文档-中文(简体)版.zip】 jar包下载地址:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar中文文档.zip,java,spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar,org.springframework.ai,spring-ai-autoconfigure-vector-store-qdrant,1.0.0-M7,org.springframework.ai.vectorstore.qdr
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
内容概要:本文详细介绍了平方根容积卡尔曼滤波(SRCKF)在永磁同步电机(PMSM)控制系统中的应用及其相对于传统CKF的优势。文章首先指出传统CKF在处理协方差矩阵时存在的数值不稳定性和非正定问题,导致系统性能下降。接着,作者通过引入SRCKF,利用Cholesky分解和QR分解来确保协方差矩阵的正定性,从而提高状态估计的精度和稳定性。文中展示了具体的电机模型和状态方程,并提供了详细的代码实现,包括状态预测、容积点生成以及观测更新等关键步骤。此外,文章还分享了实际调试过程中遇到的问题及解决方案,如选择合适的矩阵分解库和处理电机参数敏感性。最终,通过实验数据对比,证明了SRCKF在突加负载情况下的优越表现。 适合人群:从事永磁同步电机控制研究的技术人员、研究生及以上学历的研究者。 使用场景及目标:适用于需要高精度状态估计的永磁同步电机控制系统的设计与优化,特别是在处理非线性问题和提高数值稳定性方面。 其他说明:文章引用了相关领域的权威文献,如Arasaratnam的TAC论文和Zhong的《PMSM无传感器控制综述》,并强调了实际工程实践中代码调试的重要性。
# 【tokenizers-***.jar***文档.zip】 中包含: ***文档:【tokenizers-***-javadoc-API文档-中文(简体)版.zip】 jar包下载地址:【tokenizers-***.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【tokenizers-***.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【tokenizers-***.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【tokenizers-***-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: tokenizers-***.jar***文档.zip,java,tokenizers-***.jar,ai.djl.huggingface,tokenizers,***,ai.djl.engine.rust,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,djl,huggingface,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压 【tokenizers-***.jar***文档.zip】,再解压其中的 【tokenizers-***-javadoc-API文档-中文(简体)版.zip】,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件; # Maven依赖: ``` <dependency> <groupId>ai.djl.huggingface</groupId> <artifactId>tokenizers</artifactId> <version>***</version> </dependency> ``` # Gradle依赖: ``` Gradle: implementation group: 'ai.djl.huggingface', name: 'tokenizers', version: '***' Gradle (Short): implementation 'ai.djl.huggingface:tokenizers:***' Gradle (Kotlin): implementation("ai.djl.huggingface:tokenizers:***") ``` # 含有的 Java package(包): ``` ai.djl.engine.rust ai.djl.engine.rust.zoo ai.djl.huggingface.tokenizers ai.djl.huggingface.tokenizers.jni ai.djl.huggingface.translator ai.djl.huggingface.zoo ``` # 含有的 Java class(类): ``` ai.djl.engine.rust.RsEngine ai.djl.engine.rust.RsEngineProvider ai.djl.engine.rust.RsModel ai.djl.engine.rust.RsNDArray ai.djl.engine.rust.RsNDArrayEx ai.djl.engine.rust.RsNDArrayIndexer ai.djl.engine.rust.RsNDManager ai.djl.engine.rust.RsSymbolBlock ai.djl.engine.rust.RustLibrary ai.djl.engine.rust.zoo.RsModelZoo ai.djl.engine.rust.zoo.RsZooProvider ai.djl.huggingface.tokenizers.Encoding ai.djl.huggingface.tokenizers.HuggingFaceTokenizer ai.djl.huggingface.tokenizers.HuggingFaceTokenizer.Builder ai.djl.hu
3
pchook源码纯源码不是dll
# 【spring-ai-azure-store-1.0.0-M7.jar中文-英文对照文档.zip】 中包含: 中文-英文对照文档:【spring-ai-azure-store-1.0.0-M7-javadoc-API文档-中文(简体)-英语-对照版.zip】 jar包下载地址:【spring-ai-azure-store-1.0.0-M7.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【spring-ai-azure-store-1.0.0-M7.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【spring-ai-azure-store-1.0.0-M7.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【spring-ai-azure-store-1.0.0-M7-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: spring-ai-azure-store-1.0.0-M7.jar中文-英文对照文档.zip,java,spring-ai-azure-store-1.0.0-M7.jar,org.springframework.ai,spring-ai-azure-store,1.0.0-M7,org.springframework.ai.vectorstore.azure,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,springframework,spring,ai,azure,store,中文-英文对照API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压 【spring-ai-azure-store-1.0.0-M7.jar中文-英文对照文档.zip】,再解