2016년 7월 14일 목요일

PJSIP 개발 가이드


이 문서는 PJSIP Developer’s Guide의 내용을 pjsua 관점에서 간추릴려고 했으나  중요하다고 생각되는 섹션을 발번역한 것입니다. 간단하게 생각했는데... 뭐든지 쉬운게 없네요.
2016년 7월 15일 신C


1. General Design

1.1 아키텍쳐

1.2 The Endpoint

Endpoint 인스턴스(pjsip_endpoint)는 SIP 스택의 심장으로 아래의 역할을 한다.
응용프로그램은 하나의 SIP endpoint 인스턴스를 가진다.

  • 모든 SIP 컴포넌트를 위해 메모리를 할당 및 해제를 관리한다.
  • 트렌젝션의 전송 및 수신을 관리한다.
  • 전송계층으로 부터 메시지를 수신하고 이것을 자동으로 트렌젝션에 디스패치해준다.
  • timer 매니져 인스턴스 하나를 가지고, SIP 커포넌트를 위한 timer를 스케줄링한다.
  • 추가적인 SIP 모듈을 관리한다. 이 모듈을 통해서 메시지를 수신하고 파싱하는 기본 SIP 스택을 확장한다.
  • 모든 오브젝트에 대해 한번에 폴링하고 이벤트를 분배한다. 다시말해서 스레드를 추가적으로 생성하지 않고 timer, socket 이벤트를 같이 폴링한다.
  • 수신된 요청을 자동으로 처리한다. 이 처리는 여러 모듈에 의해 처리된다.
  • 하나의 PJLIB ioqueue 인스턴스를 가진다. ioqueue는 네트워크 이벤트를 디스패치하기 위해  proactor 패턴으로 동작한다.
  • 구조체 정의 pjsip/src/pjsip/sip_endpoint.c
  • pjsua의 경우 인스턴스를 생성하는 pjsip_endpt_create()는 pjsip/src/pjsua-lib/pjsua_core.c:pjsua_create() 에서 호출된다.

- pjsua의 pjsip_endpt_create 호출 경로
main -> main_func -> pjsua_app_init -> app_init -> pjsua_create -> pjsip_endpt_create

1.2.1 Pool Allocation and Deallocations

SIP 컴포넌트는 endpoint를 통해서 메모리를 할당받는다. 할당함수는 아래와 같다.
  • pjsip_endpt_create_pool()
  • pjsip_endpt_release_pool()

1.2.2 Timer Management

SIP 컴포넌트를 위한 타이머 제어 함수. endpoint가 폴링시 타이머의 완료를 체크한다.
  • pjsip_endpt_schedule_timer
  • pjsip_endpt_cancel_timer

1.2.3 Polling the Stack

아래 함수에서 타이머와 네트워크 이벤트를 체크한다. 이 함수는 이벤트를 적절한 시간안에 처리하기 위해서 응용프로그램이 반드시 주기적으로 호출해야 한다.
  • pjsip_endpt_handle_events

- 호출경로: main -> main_func -> pjsua_app_init -> app_init ->
                 pjsua_init -> worker_thread -> pjsua_handle_events -> pjsip_endpt_handle_events2

-  pjsip_endpt_handle_events함수의 오퍼레이션 절차
  1. 타이머 폴링
  2. ioqueue 폴링
    지정한 수의 이벤트가 쌓일동안 폴링한다. 타임아웃이 발생하면 대기 종료.
  3. 지정한 개수가 쌓이면 성공리턴

2. Modlue

2.1.1 Modlue Declaration

아래 모듈의 콜백 함수포인터가 NULL일  경우 성공한 것으로 처리된다.
     <pjsip/include/pjsip/sip_module.h>
struct pjsip_module
{
PJ_DECL_LIST_MEMBER(struct pjsip_module); // For internal list mgmt.
pj_str_t name;                                                   // Module name.
int id;                                                                // Module ID, set by endpt
int priority;                                                         // Priority
pj_status_t (*load) (pjsip_endpoint *endpt);           // Called to load the mod.
pj_status_t (*start) (void);                                    // Called to start.
pj_status_t (*stop) (void);                                    // Called top stop.
pj_status_t (*unload) (void);                                 // Called before unload
pj_bool_t (*on_rx_request) (pjsip_rx_data *rdata);  // Called on rx request
pj_bool_t (*on_rx_response)(pjsip_rx_data *rdata);    // Called on rx response
pj_status_t (*on_tx_request) (pjsip_tx_data *tdata);   // Called on tx request
pj_status_t (*on_tx_response)(pjsip_tx_data *tdata);  // Called on tx request
void (*on_tsx_state) (pjsip_transaction *tsx,              // Called on transaction
pjsip_event *event);                                                 // state changed
};

endpoint가 모듈의 state를 관리하기 위해 load, start, stop, unload를 직접 호출한다. 아래는 모듈의 생명주기이다.
  • rx_request, rx_response : 모듈이 endpoint 또는 다른 모듈로 부터 SIP메시지를 받기위한 함수이다.
    리턴값이 굉징히 중요한데 콜백함수가 ‘0’이 아닌 값을 리턴하면 이 모듈이 메시지를 처리한 것으로 판단하고 다른 모듈에 전달하지 않는다.
  • tx_request, tx_response: transport manager가 메시지를 전송하기전 이 콜백함수를 호출한다.
    이렇게 함으로써 메시지를 수정할 수있는 기회를 제공하는 것이다. 모든 모듈은 반드시 성공을 리턴해야 한다. 아니면 endpoint가 처리됐다고 판단하고 보내지 않을 거니깐.
  • on_tsx_state: transaction state가 변경되었을 때마다 통지를 받기위해 사용된다. transaction state는 메시지 수신, 메시지 전송, 타이머 이벤트, 전송에러에 의해 변경된다.

2.1.2 Modlue Priorities

모듈 우선순위에 따라 모듈의 콜백이 호출되는 순서가 결정된다. 우선순위가 높은 모듈(숫자가 낮은)의  on_rx_request와 on_rx_response가 처음 호출되고, on_tx_request와 on_tx_response이 마지막에 호출된다.

