본문 바로가기

MS/P2P

샘플 코드 (Client)

출처 : http://purematter.blog.me/110102481154

샘플 소스코드의 sample-p2p-clinet 프로젝트는 NAT Traversal을 테스트하기 위해 작성된 클라이언트 프로그램입니다. 테스트하려는 내용은 P2P 클라이언트가 서버의 도움없이 원격 피어와 간단한 패킷을 주고 받을 수 있는가입니다. 테스트 클라이언트는 p2pnt 라이브러리가 제공하는 NAT Traversal 기능을 사용하여 테스트를 수행합니다. sample-p2p-client는 결국 p2pnt 라이브러리를 위한 테스트 프로그램이라고 말할 수 있습니다.

피어와 통신하기 위해서 P2P클라이언트는 해당 피어가 제공하는 세션디스크립터를 알아야 합니다. 그리고 그 세션디스크립터를 p2pnt에 제공해야 합니다. 세션디스크립터에는 연결을 시도하기 위해 필요로하는 주소정보들이 포함되어 있습니다. 이 정보는 랑데부서버로부터 알아낼 수 있습니다. 랑데부 서버와의 통신을 위해서 sample-p2p-client rdvmsg 라이브리리를 사용합니다. rdvmsg 라이브러리는 랑데부 메시지를 만들고 해석하기 위한 메시지 라이브러리이며, 네트워크 라이브러리는 아닙니다. 서버와의 랑데부메시지 송수신은 ENet 라이브러리를 사용합니다.

프로그램의 구성

테스트 클라이언트는 아래의 요소들로 구성되어 있습니다.

ENet (랑데부서버와의 통신을 위한 UDP 네트워크 라이브러리)

p2pnt (원격호스트와의 통신을 위한 P2P 네트워크 라이브러리)

rdvmsg (랑데부서버와의 통신에 사용되는 메시지 라이브러리)

ENet은 랑데부서버와의 통신을 위해 사용하는 UDP Network 라이브러리입니다. p2pnt는 원격호스트와의 P2P통신을 위해 사용하는 라이브러리입니다. rdvmsg는 랑데부서버와의 통신프로토콜로 사용되는 메시지 라이브러리입니다.

작동방식

아래는 p2pnt를 중심으로 sample-p2p-client가 작동하는 방식을 나타낸 커뮤니케이션 다이어그램입니다.

1. 프로그램이 시작되면, 클라이언트는 먼저 p2p_init()를 호출하여 p2pnt 라이브러리를 초기화합니다.

2. 라이브러리가 초기화되면 p2p_manager_create()를 호출하여 매니저 오브젝트를 생성합니다. 클라이언트는 이때, P2P 통신에 사용될 가능성이 있는 모든 주소를 수집합니다. 수집된 주소는 후보(address candidates)라고 합니다. 주소수집과정은 비동기적으로 처리됩니다.

3. p2p_manager_create()의 결과는 on_create()를 통해 어플리케이션으로 콜백됩니다.

4. P2P통신준비가 완료되면 랑데부서버로 로그인 합니다. 로그인 메시지는 rdvmsg 라이브러리를 사용해서 만듭니다. 클라이언트는 로그인메시지에 자신의 세션디스크립터를 포함시켜 함께 전달해야 합니다. 세션디스크립터는 p2pntp2p_manager_get_local_sd ()를 호출하여 얻습니다.

5. 랑데부서버로부터 로그인응답이 도착합니다.

6. 랑데부서버는 다른 클라이언트의 세션정보를 전달합니다.

7. 클라이언트는 다른 클라이언트의 세션정보를 바탕으로 P2P연결을 시도합니다.

8. P2P연결의 결과는 on_ice_complete() 를 통해 p2pnt로 부터 콜백됩니다.

9. p2p_manager_sendto() 함수를 통해 원격클라이언트로 패킷을 전송합니다.

클라이언트는 ENet을 사용하여 랑데부서버에 로그인/로그아웃 기능을 구현하고, p2pnt를 사용하여 원격클라이언트와 P2P통신을 합니다.

