国标28181:libexosip2协议栈原理

1059 篇文章 286 订阅

exosip2使用原理

  • eXosip是Osip2协议栈的封装和调用。它实现了作为单个sip终端的大部分功能,如register、call、subscription等。
  • eXosip使用UDP socket套接字实现底层sip协议的接收/发送。并且封装了sip消息的解释器。
  • eXosip使用定时轮循的方式调用Osip2的transaction处理函数,这部分是协议栈运转的核心。透过添加/读取transaction消息管道的方式,驱动transaction的状态机,使得来自远端的sip信令能汇报给调用程序,来自调用程序的反馈能通过sip信令回传给远端。
  • eXosip增加了对各个类型transaction的超时处理,确保所有资源都能循环使用,不会被耗用殆尽。
  • eXosip使用jevent消息管道来向上通知调用程序底层发生的事件,调用程序只要读取该消息管道,就能获得感兴趣的事件,进行相关的处理。
  • eXosip里比较重要的应用有j_calls、j_subscribes、j_notifies、j_reg、j_pub、osip_negotiation和authinfos。J_calls对应呼叫链表,记录所有当前活动的呼叫。J_reg对应注册链表,记录所有当前活动的注册信息。Osip_negotiation记录本地的能力集,用于能力交换。Authinfos记录需要的认证信息。

模块组成

底层连接管理

与网络连接有关。实现了连接的建立、数据的接收以及发送等相关的接口

  • eXtl_udp.c:为使用UDP连接的实现
  • extl_tcp.c:为使用TCP连接的实现
  • eXtl_dtls.c、eXtl_tls.c:为使用安全socket连接的实现

内部功能模块实现

Jauth.c、jcall.c、jdialog.c、jevents.c、jnotify.c、jpublish.c、jreg.c、jrequest.c、jresponse.c、jsubscribe.c 等文件实现了内部对一些模块的管理,这些模块正如其文件名所表示的,jauth.c主要是认证,jcall.c 则是通话等等。

上层API 封装实现

eXcall_api.c、eXinsubscription_api.c、eXmessage_api.c…这几个以api 为后缀的文件,实现各个子模块的管理。应用程序可以调用这里提供的接口,方便的构造或者发送sip 消息。

其他

  • inet_ntop.c 实现ip 地址的点分十进制与十六进制表示之间的转换。
  • jcallback.c 实现一堆回调函数,这些回调函数就是用来注册到osip 库的。我们使用exosip 库,就是避免直接使用osip 库,因为一些工作exosip 已经帮我们做了,所以这样一来,可以简化上层的实现。
  • udp.c:主要用来对通过UDP 连接接收到的消息进行分类处理。
  • eXutils.c:实现一些杂项的函数。有ip 地址到字符串之间的转换,域名的解析等一些 辅助的功能函数。
  • exconf.c 文件实现了exosip 的初始化相关的接口,包括后台任务的实现。实际上是 “configuration api”的实现。
  • eXosip.c文件实现了与exconf.c 文件相似的功能。比如管道的使用,exosip 上事务的创建和查找,register 和subscribe 的更新,认证信息的处理等。

关键数据结构

eXtl_protocol

eXtl_protocol 是为实现网络通信专门定义的一个数据结构,包括了变量和方法两部分。其中:

  • 变量包括了建立网络连接过程中使用的ip 地址、端口等;
  • 方法部分封装了网络socket编程常用的系统调用接口。
    在这里插入图片描述
struct eXtl_protocol {
    int enabled;

    int proto_port;
    char proto_name[10];
    char proto_ifs[20];
    int proto_num;
    int proto_family;
    int proto_secure;
    int proto_reliable;