enum pjsip_module_priority
{
PJSIP_MOD_PRIORITY_TRANSPORT_LAYER = 8,     // Transport
PJSIP_MOD_PRIORITY_TSX_LAYER = 16,                 // Transaction layer.
PJSIP_MOD_PRIORITY_UA_PROXY_LAYER = 32,     // UA or proxy layer
PJSIP_MOD_PRIORITY_DIALOG_USAGE = 48,          // Invite usage, event subscr. framework.
PJSIP_MOD_PRIORITY_APPLICATION = 64,              // Application has lowest priority.
};

  • PJSIP_MOD_PRIORITY_TRANSPORT_LAYER: transport manager 가 사용하는 우선순위로 메시지 전송을 제어하기위해 사용된다. 예를 들어 이 우선순위(8) 보다 낮다면 on_tx_request/on_tx_response 가 transport 레이어가 메시지를 처리하기 전에 먼저 호출된다.
  • PJSIP_MOD_PRIORITY_TSX_LAYER: transactin 레이어 모듈이 사용하는 것으로 이 레이어는 전송할 모든 메시지를 가져온다.
  • PJSIP_MOD_PRIORITY_UA_PROXY_LAYER: UA 레이어 또는 proxy 레이어가 사용한다. UA layer(dialog framework)는 다이얼로그 세트에 해당하는 모든 메시지를 가져온다.
  • PJSIP_MOD_PRIORITY_DIALOG_USAGE: 현재 PJSIP는 두가지 타입의 Dialog usage를 구현했다: invite sesstion, event subscription session. dialog usage는 특정 세션에 해당하는 다이얼로그의 내부 메시지를 가져온다.
  • PJSIP_MOD_PRIORITY_APPLICATION: 일반적인 어플리케이션 모듈에 적합한 우선순위다.

2.1.3 Incoming Message Processing by Modules


메시지가 수신(pjsip_rx_data)되면 아래의 순서로 모듈이 호출된다.
Transport manager가 메시지를 수신하면 파싱하여 파싱된 데이터 구조체를 수신메시지 버퍼에 넣어서 endpoint에 전달한다.
Endpoint는 수신된 메시를 모듈의 우선순위에 따라 제로가 아닌값을 리턴받을때 까지 등록된 모든 모듈에 전달한다. 전달방법은 각 모듈의 on_rx_request 또는 on_rx_response를 호출하는 것으로 수행된다.
어느 모듈에서 중단되더라도 내부에서 다른 모듈로 전할 수도 있다. 수신된 메시지 버퍼의 transaction 필드값을 통해 내부 트랜젝션인지 외부 트랜잭션인지 구분할 수 있다.

2.1.4 Outgoing Message Processing by Modules

외부로 나가는 요청 또는 응답은 전송 데이터 버퍼(pjsip_tx_data)에 담겨진다. 이 구조체는 메세지, 메모리 풀, 연속된  버퍼, 전송 정보가 담겨져 있다.

메시지를 전송하기위해 pjsip_transport_send이 호출되면, transport manager는 등록된 모든 모듈의 우선순위에 따라 on_tx_request 또는 on_tx_response를 호출한다. 우선순위가 낮은(숫자가 높은) 모듈이 먼저 호출되고 마지막은 일반적으로 transport manager가 호출된다. 위 Figure 4의 순서 반대라고 생각하면 쉽다.

transport 레이어는 전송버퍼 내부의 전송정보와 연속버퍼에 메시지 구조체를 쓰는 것을 처리한다.
메시지 구조체를 버퍼에 쓰기전에 변경을 원한다면 우선순위를 transport layer보다 낮게, 전송하기전 실제 패킷 바이트를 확인하려면 높게 설정해야한다. 실제로  transport layer보다 높은 경우는 전송패킷을 확인하기 위한 로깅모둘을 사용할 경우이다.

2.1.5  Transaction User and State Callback

on_tx_state 함수는 특정 트랜잭션 으로 부터 트랜잭션 상태가 변화될때 통지 받는데 사용된다. 트랜잭션의 상태는  타이머의 타임아웃이나 전송에러 같이 메시지와 관련없는 이벤트에 의해 변경된될 수도 있다.

이 콜백은 모듈이 transaction user로 등록된 이후 호출된다. 트랜잭션 하나당 오직 하나의 transaction user만이 등록된다.
다이얼로그에서 트랜잭션이 생성된다면, UA 레이어 모듈이 한 다이얼로그를 대신해서 transaction user로 설정된다.

2.1.8 Sample Callback Diagrams

 Incoming Message Outside Transaction and Outside Dialog

  1. Transport 매니져(pjsip_tpmgr)이 수신한 메시지를 파싱한 후 Endpoint에 전달한다.
  2. Endpoint(pjsip_endpt) 이 등록된 모듈의 콜백에 메시지를 전달한다.
    트랜잭션 레이어가 맨 처음 받아 이 메시지가 트랜잭션 테이블에 있는지 확인하지만 매칭되는 트랜잭션을 찾지 못한다.
  3. Endpoint가 다음 우선순위인 User Agent에 전달한다.
  4. User Agent가 메시지가 다이얼로그 테이블에 있는지 확인하지만 찾지 못한다.
  5. Application 콜백이 나올때 까지 등록된 모듈에 메시지를 전달한다. 어플리케이션이 메시지를 처리한다. (예: statelessly 로 응답하던지, UAS 트랜잭션을 생성한든지, 다이얼로그를 생성한다.)