아래는 위의 커뮤니케이션 다이어그램을 시퀀스 다이어그램으로 표현한 것입니다.

커뮤니케이션다이어그램과 마찬가지로, 이 시퀀스다이어그램은 클라이언트가 P2P라이브러리를 초기화하고 랑데부서버와 통신하면서 궁극적으로는 p2p_manager_make_session()을 호출하여 원격호스트와 세션을 만드는 과정을 보여주고 있습니다.

소스코드분석

아래는 sample-p2p-client main()입니다.

int main (int argc, char *argv[])
{

int rc = 0;
if (P2P_SUCCESS != init_p2p_subsystem(argc, argv)) {

fprintf (stderr, "init_p2p_subsystem() error!\n");

return EXIT_FAILURE;

}

if (P2P_SUCCESS != create_p2p_subsystem()) {

fprintf (stderr, "create_p2p_subsystem() error!\n");

return EXIT_FAILURE;

}

rc = run_event_loop ();

destroy_p2p_subsystem();

return rc;

}

아래는 위의 소스코드를 활동 다이어그램으로 나타내는 것입니다.

init_p2p_subsystem()함수에서는 P2P 라이브러리를 초기화하고 커맨드라인 인자를 분해합니다. 클라이언트 프로그램에서 사용 가능한 커맨드라인 인자는 아래와 같습니다.

-c : 최대 PEER

-s : STUN 서버주소

-t : TURN 서버주소

-r : RTT 체크 여부

) sample-p2p-client.exe -r -c 2 -s 127.0.0.1:34780 -t 127.0.0.1:34780 127.0.0.1:51014

create_p2p_subsystem ()은 내부적으로 p2p_manager_create()를 호출하여 P2P 매니저를 생성합니다.

run_event_loop()함수는 랑데부서버와의 통신을 위한 I/O 루프를 실행합니다. ENet을 사용하므로 단순히 enet_host_service()함수를 호출합니다.

destroy_p2p_subsystem()p2p_manager_destroy()p2p_shutdown()을 호출합니다.

run_enet()을 살펴보겠습니다.

int run_enet ()
{

int rc;

ENetEvent event;

enet_uint32 timeout;

if (P2P_FALSE == init_enet ()) {

fprintf (stderr, "enet error!\n");

exit( EXIT_FAILURE);

}

timeout = 100;

while ((rc = enet_host_service (tester.client, &event, timeout)) >= 0)

{

switch (event.type)
{

case ENET_EVENT_TYPE_CONNECT:

event.peer->data = &tester;

handle_connect (&event);

break;

case ENET_EVENT_TYPE_RECEIVE:

handle_receive (&event);

enet_packet_destroy (event.packet);

break;

case ENET_EVENT_TYPE_DISCONNECT:

handle_disconnect (&event);

event.peer->data = 0;

}

}

enet_host_destroy(tester.client);

return rc;

}

클라이언트의 패킷 디스패칭은 이곳에서 수행되는 것을 알 수 있습니다. 패킷이 도착하거나 타임아웃 이벤트가 발생되면 enet_host_service()는 리턴됩니다.

처리하는 이벤트는 다음의 3가지가 있습니다.

ENET_EVENT_TYPE_CONNECT

서버로의 연결(connect())이 성공하면 발생하는 이벤트입니다.

handle_connect()가 이 이벤트를 처리합니다.

ENET_EVENT_TYPE_RECEIVE

패킷이 수신되면 enet이 생성하는 이벤트입니다.

handle_receive()가 이 이벤트를 처리합니다.

ENET_EVENT_TYPE_DISCONNECT

서버와의 연결이 끊어지면 enet이 생성하는 이벤트입니다.

handle_disconnect()가 이 이벤트를 처리합니다.

위의 3가지 이벤트만 처리하면 네트워크 프로그램을 만들 수 있습니다.

Connection Event 처리

