SIP 中的Dialog,call,session 和 transaction

本文详细阐述了SIP协议中的关键概念,包括消息、事务、对话、会话及呼叫等,揭示了它们之间的关系,并提供了图表辅助理解。
如果你对Sip协议中Call, Dialog, Transaction和Message之间的关系感觉到迷惑,那么,那么我可以告诉你,你并不孤单,因为大多数初学者对于这些名词之间的关系都会感到疑惑.

Messages(消息) 消息是在服务器和客户端之间交换的独立文本, 有两种类型的消息,分别是请求(Requests)和响应(Responses).


Transaction(事务)  事务发生于客户端和服务器端之间,包含从客户端发出请求给服务器,到服务器响应给客户端的最终消息(non-1xx message)之间的所有消息. 如果请求是一个"Invite"消息,并且最终的响应是一个non-2xx消息,那么该事务包含一个"Ack"响应消息.如果服务器的响应是一个2xx消息,那么,随后的ACK是一个单独的事务.  
A sip transaction consists of a single request and any responses to that request, which includes zero or more provisional responses and one or more final responses.The branch parameter value in the VIA header is used to identify the transaction created by that request

Dialog(对话)对话是两个UAs(user agent) 之间持续一段时间的端到端(peer-to-peer)的SIP 关系. 一个对话由一个Call-ID, 一个local tag 和 一个remote tag来标识.对话过去也叫做 "call leg".dialog的建立是收到UAS的响应(To tag)时开始建立的。收到180响应时建立dialog叫做早期对话(early dialog),收到2XX的应答开始才是真正的dialog建立。
A dialog represents a peer-to-peer SIP relationship between two user agents that persists for some time, as a call-leg.It is identified at each UA with a dialog ID, which consists of a Call-ID, From tag and To tag. We can call a dialog is established when three values are all generated

Session(会话)
session 是媒体交换之后才建立的。具体而言就是通过offer/answer方式交换sdp的媒体。 session的建立可以使INVITE-200 也可以是200-ACK。这要看媒体的交换发生的时间。 具体来说,INVITE 中的消息体用sdp语言来描述自己可处理的媒体类型,200OK中 带回UAS端可处理的媒体类型。这个时候媒体交换就算是完成了。也就是session建立起来了。 
In the SDP specification, a multimedia session is a set of multimedia senders and receivers and the data streams flowing from senders to receivers.  A session is defined by the SDP user name, session id, network type, address type, and address elements in the origin field.
A session can have multiple RTP sessions corresponding to the UDP ports define in the line of the SDP.

Call(呼叫) :一个呼叫是由一个会议中被同一个发起者邀请加入的所有成员组成的。一个 SIP 呼叫用全局唯一呼叫标识(CALL_ID)来识别。因此,如果一个用户被不同的人邀请参加同一个多点会议,每个邀请都有一个唯一的呼叫。

注: Dialog和Session都翻译成了会话,但两者显然不同.

下面的示意图清晰的显示了它们之间的关系
Sip_relation
(RINGING 是 1xx 响应,  OK是 2xx 响应) 

caller呼叫callee的号码来建立一系列的对话(Dialogs),这些对话组成了一个呼叫(Call).


1.对话和事务处于信令层,而会话处于媒体传输层。SIP使用SDP来通知传输层(RTP)来创建、增加、移除和修改会话。
2.一般来说,在会议应用中SIP可以通过请求来让另一方加入已有会话中。在这种情况下,新的对话会被创建。
3.对话是end-point对end-point的关系,即真实的通信双方,
  而transaction 是hop by hop的关系,即路由过程中交互的双方。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

呼叫(call): 呼叫是一个非正式的术语,用来表示一个多媒体会话,用Call-ID来标识;不论两方通话还是在多方通话中,在每个UA中是使用同一个Call-ID;


事务(transaction): 请求(UAC)+最终响应(相邻的UAS),SIP基于事务。所谓相邻就是说transaction存在于相邻的SIP实体,而不是存在于两个UA之间。CSeq标识。一个事务中包含一个请求消息、0个或多个临时响应消息、1个或多个最终响应消息(2xx~6xx)。SIP是事务性的协议。事务的区分通过Via字段栈顶的Branch的值来确定,这是由于对于请求消息每经过一个有事务状态的Proxy的时候,该Proxy需要为这个事务创建一个服务器端事务和一个客户端事务,并且将自己的URI添加到Via的栈顶,并生成一个Global ID做为Branch的值,以此值来表示一个与之相对应的事务。SIP在事务层面定义了状态机和定时器来实现重传。

 