Incoming Message Inside Transaction

  1. Transport 매니져(pjsip_tpmgr)이 수신한 메시지를 파싱한 후 Endpoint에 전달한다.
  2. Endpoint(pjsip_endpt) 이 등록된 모듈의 콜백에 메시지를 전달한다.
    트랜잭션 레이어가 맨 처음 받아 이 메시지와 트랜잭션 테이블에 매칭되는 트랜잭션을 찾았다.
  3. 트랜잭션 콜백이 PJ_TRUE를 리턴해서 Endpoint는 더이상 메시지를 다음 모듈에 전달하지 않는다.
  4. 트랜잭션이 응답을 처리한다(예: FSM을 업데이트) 만약 메시지 retrnasmission이면 처리는 여기서 끝난다. 아니라면 트랜잭션이 이 메시지를 Transaction User인 다이얼로그나 어플리케이션에 전달한다.
  5. TU가 다이얼로그라면, 다이얼로그는 응답을 처리하고 이 메시지를 다이얼로그 유저인 어플리케이션에 전달한다.
  6. 만약 메시지의 수신이 트랜잭션의 상태를 변경했다면, 트랜잭션은 상태를 트랜잭션 유저에 전달한다.
  7. TU가 다이얼로그라면, 다이얼로그 상태를 어플리케이션에 전달할 수 도있다.

Incoming Message Inside Dialog but Outside Transaction

  1. Transport 매니져(pjsip_tpmgr)이 수신한 메시지를 파싱한 후 Endpoint에 전달한다.
  2. Endpoint(pjsip_endpt) 이 등록된 모듈의 콜백에 메시지를 전달한다.
    트랜잭션 레이어가 맨 처음 받아 이 메시지가 트랜잭션 테이블에 있는지 확인하지만 매칭되는 트랜잭션을 찾지 못한다.
  3. Endpoint가 메시지를 다음 모듈에 전달한다. User Agent 모듈이 메시지를 받는다.
  4. User Agent 모듈이 다이얼로그 해쉬 태이블에서 메시지와 메칭되는 다이얼로그를 찾았다.
  5. User Agent 모듈은 메시지를 매칭된 다이얼로그에 전달한다.
  6. 다이얼로그는 수신요청에 대해 항상 트랜잭션을 생성한다. 그리고 on_rx_request()를 호출함으로써 요청을 전달한다. 그리고 다이얼로그 usage의 on_tsx_state()를 호출한다.

5. Message Buffers

5.1 Receive Data Buffer

PJSIP가 수신한 SIP 메시지는 그대로 사용되지 않고 pjsip_rx_data 구조체에 담겨져 다른 컴포넌트에 넘겨진다. 이 구조체는 수신한 메시지를 표현하기 위한 여러정보를 포함하게 된다. 수신/발신 데이터 버퍼의 구조체는 <pjsip/sip_transport.h> 에 선언되어 있다. (신C: 구조체 멤버로 msg_buf, from, to, via ..같은 것들이 있다.)

5.1 Transmit Data Buffer

PJSIP 어플리케이션이 메시지를 전송하려면 반드시 전송데이터 버퍼를 생성해야 한다. 전송 데이터 버퍼는 메모리 풀을 제공하는데 이것은 메시지 필드를 위해 메모리를 할당할 때나 메시지를 처리를 위한 정보를 위해  사용된다.

6. Transport Layer

트랜스포트는 네트워크를 통해 메시지를 전송할 때 사용된다. PJSIP transport framework는 확장가능하다. 즉 어플리케이션이 구현한 전송 방식을 등록해서 사용할 수 있다.

6.1 Transport Layer Design

6.1.2 Transport Manager

트랜스포트 매니져(pjsip_tpmgr)는 모든 트랜스포트 오브젝트와 팩토리를 관리한다. 제공하는 기능은 아래와 같다.
  • 트랜스포트 레퍼런스 카운트와 아이들 타이머를 이용하여 트랜스포트 생명주기를 관리한다.
  • 트랜스포트 팩토리를 제어한다.
  • 트랜스포트로 부터 패킷을 수신하고 파싱한 후 SIP endpoint에 전달한다.
  • 메시지를 전달할 목적지의 전송타입과 주소에 맞는 트랜스포트를 찾아준다.
  • 새로운 목적지에 메시지를 보낼수 있는 트랜스포트가 없으면 새로운 트랜스포트를 생성한다.
endpoint 에 하나의 트랜스포트만이 존재한다. 트랜스포트 매니져는 일반적으로 어플리케이션이 접근할 수 없고, endpoint가 제공하는 함수를 이용해야 한다.

6.1.3 Transport Factory

트랜스포트 팩토리(pjsip_tpfactory)는 리모트 endpoint에 새로운 연결을 생성하기 위해 사용된다. TCP로 연결된 경우 하나의 TCP 트랜스포트가 생성되어야 한다.
트랜스포트 매니져가 새로운 트랜스포트를 생성의 필요성을 확인하게되면 연결에 맞는 스펙으로 트랜스팩토리를 찾아 커넥션을 생성한다.

6.1.4 Transport

트랜스포트 오브젝트는 pjsip_transport 구조체로 표현된다. 각 구조체의 인스턴스는 트랜스포트 레이어가 소켓이 없는 트랜스포트를 지원하기도 하지만 보통의 경우 하나의 소켓 핸들을 나타낸다.

General Transport Operations
프레임워크의 관점에서, 트랜스포트 오브젝트는 하나의 액티브 오브젝트이다. 프레이임워크는 트랜스포트 오브젝트를 폴링하기 위한 메카니즘이 없다 대신 트랜스포트 오브젝트가 네트워크로 부터 패킷을 수신하거나 트랜스포트 매니져에게 패킷을 전달하기 위해 자신의 방식을 찾아야 한다.

추천하는 방식은 트랜스포트의 소켓 핸들을 endpoint의 I/O 큐(pj_ioqueue_t)에 등록하는 것이다. 그러면 endpoint가 I/O 큐를 폴링할때 네트워크 패킷이 트랜스포트 오브젝트에 의해 수신된다.

패킷이 트랜스포트 오브젝트에 한번 수신되면, 반드시 pjsip_tpmgr_recievie_packet() 함수를 호출하여 트랜스포트 매니져에 전달해야 한다. 그러면 패킷은 파싱된 후 나머지 남은 스택에 전달될 것이다. 트랜스포트 오브젝트는 수신데이터 버퍼(pjsip_rx_data)의 tp_info와 pkt_info 멤버를 반드시 초기화 해야한다.