    int (*tl_init) (void);
    int (*tl_free) (void);
    int (*tl_open) (void);
    int (*tl_set_fdset) (fd_set * osip_fdset, fd_set * osip_wrset, int *fd_max);
    int (*tl_read_message) (fd_set * osip_fdset, fd_set * osip_wrset);
    int (*tl_send_message) (osip_transaction_t * tr, osip_message_t * sip,
                            char *host, int port, int out_socket);
    int (*tl_keepalive) (void);
    int (*tl_set_socket) (int socket);
    int (*tl_masquerade_contact) (const char *ip, int port);
    int (*tl_get_masquerade_contact) (char *ip, int ip_size, char *port,
                                      int port_size);
};


struct eXtl_protocol eXtl_udp = {
    1,
    5060,
    "UDP",
    "0.0.0.0",
    IPPROTO_UDP,
    AF_INET,
    0,
    0,

    &udp_tl_init,
    &udp_tl_free,
    &udp_tl_open,
    &udp_tl_set_fdset,
    &udp_tl_read_message,
    &udp_tl_send_message,
    &udp_tl_keepalive,
    &udp_tl_set_socket,
    &udp_tl_masquerade_contact,
    &udp_tl_get_masquerade_contact
};



struct eXtl_protocol eXtl_tcp = {
    1,
    5060,
    "TCP",
    "0.0.0.0",
    IPPROTO_TCP,
    AF_INET,
    0,
    0,

    &tcp_tl_init,
    &tcp_tl_free,
    &tcp_tl_open,
    &tcp_tl_set_fdset,
    &tcp_tl_read_message,
    &tcp_tl_send_message,
    &tcp_tl_keepalive,
    &tcp_tl_set_socket,
    &tcp_tl_masquerade_contact,
    &tcp_tl_get_masquerade_contact
};


struct eXtl_protocol eXtl_dtls = {
    1,
    5061,
    "DTLS-UDP",
    "0.0.0.0",
    IPPROTO_UDP,
    AF_INET,
    0,
    0,

    &dtls_tl_init,
    &dtls_tl_free,
    &dtls_tl_open,
    &dtls_tl_set_fdset,
    &dtls_tl_read_message,
    &dtls_tl_send_message,
    &dtls_tl_keepalive,
    &dtls_tl_set_socket,
    &dtls_tl_masquerade_contact,
    &dtls_tl_get_masquerade_contact
};


struct eXtl_protocol eXtl_tls = {
    1,
    5061,
    "TLS",
    "0.0.0.0",
    IPPROTO_TCP,
    AF_INET,
    0,
    0,

    &tls_tl_init,
    &tls_tl_free,
    &tls_tl_open,
    &tls_tl_set_fdset,
    &tls_tl_read_message,
    &tls_tl_send_message,
    &tls_tl_keepalive,
    &tls_tl_set_socket,
    &tls_tl_masquerade_contact,
    &tls_tl_get_masquerade_contact
};

代码中定义了四个该数据结构体的全局变量:eXtl_udp、eXtl_tcp、eXtl_tls 以及eXtl_dtls。分别针对使用UDP、TCP 以及安全加密连接进行了实现。
在这里插入图片描述

eXosip_call_t

eXosip_call_t定义了call 相关的信息,包括call 的id,call 的dialogs,call 上incoming的事务和outgoing 的事务。另外,还包括了前向和后向指针,所以,所有的call 可以通过该结构体串接起来。
在这里插入图片描述

typedef struct eXosip_call_t eXosip_call_t;

struct eXosip_call_t {
  int c_id;
  eXosip_dialog_t *c_dialogs;
  osip_transaction_t *c_inc_tr;
  osip_transaction_t *c_out_tr;
  osip_transaction_t *c_cancel_tr;
  int c_retry; /* avoid too many unsuccessful retry */
  void *external_reference;

  time_t expire_time;

  eXosip_call_t *next;
  eXosip_call_t *parent;
};

eXosip_dialog_t

exosip_dialog_t 包含了dialog 相关的信息。

在这里插入图片描述

eXosip_reg_t

用来管理Register模块
在这里插入图片描述

eXosip_subscribe_t

用来管理subscribe模块
在这里插入图片描述

eXosip_pub_t

用来管理publish模块
在这里插入图片描述