클라이언트가 랑데부서버로의 연결에 성공하면 ENet은 연결완료이벤트를 생성합니다. 아래는 샘플클라이언트의 연결 이벤트핸들러입니다.

void handle_connect (ENetEvent *event)

{

int rc;

printf ("Connected!\n");

rc = login_to_rendezvous_server (tester.client, event->peer);

assert (rc == 0);

}

handle_connect()는 랑데부서버로 로그인을 시도합니다. login_to_rendezvous_server() 함수가 로그인을 처리합니다.

int login_to_rendezvous_server (ENetHost *host, ENetPeer *peer)

{

ENetPacket *packet;

rdv_msg *msg;

p2p_uint8_t *pkt;

int max_pkt_len;

size_t pkt_size;

p2p_uint8_t *sd;

unsigned sd_length;

int status;

status = rdv_msg_create (RDV_LOGIN_REQUEST, &msg);

assert (status == 0);

status = rdv_msg_add_string_attr (msg, RDV_ATTR_ID, TEST_ID);

assert (status == 0);

//get sd

sd_length = p2p_manager_get_local_sd (&sd);

assert (sd && sd_length);

status = rdv_msg_add_binary_attr (msg, RDV_ATTR_SD, sd, sd_length);

assert (status == 0);

max_pkt_len = 1500;

pkt = (p2p_uint8_t *) malloc (max_pkt_len);

assert (pkt);

//encoding

status = rdv_msg_encode (msg, pkt, max_pkt_len, &pkt_size);

assert (status == 0);

printf ("Request Login... (send packet size=%d)\n", pkt_size);

packet = enet_packet_create (pkt, pkt_size, ENET_PACKET_FLAG_RELIABLE);

enet_peer_send (peer, 0, packet);

enet_host_flush (host);

rdv_msg_destroy (msg);

free (pkt);

return 0;

}

ENet으로 패킷을 보내는 것은 중요하지 않습니다. rdv_msg를 만드는 것이 중요합니다. rdv_msgRDV_LOGIN_REQUEST 타입을 지정합니다. 그리고 자신의 ID를 전송하기 위해 RDV_ATTR_ID 타입의 속성을 rdv_msg에 추가합니다. ID는 문자열이므로 문자열속성(rdv_string_attr)으로 생성합니다. 속성값은 TEST_ID로 초기화합니다. ID와 함께 세션디스크립터도 전송해야 합니다. RDV_ATTR_SD 타입의 속성을 rdv_msg에 추가합니다. 세션디스크립터는 인코딩된 바이너리이므로 바이너리속성(rdv_binary_attr)으로 생성합니다. 세션디스크립터는 p2p_manager_get_local_sd()를 호출하여 얻을 수 있습니다.

네트워크로 전송하기 위해 최종적으로 rdv_msg를 인코딩합니다.

ENet 패킷으로 포장하여 전송합니다.

이런 과정을 거쳐 ENet의 연결 이벤트가 처리됩니다.

Receive Event 처리

클라이언트로 패킷이 도착하면 ENet은 패킷 수신 이벤트를 생성합니다. 아래는 클라이언트의 수신 이벤트 핸들러입니다.

void handle_receive (ENetEvent *event)

{

int rc;

rdv_msg msg;

size_t parsed_len;

rc = rdv_msg_decode (event->packet->data,event->packet->dataLength, &msg, &parsed_len, 0);

assert (rc == 0);

if (RDV_LOGIN_RESPONSE == msg.hdr.type) // 로그인응답

{

rdv_uint_attr *uint_attr;

uint_attr = (rdv_uint_attr *)rdv_msg_find_attr (&msg, RDV_ATTR_USN, 0);

assert (uint_attr);

printf ("Login ok (my usn=%d)\n", uint_attr->value);

}

else if (RDV_NEWUSER == msg.hdr.type) //새 유저에 대한 정보

{

rdv_uint_attr *uint_attr;

rdv_binary_attr *binary_attr;

uint_attr = (rdv_uint_attr *)rdv_msg_find_attr (&msg, RDV_ATTR_USN, 0);

assert (uint_attr);

binary_attr = (rdv_binary_attr *)rdv_msg_find_attr (&msg, RDV_ATTR_SD, 0);

assert (0 != binary_attr);

// 세션 연결 시도

rc = p2p_manager_make_session (uint_attr->value, binary_attr->data, binary_attr->length);

assert (rc == 0);

}

}