각 트랜스포트 오브젝트는 네트워크로 메시지를 전송하는 함수를 가리키는 포인터를 가지고 있다. (예를 들면 트랜스포트 오브젝트의 send_msg() 멤버). 어플리케이션 (또는 다른 스택)는 pjsip_transport_send()를 호출하므로 네트워크를 통해 메시지를 전달한다. 이 함수의 호출은 결국 트랜스포트 오브젝트의 send_msg()를 호출하게 된다. 패킷 전송이 비동기라면 트랜스포트는 반드시 PJ_EPENDING 상태를 반환하고 메시지 전송이 완료가 되면 인자로 전달받은 콜백을 호출해야한다.

Transport Management
트랜스포트는 pjsip_transport_register()를 호출하여 트랜스포트 매니져에 등록된다. 함수를 호출하기전에 구조체의 모든 정보는 세팅되어야 한다.
트랜스포트의 생명주기는 트랜스포트 매지져에 의해 자동으로 관리된다. 트랜스포트의 레퍼런스카운터가 0이 될때마다 아이들 타이머가 시작된다. 아이들 타이머의 시간이 만료될 때까지 레퍼런스 카운터가 여전히 0이라면 트랜스포트 매니져는 pjsip_transport_unregister()를 호출하여 트랜스포트를 소멸시킨다.
어떤 트랜스포트는 사용되고 있지않아도 계속 살아있어야 하는 경우도 있다(예를들면 싱글톤 인스턴스인 UDP 트랜스포트). 이런경우 트랜스포트가 소멸되는 것을 막기 위해 레퍼런스 카운터를 초기에 1로 설정해서 앞으로 0이 되지 않게 해야한다.

Transport Error Handling
커넥션 리셋이나 패킷전송 실패같은 트랜스포트의 에러는 트랜스포트 유저에 의해 처리된다. 트랜스포트 오브젝트는 이런 에러를 처리하지 않고 단지 에러 리턴을 리포팅한다. 특히 실패하거나 닫혀진 컨넥션에 대해 재접속하려고 시도해서는 안된다.

7. Sending Messages

SIP 어플리케이션의 코어 오퍼레이션은 당연히 메시지 수신과 발신이다. 수신하는 메시지는 on_rx_request()와 각 모듈의 on_rx_response() 콜백에서 처리된다.
이번장에서는 트랜잭션이나 다이얼로그 없이 메시지를 전송하는 기본적인 방법에 대해서 기술한다.
다음장인 트랜잭션은 수신 또는 발신 하는 요청을 statefully로 처리하는 방식에 대해서 기술할 것이다.

7.1 Sending Message Overview

7.1.1 Creating Messages

PJSIP는 요청이나 응답 메시지를 만들기 위해 다양한 API를 제공한다. 다양한 방식은 아래와 같다.
  • 응답메시지를 생성하는 가장 쉬운 방법은 pjsip_endpt_create_response()를 사용하는 것이다.
  • 요청메시지를 생성하기 위해 pjsip_endpt_create_request(), pjsip_endpt_create_request_from_hdr(), pjsip_endpt_create_ack(), pjsip_endpt_create_cancel()를 사용할 수 있다.
  • 프록시는 포워딩하기 위해 수신된 메시지를 기반으로 요청이나 응답메시지를 만들 수 있다. 함수는 pjsip_endpt_create_request_fwd() , pjsip_endpt_create_response_fwd() 이 이용된다.
  • 당신은 전송 버퍼를 만들어서 수동으로 요청이나 응답 메시지를 만들수도 있다. 이때 버퍼생성을 위해 pjsip_endpt_create_tdata()가, 메시지 생성을 위해 pjsip_msg_create()이, 헤더필드를 위해 pjsip_msg_add_hdr() 또는 pjsip_msg_insert_first_hdr() 가 사용될 수 있다. 그리고 메시지 바디등을 설정할 수도 있다. .
  • 상위 레이어 모듈은 메시지를 생성하기 위해 또다른 방법을 제공한다(예: 다이얼로그 레이어). 이것에 관해서는 각 모듈에 관련된 글에서 기술된다.
저레벨 함수인  pjsip_endpt_create_tdata()를 제외한 모든 메시지 생성 API는 전송버퍼(pjsip_tx_data)의 레퍼런스 카운터를 1로 세팅한다. 그러므로 어플리케이션이나 스택은 소멸시키기 위해서 반드시 레퍼런스 카운터를 감소시켜야한다.
모든 메세지 전송 API는 전송버퍼의 레퍼런스 카운트를 감소시킨다. 즉 어플리케이션이 전송버퍼의 레퍼런스 카운트를 감소하기 위해 아무것도 할 필요가 없으며, 버퍼는 메시지가 전송된 뒤 제거가 될것이다.(신C: pjsip_endpt_create_tdata()로 만든경우만 관리하면 되겠넹..)

7.1.2 Sending Messages

메시지를 전송하는 가장 기본적인 방법은 pjsip_endpt_acquire_transport()와 pjsip_transport_send()를 호출하는 것이다. 어쨌든 이 함수 호출을 위해 목적지의 주소를 알아야만 한다. (예: hostname뿐아니라 sockaddr). 이 함수는 너무 저레벨이라서 직접 사용하기에는 실용적이지 않다 왜냐하면 메시지를 만들고 정확한 소켓 주소를 알아내기위해 여러단계를 거쳐야 하기 때문이다.(RFC3263구현으로 어떤 주소가 사용될지 결정된다.)

메시지를 전송하는 코어 API는 pjsip_endpt_send_request_stateless()와 pjsip_endpt_send_response()이다. 이 두 함수는 트랜스포트 레이어를 자동으로 관리해주므로 아주 유용한 함수이다. 그리고 상위 레이어의 모듈(예:트랜잭션)에서 사용되는 기본적인 함수이다.