下图是一个回复200 OK的成功的INVITE事务:是不是INVITE事务区别在于 UAC需要为每个INVITE最终请求(2xx~6xx)生成ACK响应,而其他的请求消息(INFO,OPTION,etc)则不必如此。因为INVITE的地位比较重要, 所以需要这样一个三次握手的机制来保证会话的双方都能够确保事务的完整性,这一点和TCP连接建立的三次握手比较像。

 

 

注意在上图这两个UA中,每一个代理服务器都将自己的地址加入返回的ACK的Via头域中,而非成功的transaction则不会加入,见RFC 3261 (p.24)。CSeq头域的值必须与INVITE相同,并且CSeq的方法必须是ACK。中间响应消息 1xx 的使用则是为了节省网络开销设计的,一旦 UC 收到任何一个中间响应消息,则 UC 必须停止消息重发定时器,不再从发这个请求消息,反之则直到收到最终响应消息或重发定时器超时。一旦客户端UAC的事务在Calling状态收到任何中间响应消息1xx,事务则自动切换到Processing状态,停止请求消息的重发。并且需要将中间响应消息传送给TU事务用户。在呼叫业务中,TU以及上层应用可以根据中间响应消息在用户界面上提示用户。一旦事务切换到Processing状态,任何其他中间响应消息也都要传送给TU。

 

而非INVITE事务则如下:

当UAC发出非INVITE请求时,它就会在事务管理子层上开启定时器F(TCP)或者是E(UDP),确保超时的时候进行重传。这适用于除了 ACK请求外的其他非INVITE请求。每次超时重传时E的时间都被翻倍,直到最大的4秒。而F超时时,UAC就会认为是Timeout,这个事务将被删除。

 

对话(dialog/leg): 代表着两个SIP UA之间持续一段时间的端到端的联系(如:一段通话)。也就说仅仅存在于端到端的信令关系。当一个UAS发出对于INVITE(或者REFER)的非失败最终响应<=>200OK(BYE),则Dialog建立,同时这也是session的开始。UA和SIP代理服务器之间不会有对话。在SIP中呼叫中包含一个或多个Dialog(这仅仅存在于多方通话中)。Dialog终结于任意一端发出 BYE。Early Dialog可以通过UAC发出的CANCEL进行终结,更确切的说,所有早期对话在接收到非2XX最终响应时就被终结了。 Call-ID-value、To、From进行标识。Forking时体现明显。

在这个Forking的例子中,这个用户注册了三个设备,在用户被呼叫时,INVITE的Contact头域就被转换为三个INVITE发往三个设备。后边的q指的是优先级,q越小,优先级越高。其中的SIP注册服务器相当于一个Forking代理,尽管这个实体接收到两个ACK,但是除了这些ACK外,它与主叫方的信令交互都是属于一个transaction的,而与被叫方则分别建立了Transaction。另外,被叫方收到的两个ACK由分别建立了Transaction。注意Device3返回了488这样的非成功响应,SIP注册服务器(Forking代理服务器)没有将该响应发回主叫方,这是SIP代理一个重要的特征,SIP代理还能自行发出Request:CANCEL消息。

 

 

 

UAS对话层接收到一个新的对话请求INVITE消息后,在建立会话的响应消息2xx中,将请求消息里面的所有Route-Record字段拷贝到2xx消息中,并且UAS的对话层必须添加一个Contact字段使得对话中后续的响应(INVITE在2xx响应的情况下也包括ACK消息)、请求消息可以直接和本UA联系。当UAC收到UAS的INVITE的2xx响应消息后,如果2xx中不包含任何Route-Record字段的,则UAC可以选择直接发送ACK到Contact中地址&端口。

 


会话(session): 多方用户的媒体关系,在对话的控制下建立。

 

下图是Early dialog、Session、Dialog、Transaction等的在一个UA-UA的呼叫中的体现:

 

 