eXosip_notify_t

用来管理notify模块
在这里插入图片描述

jinfo_t

这个结构体关联了dialog、call、subscribe以及notify几个结构体

eXosip_event_t

与 event 有关的结构体。这个结构体主要用来在应用层和exosip 之间通信。Exosip 在处理事务的过程中,如果需要将结果反馈给上层应用,则会生成如上结构类型的事件,并将其放到exosip 的事件队列中。应用层会不断循环从事件队列中读取事件,然后进行应用层的处理。

 /**
 * Structure for event description
 * @struct eXosip_event
 */
  struct eXosip_event
  {
    eXosip_event_type_t type;               /**< type of the event */
    char textinfo[256];                     /**< text description of event */
    void *external_reference;               /**< external reference (for calls) */

    osip_message_t *request;       /**< request within current transaction */
    osip_message_t *response;      /**< last response within current transaction */
    osip_message_t *ack;           /**< ack within current transaction */

    int tid; /**< unique id for transactions (to be used for answers) */
    int did; /**< unique id for SIP dialogs */

    int rid; /**< unique id for registration */
    int cid; /**< unique id for SIP calls (but multiple dialogs!) */
    int sid; /**< unique id for outgoing subscriptions */
    int nid; /**< unique id for incoming subscriptions */

    int ss_status;  /**< current Subscription-State for subscription */
    int ss_reason;  /**< current Reason status for subscription */
  };

eXosip_t

在这里插入图片描述
exosip_t 是exosip 中最重要的结构体之一。从图可以看出,这个结构体比较大,其中包含了exosip 中用到的各个子模块的结构。比如call、reg、pub 等等。代码中定义了一个该结构类型的全局变量,通过该全局变量,就可以对exosip 前的状态进行掌控(许多相关的信息要么包含在该结构上,要么可以通过该结构找到)。

  • extl 是eXtl_protocol 类型的指针,保存了网络接口类。
  • j_osip 保存了osip 初始化时返回的osip 结构体。
  • j_transactions 一般是等待释放的事务。在事务经过osip处理完后,不再需要时,exosip会将其放在j_transactions上,等待释放。
    struct eXosip_t {
        struct eXtl_protocol *eXtl;
        char transport[10];
        char *user_agent;

        eXosip_call_t *j_calls;    /* my calls        */
#ifndef MINISIZE
        eXosip_subscribe_t *j_subscribes;    /* my friends      */
        eXosip_notify_t *j_notifies;    /* my susbscribers */
#endif
        osip_list_t j_transactions;

        eXosip_reg_t *j_reg;    /* my registrations */
#ifndef MINISIZE
        eXosip_pub_t *j_pub;    /* my publications  */
#endif

#ifdef OSIP_MT
        void *j_cond;
        void *j_mutexlock;
#endif

        osip_t *j_osip;
        int j_stop_ua;
#ifdef OSIP_MT
        void *j_thread;
        jpipe_t *j_socketctl;
        jpipe_t *j_socketctl_event;
#endif

        osip_fifo_t *j_events;

        jauthinfo_t *authinfos;

        int keep_alive;
        int keep_alive_options;
        int learn_port;
#ifndef MINISIZE
        int http_port;
        char http_proxy[256];
        char http_outbound_proxy[256];
        int dontsend_101;
#endif
        int use_rport;
        int dns_capabilities;
        int dscp;
        char ipv4_for_gateway[256];
        char ipv6_for_gateway[256];
#ifndef MINISIZE
        char event_package[256];
#endif
        struct eXosip_dns_cache dns_entries[MAX_EXOSIP_DNS_ENTRY];
        struct eXosip_account_info account_entries[MAX_EXOSIP_ACCOUNT_INFO];
        struct eXosip_http_auth http_auths[MAX_EXOSIP_HTTP_AUTH];

        CbSipCallback cbsipCallback;
    };

exosip的初始化

Exosip 的初始化有两部分组成,这主要是从使用exosip 的角度看。