pjsip_endpt_send_request_stateless() 는 요청 메시지를 전송하기 위한 함수이고 아래와 같은 절차로 수행된다.
  • Request-URI와 Rute 헤더의 파라미터를 통해 연결할 목적지를 결정한다.
  • RFC 3263(Locating SIP Servers)의 절차로 접속할 서버를 알아낸다.
  • 서버와 연결에 사용할 트랜스포트를 선택하고 생성한다.
  • 현재 사용되는 트랜스포트를 나타내기 위해 Via 헤더의 seny-by 를 수정한다.
  • 현재 선택된 트랜스포트를 이용하여 메시지를 전송한다.
  • 현재 트랜스포트를 통해 서버에 접속하지 못하면 다음 서버나 다른 트랜스포트로 대체한다.

pjsip_endpt_send_response()는 응답 메시지를 전송하기 위한 함수이고, 아래와 같은 절차로 수행된다.
  • 어떤 트랜스포트가 사용될지 그리고 어떤 주소로 보낼지 결정하기 위해 RFC 3261의 section 18.2.2의 절차를 따른다.
  • 추가적으로  RFC 3581의 rport 파라미터에 관련된 사항을 따른다.
  • 선택된 트랜스포트를 이용하여 응답을 전송한다.
  • 선택된 트랜스포트로 전송을 실패하면 RFC 3263을 따라 어떤 서버인지 확인하고 다음 주소로 시도한다.
메시지가 비동기로 전송될 수도 있으므로 이 함수들은 전송상태를 알리기 위한 콜백함수를 제공한다. 이 콜백은 실패할 경우 다른 시도를 하면 이를 통지해주고 어플리케이션이 다른 행동으로 대체할 수 있는 기회를 제공한다.

8. Transactions

8.1.1 트랜잭션이란

트랜잭션은 pjsip_transaction 구조체로 표현된다. 생명주기는 아래와 같다.
(ShinC 간단히 말하면  메소드 타입등 상위레벨의 개념과는 상관없이 UAC와 UAS의 하나의 요청 및 응답에 대한  연결 및 데이터 전송을 의미하는 것 같다. 예: INVITE요청시 트랙잭션생성, INVITE에 대한 ACK전송시 트랜잭션 소멸)
  1. pjsip_tsx_create_uas 또는 pjsip_tsx_create_uac 함수 호출로 트랙잭션이 생성된다.
  2. UAS transaction 초기화후, 어플리케이션은 pjsip_tsx_recv_msg(transaction user, rdata, transaction) 을 호출
    - rdata의 트랜젝션 모듈 데이터에 transaction 주소를 세팅한다..
    - 트랜잭션 상태는 NUUL에서 TRYING으로 변경된다.
    - 다음의 재전송 요청은 이 트랜잭션에서 처리된다.
  3. pjsip_tsx_send_msg 를 호출하여 요청이나 응답을 USA로 전송한다.
  4. 메시지가 전달되면 트랜잭션 상태는 자동으로 변경된다. 트잭션유저는 on_tsx_state 콜백을 통해 트랜잭션 상태변경을 통지 받는다.
  5. 트랜잭션 상태가  PJSIP_TSX_STATE_TERMINATED로 변경되면 트랜잭션은 자동적으로 소멸된다. 어플리케이션은 pjsip_tsx_terminate를 호출해서 강제로 소멸시킬수도 있다.

8.1.2 Timers and Retransmissions

트랜잭션은 재전송 타이머와 시간만료 타이머 두 가지 타이머를 가진다. 두 타이머 타입의 값은 트랜잭션에 의해 자동으로 설정된다. 이 설정은 트랜잭션타입(UAS/UAC), transport(reliable or non-reliable), method(INVITE or no-INVITE) 에 따라 결정된다.

어플리케이션은 전역적인 타이머의 인터벌 값을 설정할 수 있다. (아마 컴파일 시간동안만 가능)

트랜잭션은 수신 또는 발신하는 재전송을 처리한다. 수신되는 재정송은 트랜잭션에 의해 무시되거나 조용하게 처리된다. 트랜잭션은 수신되는 재전송에 관련된 통지는 따로 하지 않는다. 발신하는 재전송은 필요한 경우 트랜잭션에 의해 자동으로 재전송된다.이때도 역시 재전송에 대한 통지는 따로 하지 않는다.

8.1.3 INVITE Final Response and ACK Request


  • 실패한 INVITE 요청
Client 트랜잭션: Client 트랜잭션이 INVITE요청에 대해서 마지막 300-699(실패) 응답을 받았을때, 트랜잭션은 자동으로 ACK 요청을 보낸다. 그 다음 트랜잭션은 소멸되기전에 타이머 D 인터벌시간동안 대기한다 대기중에 300-699(실패) 응답의 재전송을 수신하면 ACK 요청을 자동으로 보낸다.

Server 트랜잭션: Server 트랜잭션이 300-699(실패) 마지막 응답  전송요청을 할때, 트랜잭션은 응답을 전송하고 ACK 받거나 타이머 H 인터벌이 만료될때까지 재전송한다. 인터벌동안 ACK 요청을 받으면 트랜잭션은 Confirmed 상태로 변경되고 타이머 I 인터벌이 지난 후 소멸된다. 타이머 H 이 만료될때 까지 ACK요청을 받지 못하면, 트랜잭션은 소멸된다.

  • 성공적인 INVITE 요청
Client 트랜잭션: Client  INVITE 트랜잭션이 2xx(성공)인 마지막 응답을 받으면, 트랜잭션은 트랜잭션 유저(dialog or applicatoin)에 응답을 전달한 후 자신을 소멸시킨다. 다음 수신되는 2xx응답 재전송은 dialog나  어플리케이션에 바로 전달된다.
어떤 경우나, 어플리케이션은 INVITE의 마지막 응답으로 2xx를 받으면 반드시 ACK를 보내야 한다.

Server 트랜잭션:  server INVITE 트랜잭션이 2xx를 마지막 응답으로 보내는 경우, 트랜잭션은 ACK를 받거나 어플리케이션에 의해 트랜잭션이 종료될때까지 응답을 계속 재전송한다.

심플한 구현을 위해, 일반적인 UAS dialog는 트랜잭션에 2xx INVITE 응답의 재전송을 맡긴다. 하지만 프록시 어플리케이션은 2xx 응답을 받은 후 반드시 UAS 트랜잭션을 소멸시켜야한다. 이것은 2xx 재전송이 User Agent 간에 처리되도록 하기위해 필요하다.