在这个例子中,通过INVITE事务而成功建立起来的dialog必须有一个ACK进行回应,这是第二个transaction的开始,尽管ACK并没有回复,但是由于新的 branch-value被填入,所以这个ACK代表了一个新的Transaction的开始。注意,此时 transaction number (CSeq) 并没有根据INVITE而增加--也就是说若收到的最终响应不是2XX(是3XX--6XX),则该transaction中包含ACK,若最终响应是2XX,则ACK属于一个新的transaction(此处存疑,国外有资料将其视为一个新的transaction,但是RFC3261中的意思却是ACK不属于INVITE Transaction,也不创建新的Transaction,但会重新计算Transaction参数--branchID)。早期对话是UAS以一个1XX响应作为回应时建立的。这样做的好处是在UAC可能在早期对话中发出诸如UPDATE这样的SIP请求。


#ifndef _SIP_PROTOCOL_H #define _SIP_PROTOCOL_H #include "common/sip_common.h" #ifdef SIP_USE_TCP #define SIP_TRANSPORT "TCP" #else #define SIP_TRANSPORT "UDP" #endif /* 字符数组长度宏定义 */ #define SIP_URI_SCHEME_LEN 8 #define SIP_URI_USER_LEN 32 #define SIP_URI_HOST_LEN 32 #define SIP_URI_PARAM_LEN 32 #define SIP_VIA_PROTOCOL_LEN 16 #define SIP_VIA_TRANSPORT_LEN 8 #define SIP_VIA_HOST_LEN 64 #define SIP_VIA_BRANCH_LEN 32 #define SIP_VIA_RECEIVED_LEN 32 #define AUTH_REALM_LEN 64 #define AUTH_NONCE_LEN 64 #define AUTH_ALGORITHM_LEN 32 #define AUTH_QOP_LEN 32 #define AUTH_OPAQUE_LEN 64 #define AUTH_RESPONSE_LEN 33 #define SIP_HEADER_CALLID_LEN 64 #define SIP_HEADER_CONTENT_TYPE_LEN 64 #define SIP_HEADER_CONTACT_LEN 64 #define SIP_HEADER_USER_AGENT_LEN 64 #define SIP_BODY_TYPE_LEN 32 #define SIP_REASON_PHRASE_LEN 32 #define SIP_CONFIG_USER_AGENT_LEN 32 #define SIP_BRANCH_LEN 32 #define SIP_TAG_LEN 32 #define SIP_CALLID_LEN 64 #define T1_TIMEOUT 500 #define T2_TIMEOUT 64 * T1_TIMEOUT #define T4_TIMEOUT 5000 /* SIP方法定义 */ typedef enum sip_method { SIP_METHOD_INVITE = 0, SIP_METHOD_ACK, SIP_METHOD_BYE, SIP_METHOD_CANCEL, SIP_METHOD_REGISTER, SIP_METHOD_OPTIONS, SIP_METHOD_MAX } sip_method; /* SIP响应状态码 */ /* SIP响应状态码枚举 (RFC 3261及相关扩展) */ typedef enum sip_status_code { /* 1xx 临时响应 */ SIP_100_TRYING = 100, SIP_180_RINGING = 180, SIP_181_CALL_IS_BEING_FORWARDED = 181, SIP_182_QUEUED = 182, SIP_183_SESSION_PROGRESS = 183, /* 2xx 成功响应 */ SIP_200_OK = 200, SIP_202_ACCEPTED = 202, // RFC3265 /* 3xx 重定向响应 */ SIP_300_MULTIPLE_CHOICES = 300, SIP_301_MOVED_PERMANENTLY = 301, SIP_302_MOVED_TEMPORARILY = 302, SIP_305_USE_PROXY = 305, SIP_380_ALTERNATIVE_SERVICE = 380, /* 4xx 客户端错误 */ SIP_400_BAD_REQUEST = 400, SIP_401_UNAUTHORIZED = 401, SIP_402_PAYMENT_REQUIRED = 402, SIP_403_FORBIDDEN = 403, SIP_404_NOT_FOUND = 404, SIP_405_METHOD_NOT_ALLOWED = 405, SIP_406_NOT_ACCEPTABLE = 406, SIP_407_PROXY_AUTHENTICATION_REQUIRED = 407, SIP_408_REQUEST_TIMEOUT = 408, SIP_410_GONE = 410, SIP_413_REQUEST_ENTITY_TOO_LARGE = 413, SIP_414_REQUEST_URI_TOO_LONG = 414, SIP_415_UNSUPPORTED_MEDIA_TYPE = 415, SIP_416_UNSUPPORTED_URI_SCHEME = 416, SIP_420_BAD_EXTENSION = 420, SIP_421_EXTENSION_REQUIRED = 421, SIP_423_INTERVAL_TOO_BRIEF = 423, SIP_480_TEMPORARILY_UNAVAILABLE = 480, SIP_481_CALL_TRANSACTION_DOES_NOT_EXIST = 481, SIP_482_LOOP_DETECTED = 482, SIP_483_TOO_MANY_HOPS = 483, SIP_484_ADDRESS_INCOMPLETE = 484, SIP_485_AMBIGUOUS = 485, SIP_486_BUSY_HERE = 486, SIP_487_REQUEST_TERMINATED = 487, SIP_488_NOT_ACCEPTABLE_HERE = 488, SIP_489_BAD_EVENT = 489, // RFC3265 SIP_491_REQUEST_PENDING = 491, SIP_493_UNDECIPHERABLE = 493, /* 5xx 服务器错误 */ SIP_500_SERVER_INTERNAL_ERROR = 500, SIP_501_NOT_IMPLEMENTED = 501, SIP_502_BAD_GATEWAY = 502, SIP_503_SERVICE_UNAVAILABLE = 503, SIP_504_SERVER_TIME_OUT = 504, SIP_505_VERSION_NOT_SUPPORTED = 505, SIP_513_MESSAGE_TOO_LARGE = 513, /* 6xx 全局错误 */ SIP_600_BUSY_EVERYWHERE = 600, SIP_603_DECLINE = 603, SIP_604_DOES_NOT_EXIST_ANYWHERE = 604, SIP_606_NOT_ACCEPTABLE = 606 } sip_status_code; /* SIP事务状态定义 */ typedef enum sip_transaction_state { Calling = 0, Trying, Proceeding, Completed, Terminated } sip_transaction_state; /* SIP URI结构 */ typedef struct sip_uri { char scheme[SIP_URI_SCHEME_LEN]; /* "sip" or "sips" */ char user[SIP_URI_USER_LEN]; /* username */ char host[SIP_URI_HOST_LEN]; /* domain or IP */ U16 port; /* port number */ char parameters[SIP_URI_PARAM_LEN]; /* URI parameters */ } sip_uri; /* SIP Via头结构 */ typedef struct sip_via { char protocol[SIP_VIA_PROTOCOL_LEN]; /* "SIP/2.0" */ char transport[SIP_VIA_TRANSPORT_LEN]; /* "UDP", "TCP" */ char host[SIP_VIA_HOST_LEN]; /* sent-by host */ U16 port; /* sent-by port */ char branch[SIP_VIA_BRANCH_LEN]; /* branch parameter */ char received[SIP_VIA_RECEIVED_LEN]; /* received parameter */ U16 rport; /* rport parameter */ } sip_via; /* 认证信息结构体 */ typedef struct auth_info_t { char realm[AUTH_REALM_LEN]; char nonce[AUTH_NONCE_LEN]; char algorithm[AUTH_ALGORITHM_LEN]; char qop[AUTH_QOP_LEN]; char opaque[AUTH_OPAQUE_LEN]; int stale; char response[AUTH_RESPONSE_LEN]; } auth_info_t; /* SIP消息头结构 */ typedef struct sip_headers { sip_uri from; /* From header */ sip_uri to; /* To header */ sip_via via; /* Via header */ char call_id[SIP_HEADER_CALLID_LEN]; /* Call-ID header */ U32 cseq; /* CSeq number */ enum sip_method cseq_method; /* CSeq method */ U8 max_forwards; /* Max-Forwards header */ char content_type[SIP_HEADER_CONTENT_TYPE_LEN]; /* Content-Type */ char contact[SIP_HEADER_CONTACT_LEN]; /* Contact header */ U32 content_length; /* Content-Length header */ U32 Expires; char user_agent[SIP_HEADER_USER_AGENT_LEN]; /* User-Agent */ auth_info_t auth; } sip_headers; /* SIP消息体结构 */ typedef struct sip_body { char *content; /* Message body content */ U32 length; /* Message body length */ char type[SIP_BODY_TYPE_LEN]; /* Content type */ } sip_body; /* SIP消息结构 */ typedef struct sip_message { U8 type; /* 0:request, 1:response */ enum sip_method method; /* Method (requests) */ U16 status_code; /* Status code (responses) */ char reason_phrase[SIP_REASON_PHRASE_LEN]; /* Reason phrase */ struct sip_uri request_uri; /* Request-URI */ struct sip_headers headers; /* SIP headers */ struct sip_body body; /* Message body */ struct sockaddr_in source; /* Message source */ struct list_head list; /* List head for queue */ } sip_message; /* SIP事务信息 */ typedef struct sip_transaction { char branch[SIP_BRANCH_LEN]; /* Transaction branch */ enum sip_method method; /* Transaction method */ U32 cseq; /* CSeq number */ int timeout_idx; struct sip_message *request; /* Original request */ struct sip_message *last_response; /* Last response */ sip_transaction_state state; } sip_transaction; /* 增强的对话状态 */ typedef enum dialog_state { DIALOG_EARLY, /* 早期对话(收到1xx) */ DIALOG_CONFIRMED /* 已确认对话(收到2xx) */ } dialog_state; /* SIP对话信息 */ typedef struct sip_dialog { char remote_tag[SIP_URI_PARAM_LEN]; /* From */ char local_tag[SIP_URI_PARAM_LEN]; /* To */ char call_id[SIP_HEADER_CALLID_LEN]; sip_uri remote_uri; sip_uri local_uri; } sip_dialog; /* 协议栈配置 */ typedef struct sip_protocol_config { char user_agent[SIP_CONFIG_USER_AGENT_LEN]; U8 max_forwards; U32 t1_timeout; /* T1 timer (RTT) */ U32 t2_timeout; /* T2 timer (64*T1) */ U32 t4_timeout; /* T4 timer (5000ms) */ } sip_protocol_config; /* SIP协议栈接口 */ /** * @brief SIP消息解析 * * @param[out] msg 解析出的sip消息 * * @param[in] data sip消息原始数据 * * @param[in] length 数据长度 * * @return 错误码 * */ int sip_message_parse(sip_message *msg, const char *data, U32 length); /** * @brief SIP消息解析 * * @param[in] msg sip消息结构体 * * @param[out] buffer 构造出的sip消息 * * @param[in] size 报文大小 * * @return 错误码 * */ int sip_message_build(sip_message *msg, char *buffer, U32 size); void sip_message_free(sip_message *msg); /** * @brief SIP消息拷贝 * * @param[in] src 源消息指针 * * @return sip消息指针 * */ sip_message *sip_message_copy(sip_message *src); int sip_uri_parse(sip_uri *uri, const char *uri_str); int sip_uri_build(sip_uri *uri, char *buffer, U32 size); /** * @brief 创建SIP事务 * * @param[ori_req] 源请求 * * @return SIP事务指针 * */ sip_transaction *sip_transaction_create(sip_message *ori_req, void *timeout_hanler); void sip_transaction_terminate(sip_transaction *trans); void sip_transaction_free(sip_transaction *trans); sip_dialog *sip_dialog_create(char *to_tag, char *from_tag, char *call_id); void sip_dialog_free(sip_dialog *dialog); const char *sip_method_to_string(enum sip_method method); sip_method sip_string_to_method(const char *method_str); const char *sip_status_to_reason(U16 status_code); void sip_generate_branch(char *branch, U32 size); void sip_generate_tag(char *tag, U32 size); void sip_generate_call_id(char *buf, int len); #endif /* _SIP_PROTOCOL_H */ #ifndef _SIP_CLIENT_H #define _SIP_CLIENT_H #include "common/sip_common.h" #include "sip_protocol.h" #include "media.h" #include "rtmp_sys.h" #include "sdp.h" #define MD5_APPEND(md5, buf, len) tpssl_Md5Update(md5, (uint8_t *)buf, (unsigned)len) #define REGISTER_INTERVAL 30 #define CALL_TIMEOUT 30 #define ESTABLISHED_TIMEOUT 120 #define TERMINATE_TIMEOUT 2 #define MAX_REGISTER_RETRY 10 #define TCP_RECONNECT_INTERVAL 10 /* 限制配置内容不超过对应最大长度 */ #define SIP_MAX_USERNAME 32 #define SIP_MAX_PASSWORD 32 #define SIP_MAX_ADDR_LEN 64 /* SIP客户端状态 */ typedef enum sip_client_state { SIP_STATE_IDLE = 0, SIP_STATE_INVITING, SIP_STATE_RINGING, SIP_STATE_ESTABLISHED, SIP_STATE_ERROR, SIP_STATE_MAX } sip_client_state; /* 客户端事件类型 */ typedef enum sip_event_type { /* 本地控制事件 */ EVENT_LOCAL_CALL = 0, // 发起呼叫 EVENT_LOCAL_ANSWER, // 接听呼叫 EVENT_LOCAL_HANGUP, // 挂断通话 EVENT_LOCAL_TIMEOUT, // 事务超时 /* 网络请求事件 */ EVENT_NET_INVITE, // 收到INVITE请求 EVENT_NET_ACK, // 收到ACK请求 EVENT_NET_BYE, // 收到BYE请求 EVENT_NET_CANCEL, // 收到CANCEL请求 /* 核心响应事件 */ EVENT_NET_1XX_PROVISIONAL, // 所有1xx临时响应 EVENT_NET_2XX_SUCCESS, // 200类成功响应 EVENT_NET_3XX_REDIRECTION, // 300类重定向响应 EVENT_NET_4XX_CLIENT_ERROR, // 400类客户端错误 EVENT_NET_5XX_SERVER_ERROR, // 500类服务器错误 /* 关键特殊事件 */ EVENT_NET_180_RINGING, // 180振铃提示 EVENT_NET_AUTH_CHALLENGE, // 认证挑战(401/407) /* 系统级事件 */ EVENT_ERROR, // 传输错误 EVENT_MAX // 事件类型总数 } sip_event_type; /* 回调函数类型 */ typedef int (*sip_callback_t)(sip_event_type event, void *sip_msg); /* 客户端配置 */ typedef struct sip_client_config_t { BOOL usetsl; char server_addr[SIP_MAX_ADDR_LEN]; char username[SIP_MAX_USERNAME]; char password[SIP_MAX_PASSWORD]; char local_addr[SIP_MAX_ADDR_LEN]; U16 server_port; } sip_client_config; // 200B /* SIP客户端结构 */ typedef struct SIPCLIENT_S { /* 当前事务 */ sip_dialog *current_dialog; sip_transaction *current_trans; sip_message cur_register; auth_info_t auth; enum sip_client_state state; sip_client_config config; int state_timeout_timer_idx; int registered_expired_timer_idx; int response_timeout_timer_idx; int register_timer_idx; int tcp_reconnect_timer_idx; /* 网络连接 */ int isock; S32 sip_socket; TPSIPSESSION *media_session; // 200B BOOL registered; BOOL connected; sdp_t *sdp_net; sdp_t *sdp_local; U32 call_count; char from_tag[32]; } SIPCLIENT; // 1,956 /* SIP客户端接口 */ /** * @brief SIP客户端初始化 * * @param[in] config 客户端配置 * * @return 错误码 * */ int sip_client_init(sip_client_config config); int sip_client_deinit(); /** * @brief SIP客户端拨号 * * @param[in] number 目标号码 * * @return 错误码 * * @note 只有在空闲状态下才能发出拨号,其余状态不做处理 */ int sip_client_make_call(const char *number); /** * @brief SIP客户端接听来电 * * @return 错误码 * * @note 只有在振铃状态下才会成功接听来电,其余状态不做处理 */ int sip_client_answer_call(); /** * @brief SIP客户端挂断电话 * * @return 错误码 * * @note 只有在非空闲状态下才能挂断电话,空闲状态不做处理 */ int sip_client_hangup_call(); /** * @brief 获取当前SIP客户端结构体指针 * * @return SIPCLIENT:SIP客户端结构体指针 * */ SIPCLIENT *get_sip_client(); /** * @brief 获取当前SIP客户端配置结构体指针 * * @return SIPCLIENT:SIP客户端配置结构体指针 * */ sip_client_config *get_sip_config_client(); void sip_client_set_idle_cb(sip_callback_t cb); void sip_client_set_inviting_cb(sip_callback_t cb); void sip_client_set_ringing_cb(sip_callback_t cb); void sip_client_set_established_cb(sip_callback_t cb); #endif /* _SIP_CLIENT_H */ 帮我想一下怎么修改设计来使这个sip客户端设计更加合理,能够同时支持多个对话
最新发布
11-15
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值