(1)对exosip 全局结构体变量的配置。这步通过调用接口eXosip_init 完成。主要完成工作如下:

  • 初始化条件变量和互斥信号量。
  • 调用osip_init 初始化osip 库,并将生成osip 结构体给exosip,同时也让osip 的application_contexgt 指针指向exosip,也就是二者相互指向。
  • 调用eXosip_set_callbacks 设置osip 的回调函数,所以回调函数都是exosip 自己实现。
  • 调用jpipe 创建通信用的pipe。对于windows 平台,是通过socket 接口模拟实现的。
  • 初始化其上的事务和事件队列。这不同于osip 的事务和事件队列。
  • 调用extl 指向的结构体的init 函数指针,初始化网络接口。

初始化完后,全局exosip结构在内存中的状态基本如下图:
在这里插入图片描述
其中的橘红色标识了初始化过程中涉及到的部分。

(2)在socket 接口上进行监听
这步通过调用eXosip_listen_addr 接口完成。主要完成工作如下:

  • 将eXosip 全局变量的eXtl 指针指向eXtl_udp 全局变量

  • 根据参数,配置extl_protocol 和exosip 上有关ip 端口地址等信息。另外,调用extl_udp的tl_open 函数指针,完成在本机指定的端口上监听连接的工作。需要注意的是,虽然是监听,但是使用的UDP 来建立连接的,所以消息的recv 和发送在同一个socket 上完成。在osip中设置的out_socket 并不会起作用。

  • 调用osip_thread_create 创建exosip 后台任务,用于驱动osip 的状态机。(在osip 中,在发送sip 消息部分,提到将9 个函数放到一个线程中执行,exosip 就是这样做的)

    这部分工作完成后,内存状态如下图所示:
    在这里插入图片描述
    上图中浅绿色部分显示了这部分工作所作的配置。J_thread保存了任务的句柄。

示例代码

#include "stdio.h"
#include <eXosip2/eXosip.h>
#include <iostream>
#include <rpc/types.h>

using namespace std;


int main(void)
{
    int listenport = 8888;
    //库处理结果
    int result = OSIP_SUCCESS;
    TRACE_INITIALIZE (6, stdout);
    eXosip_t *ctx = eXosip_malloc();
    result = eXosip_init(ctx);
    if (OSIP_SUCCESS != result)
    {
        printf("Can't initialize eXosip!\n");
        return 1;
    }
    printf("eXosip_init successfully!\n");
    //监听
    result = eXosip_listen_addr(ctx, IPPROTO_UDP,NULL,listenport,AF_INET,0);
    if (OSIP_SUCCESS != result){
        printf("eXosip_listen_addr failure.\n");
        return 1;
    }
    printf("eXosip_listen_addr successfully.\n");
    // 结束
    eXosip_quit(ctx);
    ctx = nullptr;

    return 0;
}



这样,在初始化完成后,我们基本上完成了对内存中所用数据结构的配置,同时启动了一个后台任务负责osip状态机的驱动。用户就可以发送和接收SIP事件了

数据收发整体框架

接收过程

在初始化过程中我们创建了一个后台任务,现在可以看看这个后台任务都做了哪些操作。任务的执行函数为_eXosip_thread,在该接口中,循环不断的调用eXosip_execute。在每一次的eXosip_execute执行中,完成如下的工作:

  • 首先计算出底层osip离当前时间最近的超时时间。也就是查看底层所有的超时事件,找出其中的最小值,然后将其与当前时间做差,结果就是最小的超时间隔了。这步是通过调用接口osip_timers_gettimeout完成的。主要检查osip全局结构体上的ict、ist、nict、nist以及ixt上所有事务的事件的超时时间。 如果ict事务队列上没有事件,则说明没有有效的数据交互存在,返回值为默认的一年,实际上就是让后面的接收接口死等。如果有事务队列上的事件的超时时间小于当前值,则说明已经超时了,需要马上处理,此时将超时时间清为零,并返回。
  • 调用eXosip_read_message接口从底层接收消息并处理。如果返回-2,则任务退出。
  • 执行osip的状态机。具体为执行osip_timers_ict(ist|nict|nist)_execute和osip_ict(ist|nict|nist)_execute这几个函数。最后还检查释放已经终结的call、registrations以及publications。
  • 如果keep_alive设置了,则调用_eXosip_keep_alive检查发送keep_alive消息。