INVITE server 트랜잭션의 디폴트 기능은 트랜잭션을 생성한 후 transaction->handle_200resp() 를 널로 설정을 하면 오버라이딩 된다. 이경우 UAS INVITE 트랜잭션은 INVITE에 대한 응답으로 2xx을 보내자마자 소멸될 것이다. (신C: ACK를 확인할 수 없으므로 특별한 경우가 아니라면 설정할 일은 없겠군..)

8.1.4 Incoming ACK Request

server INVITE 트랜잭션이 실패로 완료가 되는 경우, ACK 는 트랜잭션에 의해 처리된다. 트랜젹션 유저는 ACK의 수신을 통지 받지 못하게 된다
server INVITE 트랜잭션이 성공으로 완료가 되는 경우, 첫번째 ACK는 트랜잭션 유저에 통지된다. 하지만 다음에 재전송된 ACK의 수신은 통지되지 않을 것이다.

10.1 Baic Dialog Concept

base UA diallog는 basic dialog state, session counter, Call-ID, From, To,같은 SIP dialog와 dialog usage를 제어하기 위한 기본적인 것들을 제공한다.

기본 UA dialog는 INVITE session, SUBSCRIBE/NOTIFY sessions, REFER/NOTIFY sessions 등 어떤 세션이 사용되더라도 영향을 받지 않는다. 그리고 다른 타입의 세션을 하나의 dialog에 동시에 여러게 생성하는 것도 가능하다.

PJSIP dialog는 일반적인 다이얼로그 속성을 가지는 수동적인 자료구조로 볼 수 있다. 하지만 절대로 INVITE 세션이랑 헤깔리면 안된다요. 다이얼로그에 하나의 INVITE세션만을 가지긴 하지만 INVITE 세션은 다이얼로그 내부의 하나의 세션이다.
다이얼로그는 내부에 어떤 세션이 있는지는 모른다. 오직 활성화된 세션이 몇개인지만 관리한다. 다이얼로그에 세션 개수가 제로가 되고 마지막 트랜잭션이 종료되면, 다이얼로그는 소멸된다.


위 다이어그램은 user agent와 Dialog 그리고 Usage의 관계를 보여준다.  기본적인 저레벨 시나오로 보면, 어플리케이션 모듈 만이 다이얼로그의 usage이다.  좀 더 상위 시나리오에서 보면 상위 레벨의 모듈이(pjsip_invite_usage, pjsip_subscribe_usage) usage로 다이얼로그에 등록할 수도있다. 그리고 어플리케이션은 다이얼로그 대신에 이 usage로 부터 이벤트를 수신할 것이다.
(신C: Dialog를 그대로 해석하면 대화다. UAC<->UAS 간의 전화연결이 Dialog이고 연결도중 수행되는 invite, subscribe, 등 요청의 시작부터 완료까지 모듈의 처리과정을 다이얼로그 세션이라하고 이 세션을 관리하는 모듈을 dialog usage라 고 하는 것 같다.)

10.1.1 Dialog Sessions

다이얼로그 세션은 레퍼런스 카운터로 표현된다. 레퍼런스 카운터는 세션의 생성 소멸에 맞춰 다이얼로그 usage 모듈에 의해 증감된다.  invite usage를 제외하고 다이얼로그 usage는 여러개의 세션을 생성할 수 있다.
세션의 정확한 의미는 다이얼로그 usage 모듈에 의해 정의된다. 앞에서 언급했지만 basic dialog는 다이얼로그에서 현재 활성화된  세션의 개수만 관심이 있다.

10.1.2 Dialog Usages

다이얼로그 usage는 다이얼로그의 이벤트를 수신하기위해 다이얼로그에 등록 되는 PJSIP 모듈이다.  하나의 다이얼로그에 여러 usage가 등록될 수 있고 각 usage는 특정 세션을 관리한다. 예를들어 subscribe usage 모듈은 SUBSCRIBE 요청을 받을 때마다 subscribe session을 생성하고 다이얼로그의 세션 카운터를 증가시킨다.
Dialog usage의 처리과정은 endpoint에 의한 module처리와 비슷하다. on_rx_request 그리고 on_rx_response 이벤트가 발생하면, 다이얼로그는 각 dialog usage의 우선순위에 따라 제로가 아닌값이 리턴될때까지 이벤트를 각 usage module에 전달한다. on_tsx_state 통지는 모든 다이얼로그 usage에 전달되고 각 다이얼로그 usage는 관심있는 트랜잭션 이벤트만을 처리한다.
기본적인 저레벨 API를 사용하는 경우, 어플리케이션은 다이얼로그를 직접 제어한다. 이 경우 어플리케이션이 다이얼로그의 유일한 usage이다. 어플리케이션은 dialog내부의 세션들을 제어해야한다 즉 모든 요청과 응답을 처리하고 세션의 생성과 소멸을 직접관리해야 한다.

나중에 우리는 세션을 제어하는 상위레벨의 API에 관해 배울것이다. 이 상위레벨 API는 dialog에 dialog usage로 등록되는 PJSIP 모듈이다.이 모듈은 각각 다른종류의 메시지를 처리한다. (예를들어 invite usage: INVITE, PRACK, CANCEL, ACK, BYE, UPDATE and INFO, subscribe usage: REFER, SUBSCRIBE, and NOTIFY, ) 이 상위레벨 API는 세션에 따라 상위레벨의 콜백함수를 제공한다.

이번 장에서 우리는 기본적인 낮은 레벨의 dialog usage에 대해서 배울것이다.

10.1.3 Dialog Set

각각의 다이얼로그는 다이얼로그 세트의 멤버이다. 다이얼로그 세트는 일반적인 로컬 tag(예: From tag) 로 구분된다.  일반적으로 다이얼로그 세트는 멤버로 하나의 다이얼로그를 가진다. 여러개를 가질 경우는 외부로 나가는 INVITE를 fork할경우이다. 이 경우 다른 To tag를 가지는 각 수신메시지가 새로운 다이얼로그를 하나의 다이얼로그 세트에 생성할 것이다.