샘플 랑데부 클라이언트는 2가지 타입의 메시지만 처리하면 되는 간단한 프로그램입니다. 하나는 로그인 응답(RDV_LOGIN_RESPONSE) 메시지이고, 나머지 하나는 다른 클라이언트의 세션정보(RDV_NEWUSER)에 대한 메시지입니다. RDV_LOGIN_RESPONSE를 수신한 클라이언트는 응답을 확인만 할 뿐 특별한 작업을 따로 수행하지는 않습니다. RDV_NEWUSER 메시지는 다른 유저(클라이언트)에 대한 정보 메시지입니다. 서버는 RDV_NEWUSER 타입의 메시지를 전송하여 다른 클라이언트에 대한 정보를 전달합니다. 이 메시지에는 USN과 세션디스크립터가 포함되어 있습니다. 모든 클라이언트는 바로 RDV_NEWUSER 타입의 메시지를 통해 서로를 알게 됩니다.

클라이언트는 RDV_NEWUSER 메시지를 통해 알게 된 정보를 바탕으로 P2P연결을 시도할 수 있습니다. 중요한 것은 P2P세션이 성립되려면 양측이 모두 서로에게 P2P세션수립을 요청해야 한다는 것입니다. 이점은 홀펀칭의 작동방식과 같습니다. sample-p2p-client에서는 RDV_NEWUSER 메시지를 수신하면 즉시 P2P세션연결을 시도하도록 작성되어 있습니다. sample-p2p-server는 미리 정의된 수만큼의 유저가 로그인하면 그 유저들을 하나의 그룹으로 묶습니다. 이를 "세션그룹"이라고 합니다. 그리고 세션그룹의 모든 유저에게 RDV_NEWUSER 메시지를 전송합니다. N명의 유저가 로그인 했다고 가정한다면 각 클라이언트는 N-1개의 RDV_NEWUSER 메시지를 수신하게 됩니다.

세션그룹이 형성되고 모든 클라이언트가 P2P통신을 하고 있는 도중에, 새로운 유저가 로그인하게 되면 어떻게 될까요?

이 또한 프로그래머가 결정하기에 달렸습니다. 세션그룹에 대한 정보는 랑데부서버가 관리합니다. 기존의 클라이언트들이 구성하고 있는 세션그룹에 새로운 클라이언트가 합류하려면 서버 측에서 처리 해주면 됩니다. 서버는 새로운 클라이언트의 정보를 세션그룹에 추가한 후, 새로운 클라이언트에게 세션그룹에 포함된 모든 클라이언트의 정보를 전송합니다. 물론 이 메시지의 전송은 RDV_NEWUSER 타입으로 합니다.

Disconnection 처리

sample-p2p-clientsample-rendezvous-server와 연결이 끊어지는 것에 대해 특별한 처리를 하지 않습니다. handle_disconnect()는 아무 일도 하지 않도록 구현되었습니다.

샘플클라이언트는 명시적으로 프로그램을 종료하는 인터페이스를 제공하지 않습니다. ENet TCP와 같은 신뢰성을 제공하기 때문에, 클라이언트가 강제로 종료하게 되면 서버에서 알고 적절한 처리를 할 수 있습니다.




'MS > P2P' 카테고리의 다른 글

TURN - Server (Relay server)  (0) 2012.11.07
PJNATH - TURN 전송 모듈  (0) 2012.11.07
PJNATH - TURN 세션 모듈  (0) 2012.11.07
PJLib - APIs  (0) 2012.11.07
P2P network library project  (0) 2012.11.07