这样,当远端的终端代理发送sip消息过来时,会被之前创建的监听端口捕获(sip协议默认的端口为5060)。在调用eXosip_read_message接口时会将其接收上来。

接收上来的数据存放在buffer中交给接口_eXosip_handle_incoming_message来处理。在其中首先调用osip_parse进行消息的解析,这是osip的核心功能之一。数据解析后,会生成一个osip_event类型的事件。接着调用osip_message_fix_last_via_header将接收到该消息的ip地址和端口根据需要设置到数据头的via域中。这在消息返回时有可能发挥作用。为了能够让消息正确的被处理,调用osip_find_transaction_and_add_event接口将其添加到osip的事务队列上。处理在这之后发生了分叉,如果osip接纳了该事件,接口直接返回,因为这说明该事件在osip上已经有匹配的事务了,或者说该事件是某一个事务过程的一部分。这样在后面执行状态机的接口时,该事件会被正确的处理。如果osip没有拿走该事件,则说明针对该事件还没有事务与之对应。此时,我们首先检查其类型,如果是request,则说明很可能是一个新的事件到来(这将触发服务端的状态机的建立),调用eXosip_process_newrequest接口进行处理。如果是response,则调用接口eXosip_process_response_out_of_transaction处理。

在eXosip_process_newrequest接口中,如果是合法的事件,则会为其创建一个新的事务。也就是说这是新事务的第一个事件。经过一大堆的处理后,该事件可能就被osip消化了,或者被exosip消化了。如果需要上报给应用,由应用拿来对一些信息进行存储或者进行图形显示之类,则会将该事件添加到exosip的事件队列上。如下图所示:

在这里插入图片描述
应用程序在exosip初始化完之后需要调用如下类似的代码,不断从事件队列上读取事件,并进行处理。