10.1.4 Client Authentication

하나의 다이얼로그는 하나의 클라이언트 인증 세션을 관리한다. basic dialog는 외부로 나가는 요청을 적절한 인증 헤더를 포함해서 초기화 한다. 그리고 dialog usage들이 에 의해 인증이 처리된다. 예를 들면 basic dialog는 401/407(인증에러) 실패에 대해서 자동으로 재시도를 하지 않는다.

10.1.6 Forking

User Agent 모듈은 proxy로 부터 forked response 를 받으면 처리하는 콜백함수를 제공한다. forked response는 2xx 이나 provisional 응답이 될 수 있다. 이 응답의 다이얼로그는 기존에 존재하는 다이얼로그와 다른 To tag를 가진다.
이런 종류의 응답을 받으면, User Agent 모듈은 on_dlg_forked() 콜백을 호출한다. 이 때 파라미터로 받은 응답과 어플리케이션이 만든 원래 다이얼로그를 전달한다.

# SIP forking proxy:
forking proxy는 하나의 SIP 요청을 여러 SIP 주소에 전송하고 요청을 보낸 UA에 각 주소의 대한 응답을 전송한다.

# Multiple registration and Forking with SIP

10.1.7 CSeq

다이얼로그의 cseq는 요청을 보낼때 업데이트 된다. CSeq 헤더가 요청 메시지에 존재한다면, 그 값은 요청이 전송될 때 변경될 수 있다.
다이얼로그의 remote cseq는 요청을 받았을때 업데이트 된다. remote cseq가 없으면, 첫번째 받은 요청의 remote cseq로 세팅될 것이다.

10.1.8 Transactions

다이얼로그는 항상 statefully로 동작한다.  다이얼로그는 요청을 받으면 UAS 트랜잭션을 생성하고, 요청을 보낼때는 UAC 트랜잭션을 생성한다.
다이얼로그가 statelessly로 동작하는 경우는 (statefully가 아닌경우)는 수신된 요청의 CSeq 값이 현재 CSeq보다 작을 경우이다. 이 경우 요청에 대해 500(Internal server Error)로 응답한다.
다이얼로그 API를 통해 UAS 또는 UAC 트랜잭션을 생성했을 경우, User Agent 인스턴스가 트랜잭션 사용자(TU)로 등록된다. 그리고 다이얼로그 인스턴스는 트랜잭션 mod_data의 User agent index에 설정된다. 이벤트 또는 메시지가 수신되면 트랜잭션은 User Agent 모듈에 이벤트를 Uer Agent에 통보한다. 그려면 User Agent는 다이얼로그를 찾아 이벤트를 전달한다. (ShinC: 이벤트 전달 시퀀스:Transaction -> User Agent -> Dialog -> Dialog uage modlue)

11. SDP Offer/Answer Framework

SDP media negotiator를 표현하는 pjmedia_sdp_neg 구조체는 아래의 주요 멤버변수를 가지고 있다.
negotiator는 Invite session의 멤버로 등록된다.
pjmedia_sdp_session
*initial_sdp_tmp: local SDP 정보를 가지고 있고  remote SDP 와 협상할때 매번 사용된다.
*active_local_sdp: remote SDP와 협상이 완료된 local SDP로서 미디어 설정시 이 정보가 이용된다.
*active_remote_sdp: 현재 remote 또는 peer에서 사용되는 SDP.
*neg_local_sdp: 협상할때 내부에서 임시로 사용하는 변수.
*neg_remote_sdp: 협상할때 내부에서 임시로 사용하는 변수.

11.2 SDP Negotiator Session

SDP offer/answer session의 상태변화 다이어그램:
협상세션의 상태는 PJMEDIA_SDP_NEG_STATE_NULL 로 시작한다.

UAC의 경우:
다이얼로그가 로컬 미디어를 준비하고 리모트에 미디어를 전송하려할 때, 다이얼로그는 pjmedia_sdp_neg_create_w_local_offer() 함수를 이용하여 SDP negotiator를 생성한다. 이 함수를 수행하면 로컬 SDP가 설정되고 상태는 PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER로 변경된다. INVITE 요청이 이 SDP가 전송된다. 다이얼로그가 리모트 SDP를 한번 받으면, 반드시 리모트 SDP를 인자로해서 pjmedia_sdp_neg_rx_remote_answer()를 수행해야한다. 이 후 협상함수 pjmedia_sdp_neg_negotiate()를 호출할 수 있다.

UAS의 경우:
다이얼로그가 이미 리모트 SDP를 가지고 있다면, pjmedia_sdp_neg_create_w_remote_offer()를 수행하여 SDP negotiator를  생성해야한다. 이때 함수에 로컬 SDP와 리모트 SDP를 인자로 제공해야한다. 이 후 협상함수pjmedia_sdp_neg_negotiate()를 호출 할 수 있다.

다이얼로그가 SDP를 리모트에 다시 보낸 때
  • 수정없이 같은  Active local SDP를 보낼경우 pjmedia_sdp_neg_tx_local_offer()를 수행햐여 active local SDP를 받고, SDP를 송신하고, 리모트의 응답을 기다릴 수 있다.
  • active local media를 수정하는 경우(예를 들어 스트림 방향, 코덱 활성화 변경,등)는 pjmedia_sdp_neg_get_local()로 active local media를 받은 후 수정하고,  pjmedia_sdp_neg_modify_local_offer()를 수행하여 업데이트한다. 그리고 로컬 SDP를 전송하고 리모트의 응답을 기다린다.
  • 다이얼로그가 로컬미디어의 정보를 거의 바꾼경우(예를 들어 IP address, codec, media 라인, 등)는 initial_sdp를 바꾸게 될것이므로, pjmedia_sdp_neg_reinit_local_offer()를 이용하여 새로운 로컬 SDP를 만들고 다시 협상을 진행한다.
