그래픽 & 멀티미디어
이번 시간에는 IPC 방법 중에 제가 제일 애용하는 UDS에 대해 알아 보도록 하겠습니다. UDS에 대해 처음 접하시는 분은 프로세스들 끼리의 통신 UDS를 먼저 보아 주시기 바랍니다.
이미 QSocket으로 TCP/IP를 통신해 보았고, 자료를 수신했을 때에는 SIGNAL을 이용한 이벤트 처리가 가능했습니다. 저는 이와 같은 방법을 UDS로 이용하고 싶어서 자료를 찾아 보았고, 나름 노력을 했습니다만 실력의 한계로 결국 만들지 못하고, 방법을 바꾸어서 쓰레드를 이용하는 방법을 먼저 확인하기로 했습니다.
쓰레드를 이용하는 이유는 단 한가지 입니다. QSocket에서는 자료가 수신되었을 때, SIGNAL로 알려 주시면 UDS를 그냥 사용했을 때에는 언제 보내 줄지 모르는 자료를 수신하기 위해 read()하면 상대방이 송신할 때까지는 애플리케이션이 block되어 버리기 때문입니다.
쓰레드에 대한 저의 생각
쓰레드에 대한 저의 견해는, 물론 지극히 저의 개인적인 생각입니다만, 쓰레드를 좋아하지 않습니다. 물론 pork()로 프로세스를 나누는 것보다는 프로그램이 간결할 수 있겠습니다만 작업의 안정성을 보장 받지 못한다는 점에서 저는 가급적 사용을 피합니다.
"그거야 네가 실력이 딸리니까 그렇지!!"라고 말씀하신다해도 드릴 말씀이 없는 것이, 정말 실력이 딸려서 쓰레드를 제대로 사용하지 못한다면, 그 점을 인정하고 다른 안정된 방법을 사용해야된다고 생각됩니다.
간단한 쓰레드야 당연히 사용하죠. 그러나 다른 쓰레드나 애플리케이션과 함께 사용하는 공유 자원을 빈번히 사용해야 한다면 정말 권하고 싶지 않습니다. 일반 프로그램에서 이상이 없는 코드도 쓰레드 안에서는 예상치 못하는 문제가 되기 때문입니다.
가장 대표적인 것이, 쓰레드 내에서 직접 폼의 위젯들을 함부로 건드려서는 안됩니다. 위젯은 말 그대로 길가의 고깃덩어리이기 때문에 쓰레드 혼자만 사용하는 것이 아니라 다른 여러 쓰레드가 건드릴 수 있기 때문에 그냥 사용하면 매우 위험합니다. 대신에 그 위젯을 가지고 있는 폼에게 부탁을 해야 하는데, 그냥 부탁하는 것이 아니라 정중하게 부탁을 해야 합니다.
즉, 폼에 만들어진 함수를 호출해서 처리하면 안 됩니다. 그렇게 하려면 차라리 위젯을 그대로 사용하는게 낫죠. 대신에 이렇게 처리해 주십시오하고 신청서를 작성해서 메시지 큐에 올려 주어야 합니다.
더 자세한 말씀 대신에 QCustomEvent를 사용해야 된다는 말씀으로 축약하겠습니다. 예제에 다시 나오므로 그 때 보시면 아시겠습니다만, 쓰레드가 처리하는 것이 아니라 폼에게 처리해 달라는 부탁을 하고, 자신은 원래 작업을 계속( postEvent() ) 하거나 폼이 작업이 다 끝날 때 까지 대기( sendEvent() )할 수 있습니다.
간략히 다시 말씀드린다면 Microsoft Windows의 Visual C에서 메시지 큐에 메시를 올리는 것을 생각하시면 되겠습니다. 물론 공유 자원을 사용할 필요없이 자기만의 데이터 영역으로 작업한다면 이런 골치아픈 일은 없겠죠. ^^
시그널과 슬롯
흠~ 왜 메시지 큐를 이용하나? 차라리 QThread 에서 시그널을 생성하고, 폼에서 슬롯을 만들어서 사용하면 편리하지 않을까?라고 말씀하시는 분이 계실지 모르겠습니다. 사실 저도 이 방법을 사용하고 싶어서 프로그램을 작성해 보았습니다만 아쉽게도 시그널을 만들기 위해서는 QWidget을 상속받아야 하는데, QT 3.x.x 이전 버전은 QThread가 QT에서 상속 받았기 때문에 시그널을 생성할 수 없습니다.
대신에 QT 4.x.x 버전부터는 QThread도 QWidget으로부터 상속을 받았기 때문에 시그널을 생성할 수 있기 때문에 가능할 것으로 생각됩니다만, 저는 QT 3.x.x를 사용하고 있어서 확인하지는 못했습니다.
프로그램 코딩 시작
이번에 만들 UDS 프로그램은 첫번째 예제 답게 상대편으로부터 전송되어온 문자열을 반송하는 에코 프로그램입니다. 수신한 문자열은 QLabel을 통해 출력하겠습니다.
우선 프로젝트 파일부터 메인폼까지 먼저 만들어야 겠지요. ^^
- 프로제트 파일 생성, "C++ Project"를 선택합니다.
- 적당한 프로젝트 이름을 입력합니다.
- 프로젝트 파일을 저장합니다.
- 폼 이름을 지정합니다. 이 이름이 클래스 이름이 되므로 적당한 이름을 지정해 줍니다. 여기서는 frmMain으로 입력하겠습니다.
- 필요한 위젯을 배치합니다. QLabel과 버튼입니다.
- 필요 함수를 먼저 등록하겠습니다.init()함수는 폼이 윈도우로 생성한 후에 발생하는 함수로 여러번 소개된 함수라서 설명을 생략하겠습니다.
쓰레드에서 자료가 수신되면 폼에 QLabel에 직접 자료를 출력하는 대신에,
데이터를 폼에게 전달하면서 출력을 부탁합니다.
이 부탁을 받은 함수가 customEvent() 입니다.
당연히 init()처럼 함수이름은 이미 고정된 이름입니다.
- File >> New 를 이용하여 헤더파일 하나를 생성합니다.
- 헤더파일은 thrUDS.h 로 저장하겠습니다. 일단 소스를 보시겠습니다.
#ifndef __THR_UDS_H__ #define __THR_UDS_H__ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/un.h> #include <qapplication.h> #include <qthread.h> #include <qevent.h> #define BUFF_SIZE 1024 #define SOCK_LOCAL "/tmp/uds_a" class udsReceiveEvent: public QCustomEvent { private : char buff[BUFF_SIZE+5]; public : udsReceiveEvent() : QCustomEvent( 9527) { } char *readData( void) { return buff; } void writeData( char *data) { strcpy( buff, data); } }; class TthrUDS : public QThread { private: int sock; QObject *owner; public: TthrUDS( QObject *parent=0) { owner = parent; } void run() { struct sockaddr_un addrLocal; struct sockaddr_un addrGuest; char buffReceive[BUFF_SIZE+5]; size_t szAddr; udsReceiveEvent *eveReceive = new udsReceiveEvent(); if ( 0 == access( SOCK_LOCAL, F_OK)) unlink( SOCK_LOCAL); sock = socket( PF_FILE, SOCK_DGRAM, 0); if( -1 == sock) { eveReceive->writeData( "socket error!!"); QApplication::postEvent( owner, eveReceive); return; } memset( &addrLocal, 0, sizeof( addrLocal)); addrLocal.sun_family = AF_UNIX; strcpy( addrLocal.sun_path, SOCK_LOCAL); if( -1 == bind( sock, (struct sockaddr*)&addrLocal, sizeof( addrLocal)) ) { eveReceive->writeData( "bind error!!"); QApplication::postEvent( owner, eveReceive); return; } eveReceive->writeData( "start!!"); QApplication::sendEvent( owner, eveReceive); eveReceive->writeData( "start!!"); QApplication::sendEvent( owner, eveReceive); while(1) { szAddr = sizeof( addrGuest); memset( buffReceive, 0, BUFF_SIZE); recvfrom( sock, buffReceive, BUFF_SIZE, 0 , ( struct sockaddr*)&addrGuest, &szAddr); sendto( sock, buffReceive, strlen( buffReceive)+1, 0, ( struct sockaddr*)&addrGuest, sizeof( addrGuest)); // +1: NULL까지 포함해서 전송 eveReceive->writeData( buffReceive); QApplication::sendEvent( owner, eveReceive); } } }; #endif
프로그래 내용을 자세히 보겠습니다.
#define BUFF_SIZE 1024 #define SOCK_LOCAL "/tmp/uds_a"
BUFF_SIZE는 읽기나 쓰기를 위한 버퍼의 크기입니다. SOCK_LOCAL은 UDS에서 사용할 파일의 이름으로 이 파일 이름이 마치 TCP/IP나 UDP/IP에서 LAN카를 구별하기 위한 IP와 같이 통신 대상의 주소가 되겠습니다.
class udsReceiveEvent: public QCustomEvent { public : udsReceiveEvent() : QCustomEvent( 9527) };
쓰레드에서 메인 폼에 메시지를 전송하기 위한 QCustomEvent입니다. 하나의 프로그램에 여러 QCustomEvent를 생성할 수 있으므로 구별하기 위해서라도 초기값을 지정해 줍니다. 초기값은 1000 이상에서 65535 사이 값을 지정합니다.
이제, 이 메시지를 처리하는 메인폼의 customEvent()내용을 보겠습니다.
void frmMain::customEvent( QCustomEvent *eve ) { if ( 9527 == eve->type()) { udsReceiveEvent *ceve = ( udsReceiveEvent *)eve; labReceive->setText( ceve->readData()); } }
인수로 받은 eve의 type()값으로, 전송하는 사용자 이벤트를 구분할 수 있습니다.
class TthrUDS : public QThread { private: int sock; QObject *owner; public: TthrUDS( QObject *parent=0) { owner = parent; } void run() { : { };
TthrUDS를 생성할 때 인수로 받은 QObject로 owner값을 지정해 줍니다. 이 owner값으로 앞서 QCustomEvent를 전송합니다. 그럼으로써 정확히 이벤트 메시지를 전송하는 목적지를 정확히 지정할 수 있습니다.
class TthrUDS : public QThread { : void run() { : 이 부분은 UDS에서 설명한 내용이라 생략합니다. while(1) { szAddr = sizeof( addrGuest); memset( buffReceive, 0, BUFF_SIZE); recvfrom( sock, buffReceive, BUFF_SIZE, 0 , // 상대방으로부터 값을 수신 ( struct sockaddr*)&addrGuest, &szAddr); sendto( sock, buffReceive, strlen( buffReceive)+1, 0, ( struct sockaddr*)&addrGuest, sizeof( addrGuest)); // 수신 받은 것을 다시 전송 eveReceive->writeData( buffReceive); QApplication::sendEvent( owner, eveReceive); } } };
위의 내용 중에 UDS통신 부분은 "프로세스 끼리의 통신 UDS"에서 UDS 프로그램 A 에 있는 내용과 같습니다.
수신된 내용을 다시 정송하고 수신된 내용을 QCustomEvent에 적용한 후 owner에 지정된 객체로 Event 메시지를 전송합니다.
frmMain의 전체 소스를 보겠습니다.
/**************************************************************************** ** ui.h extension file, included from the uic-generated form implementation. *****************************************************************************/ #include <qapplication.h> #include "thrUds.h" TthrUDS *thrUDS; void frmMain::customEvent( QCustomEvent *eve ) { if ( 9527 == eve->type()) { udsReceiveEvent *ceve = ( udsReceiveEvent *)eve; labReceive->setText( ceve->readData()); } } void frmMain::init() { thrUDS = new TthrUDS( this); connect( btnClose, SIGNAL( clicked()), qApp, SLOT( quit())); thrUDS->start(); }
main.cpp 생성
프로젝트를 빌드하기 위해서 main.cpp를 생성합니다. File >> New메뉴를 선택합니다. main.cpp를 선택합니다.
frmMain을 선택하고 프로젝트를 빌드합니다.
]# qmake ]# make ]# ./uds -> 프로젝트 파일 이름
실행하면 프로그램이 실행됩니다.
이번에는 /tmp/uds_a로 UDS소켓을 사용하는 프로그램으로 문자열을 전송하여 보십시요. 예로, "프로세스 끼리의 통신 UDS"에서 UDS 프로그램 B를 사용하시면 되겠습니다.
]$ ./a.out forum.falinux.com forum.falinux.com ]$
이와 같이 에코가 실행되면서 프로그램의 출력은 아래와 같이 바뀔 것 입니다.