//开启循环消息,实际应用中可以开启多线程同时接收信号
    eXosip_event_t* osipEventPtr = NULL;

    while (true)
    {
        // Wait the osip event.
        osipEventPtr = ::eXosip_event_wait(0, 200);
                eXosip_lock();
        //一般处理401/407采用库默认处理
        eXosip_default_action(osipEventPtr);
        eXosip_unlock();
        // If get nothing osip event,then continue the loop.
        if (NULL == osipEventPtr)
        {
            continue;
        }
        // 事件处理

        switch (osipEventPtr->type)
        {
        //需要继续验证REGISTER是什么类型
        case EXOSIP_REGISTRATION_NEW:
        {
            //注册事件处理
        }
            break;
        case EXOSIP_MESSAGE_NEW:
        {
                //消息事件处理
        }
            break;
            case XXXXX:
        {
                //事件处理
        }
            break;
           case XXXXX:
        {
                //事件处理
        }
            break;
        default:
            cout << "未处理消息 : " << osipEventPtr->type<<endl;
            break;
        }
        eXosip_event_free(osipEventPtr);
        osipEventPtr = NULL;

每当发送一个SPI消息,都会收到一个事件。每个事件包含受影响事务的原始请求,以及触发有效事件的最后应答,
你可以从这些信息获取所有的头部,并存储起来,以方便后续的操作或者显示。
例如:当收到一个呼叫传输REFER请求时,你可以检索“refer-To”头部:

发送过程

要发送数据时,需要根据消息类型,调用exosip对应模块的api接口函数来完成。如果要发送的sip消息不属于当前已有的任何事务,则类似接收过程,调用osip的相关接口创建一个新的事务,同时根据消息生成一个事件,加到事务的事件队列上。最后,唤醒exosip后台进程,使其驱动osip状态机,执行刚添加的事件,从而完成数据的状态处理和发送。当然,也有一些消息并不通过osip状态机,而是由exosip直接调用回调函数cb_snd_message完成发送。

小结

使用eXosip时,第一步工作是初始化eXosip和libosip库(解析器和状态机)。在使用libeXosip2前必须必须先做这一步。

   #include <eXosip2/eXosip.h>
 
   int i;
 
   i = eXosip_init(); // 初始化eXosip和osip协议栈
 
    if (i != 0)
        return -1;
 
    i = eXosip_listen_addr (IPPROTO_UDP, NULL, 5060, AF_INET, 0); // 打开信号socket
 
    if (i != 0) // 传输层初始化失败
    {
        eXosip_quit();
 
        return -1;
    }

然后可以发送消息,等待并处理eXosip事件。

     eXosip_event_t *je = NULL;
 
    while (1)
    {    
        je = eXosip_event_wait(0,50); // 侦听是否有消息到来
 
         eXosip_lock();
        eXosip_default_action(je);
        eXosip_unlock ();
 
        if (je == NULL) // 没有接收到消息
            continue;
 
        switch (je->type)
        {
         case EXOSIP_MESSAGE_NEW:
             break;
 
         case EXOSIP_CALL_INVITE:
             break;
 
         case EXOSIP_CALL_ACK:
             break;
 
         case EXOSIP_CALL_CLOSED:   
             break;
 
          case ...:
              break; 
 
          default:
               break;      
          }
 
          eXosip_event_free(je);
    }

每当发送一个SIP消息,都会收到一个事件。每个事件包含受影响事务的原始请求,以及触发有效事件的最后应答,

你可以从这些信息获取所有的头部,并存储起来,以方便后续的操作或者显示。

例如:当收到一个呼叫传输REFER请求时,你可以检索“refer-To”头部:

     osip_header_t *referto_head = NULL;
    i  = osip_message_header_get_byname(evt->sip, "refer-To", 0, &referto_head);
    if (referto_head == NULL || referto_head->value == NULL)

eXosip_event包含呼叫标识符、注册信息、传入订阅、呼出订阅等。这些标识符将会被API用于控制呼叫、注册、传入或呼出订阅,这些API将会建立默认的带SIP头的信息头(From, To, Call-ID, CSeq, Route, Record-Route,Max-Forward…),并为你发送信息。

关系

exosip与上层应用以及osip之间的流程关系

应用需要接收数据时流程如下:
在这里插入图片描述
上图为接收过程的示意图。Exosip后台任务不断从网络另一端读取sip消息,交给osip的parser模块解析,并将其转换为events,添加到事务队列上。同时,exosip后台任务在不断的驱动osip的状态机,这样,事务队列上的事件就会被处理。如果需要响应对端,状态机会根据回调函数的设置,直接完成数据的发送。同样,如果要将当前处理反馈给应用,则将其发送到事件队列上(这里是exosip的事件队列),并通过e-ctl管道通知应用进行处理。

应用需要发送数据时,流程如下图所示:
在这里插入图片描述
此时,应用调用exosip提供的辅助函数(上图中虚线示意此关系),构造osip事件,将其添加到osip的事务队列上。同时,应用通过s-ctl管道通知exosip后台任务执行状态机。在exosip执行状态机的过程中,sip消息会被发送到网络另一端的终端。

数据结构体之间的关系

在exosip中,有一个比较大的数据结构图是exosip_t,这个在前面的数据结构说明中就介绍了。这个结构体中包含了一些子模块的数据结构,比如call,register等等。程序中,定义了一个exosip_t类型的全局变量,这样,通过该全局变量,就可以获取各个子模块的信息。

在许多的开源软件中,经常会看到这种设计方法。

在这里插入图片描述

上图中,像call、register、subscribe等等,都有指向自身类型的前后指针,因此,实际运行中,多个实例都是串接在一起,形成一个双向链。

其他资源

参考

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值