여기서 리모트에 SDP를 전송하고 응답을 대기한다는 것은 pjmedia_sdp_neg_negotiate()의 수행을 의미한다.

12. Dialog Invite Session and Usage

Dialog invite session은 어플리케이션에 의한 상위레벨의 iinvite session 관리수단을 제공한다 (SDP 관리포함). invite session은 basic Dialog를 추상화하기 위해 설계되어 어플리케이션은 invite session API를 사용할때는 basic dialog API를 바로 사용해서는 안된다.

Dialog invite session은 Dialog invite usage에 의해 관리되는데 바로 이것이 PJSIP 모듈이다. Dialog invite usage는 Dialog로 부터 이벤트를 해당 invite session과 forked dialog에 전달해준다.

Dialog invite session과 usage는 pjsip-ua 같은 동적 라이브러리에 따로 구현된다. 어플리케이션은 이 라이브러리를 이용하기 위해는 반드시 <pjsip-ua/sip_inv.h>를 포함해야한다. 아니면 pjsip-ua 라이브러리의 모든 기능을 포함하기 위해 <pjsip-ua.h>를 포함해도 된다.

12.1.1 용어

Dialog invite session 란 하나의 다이얼로그 내에 존재하는 invite session이다. 만약 어플리케이션이 상위 레벨의 invite session 매니지먼트를 이용하려 한다면, 각 다이얼로그 당 단 Dialog  invite session 인스턴스 하나만 생성하면 된다.(ShinC: 위에서 언급한것 처럼 invite usage에 의해  관리된다.)

Dialog invite usage 란 PJSIP endpoint에 등록되는 하나의 PJSIP 모듈이다. dialog invite session이 하나의 다이얼로그를 가지고 있다면, 이 모듈은 특정 다이얼로그에 dialog usage로 등록되어야 한다. 이런 동작은 invite session을 생성할때 자동으로 수행된다.

12.1.2 기능

Dialog invite session은 아래의 기능을 제공한다.
  • Session 진행상태 보고 (예: session progressing, connected, confirmed, disconnected),
  • 자동 권한 제어 (예: 401/407응답 수신에 따른 자동 재요청),
  • SDP offer/answer 제어.
  • 상위레벨의  forking 제어,
  • Session timeout (i.e. Expires header),
  • session timer, and reliable provisionalresponse 과 같은 Session extension.

12.1.3 상태전환

Dialog invite usage는 어플리케이션에 세션의 상태를 통지해주는 콜백함수를 제공한다. 이것은 전화상태와 세션상태가 연동되도록 구현한 전화의 경우 굉장히 유용하게 사용될 수 있다.

12.1.4 Invite Session 생성

전화 발신자와 같이 외부로 나가는 dialog의 경우, 어플리케이션은 pjsip_dlg_create_uac()를 이용해 UAC dialog를 만들어야 한다.  그다음 어플리케이션은 pjsip_inv_create_uac()를 호출해 invite session을 만든다 이때 인자로 UAC dialog instance를 넘겨준다. 어플리케이션은 invite session을 생성하기전에 INVITE 요청을 보내면 안된다. 그렇지 않으면 invite session은 관련된 여러 이벤트를 놓칠수도 있기때문이다.

전화를 수신하는 경우 어플리케이션은 pjsip_inv_verify_request()를 호출하여 처리할 수 있는 요청을 받았는지 확인한다. 수락할 수 없는 요청이라면 거절하는 응답을 만들고, 그렇지 않다면 pjsip_dlg_create_uas()를 호출하여 invite session을 생성한다. UAC와 마찬가지로 invite session을 생성하기 전에 응답을 보내면 안된다.

전화를 발신하는  복사된(forked) dialog의 경우, 원래 dialog에 invite session이 있으면 invite usage는 복사된 dialog의 invite session을 자동으로 만들어 준다. 어플리케이션은 콜백함수를 통해 invite session이 만들어 짐을 통보받게 된다.

invite session생성함수를 통해 invite session을 만들면 자동으로 invite usage가 dialog에 등록되므로 일부러 등록하기 위해 pjsip_dlg_add_usage()를 호출할 필요가 없다.

12.1.5 Messages Handling

invite session은 invite session의 상태를 변화시키는 모든 SIP 메소드 처리한다. 현재의 버젼에서 invite session은 INVITE, BYE, ACK, CANCEL, UPDATE, PRACK 메소드를 처리한다.

어플리케이션은 위에서 언급한 메소드의 요청이나 응답을 생성하거나 전송할 때 반드시 invite session API를 사용해야한다. 그래야만 메시지가 제대로 처리되는 것이 보장되고 지원되는 올바른 기능이 사용되어 reliable provisional response 같은 것이 처리된다.

위를 따라야 하지만 여전히 어플리케이션은 기본적인 dialog API를 사용하는 것이 가능하다. 예를들면, 기본적인 dialog API로 dialog 내에서 메시지를 생성하고 전송할 수 있다.

12.1.6 Extending the Dialog

이전에 이야기한것 처럼, invite session은 dialog에서 발생하는 INVITE, BYE, ACK, CANCEL, UPDATE, PRACK 메시지를 처리한다. 어플리케이션이 다른 타입의 메시지를 처리하려면 반드시 dialog usage로 등록해야한다. 이렇게 하여 어플리케이션이 기존의 dialog usage로 처리되지 않은 메시지를 처리할 수 있게 된다.(subscribe usage 참고)
어플리케이션 모듈의 우선순위를 정확하게 설정하는 것은 매우 중요하다. 어플리케이션의 우선순위는 반드시 PJSIP_MOD_PRIORITY_APPLICATION  그리고 invite usage는 (PJSIP_MOD_PRIORITY_APPLICATION-1)이어야 한다. 그래야만 invite usage가 어플리케이션보다 먼저 메시지를 처리되는 것이 보장된다.

12.1.7 Extending the Invite Session

앞으로, invite session은 콜 전환, dialog targetting과 같은 더 많은 확장 SIP를 지원할지도 모른다. 현재는 이런 기능을 위해 어플리케이션이 따로 구현해야 한다.

댓글 없음:

댓글 쓰기