QT가 GUI를 위한 프로그래밍 툴킷이라고만 말하기에는 너무 아쉽네요. 왜냐? QT를 이용하면 소켓 프로그램을 매우 간단하게, 그리고 간편하게 작성할 수 있기 때문입니다. 오늘은 얼마나 간단하고 간편하게 소켓 프로그램을 작성할 수 있는지 보여 드리도록 하겠습니다.

주의 사항 한가지 아쉬운 것은 제가 PC에서 학습을 하면 반드시 임베디드로 옮겨서 확인해 봅니다. 그런데 제가 뭘 잘못해서 인지는 모르겠습니다만, 상대방으로부터 수신한 자료를 화면에 출력하기 위해 TextEdit를 사용했는데, X11용으로 컴파일하고 실행하면 이상이 없지만, Arm 보드용으로 컴파일하고, Arm 보드에서 실행하면 실행이 안되고 block되는 모습이 되네요.

아마도 TextEdit가 메모리를 많이 소모하기 대문으로 생각됩니다만 어디까지나 저의 개인적인 생각일 뿐입니다. 이런 이유로 TextEdit 대신에 QLabel로 수신자료를 출력했습니다.

첫번째로 보실 내용은 TCP/IP를 이용한 클라이언트 프로그램입니다. 소켓 프로그램이 다 그렇듯이 아래의 함수가 준비되어야 겠습니다.

  • 서버와 연결하는 함수
  • 서버와 연결을 끊는 함수
  • 서버와 연결이 되었음을 확인해 주는 함수
  • 서버로부터 연결이 끊겼음을 확인해 주는 함수
  • 서버와 연결이 끊어 졌음을 확인해 주는 함수
  • 통신 중에 에러가 발생했으면 알려주는 함수
  • 서버로 자료를 전송해 주는 함수
  • 서버로부터 수신한 자료를 처리하는 함수

QT에서는 소켓프로그램을 위해 QSocket 객체를 제공해 줍니다. 앞으로 QSocket을 더 자세히 공부하고 싶습니다. 혹시 이 QSocket으로 UDP/IP나 UDS로 이용하신 분은 꼭 저에게 간단하게라도 사용방법을 알려 주시면 감사하겠습니다.

QSocket의 시그널

소켓 프로그램뿐만 아니라 시리얼통신도 마찬가지고 상대방이 언제 자료를 던져 줄지 모르기 때문에, 얼마나 원할하게 대기하느냐가 중요합니다. 자기도 할일 있는데 무작정 상대편만 보고 있을 수 만은 없습니다. 효율적으로 자기의 본래 일을 하면서, 상대편이 자료를 전송해 주면 바로는 아니더라도 가급적 빠르게 처리해 줄 수 있도록 작성되어야 겠습니다.

그러나 어떻게? 일반 C 프로그램이라면 POLL을 사용하면 되지만 QT 프로그램에서는 이미 메시지 루프가 qApp.exec(); 로 묶여 있습니다. 메인 함수를 한번 보시죠.

#include <qapplication.h>
#include "frmMain.h"

int main( int argc, char ** argv )
{
    QApplication a( argc, argv );
    frmMain w;
    w.show();
    a.connect( &a, SIGNAL( lastWindowClosed() ), &a, SLOT( quit() ) );
    return a.exec();
}

일반 C 프로그램이라면 main() 함수 안에 while( true) 문을 두고 그 안에서 POLL을 운영할 수 있지만, QT에서는 a.exec(); 로 메시지 루프를 묶어 놓았기 때문에 나의 루프를 추가할 수 없습니다.

이렇기 때문에 QSocket 객체가 반갑고, QSocket의 시그널이 처리가 반갑습니다. POLL을 운영하는 이유가 상대방이 언제 줄지 모르는 자료를 감시하기 위해 계속 호출되어져야 한다면, QSocket의 시그널은 수신 자료 외에 프로그램에서 체크해야될 각종 이벤트를 그때그때 알려 주기 때문에, 굳이 프로그래머가 POLL()을 운영할 필요가 없기 때문입니다.

그럼 QSocket에 어떤 시그널이 있는지 보겠습니다.

시그널 이름 설명
void hostFound () connectToHost() 함수 호출 후에 상대 호스트가 있다면 발생합니다.
void connected () 상대 호스트와 연결이 이루어 지면 발생합니다.
void connectionClosed () 연결이 끊어지면 발생합니다.
void delayedCloseFinished () 연결 끊기를 요청했지만 바로 끊기지 않고 지연을 가지고 끊기를 시도하다가
드디어 끊어 졌을 때 발생합니다.
void readyRead () 읽어 들일 자료가 있다면 발생합니다.
void bytesWritten ( int nbytes ) nbytes만큼 전송이 되었음을 알려 주는 시그널입니다.
void error ( int ) 에러가 발생하면 알려주는 시그널입니다.

void delayedCloseFinished() 시그널을 보면 QSocket이 얼마나 잘 만들어 졌는지를 알 수 있습니다. 그냥 단순한 소켓 객체가 아니라는 것으로, 사용자가 어설프게 만든 소켓보다 훨씬 안전하겠지요. ^^

슬롯 준비

QSocket의 시그널을 알았으니 슬론을 준비해야 겠지요. 저는 아래와 같은 시그널을 사용하고 그에 대한 슬롯을 준비하겠습니다.

시그널 이름 설명
void connected () onConnected()
void connectionClosed () onDisconnectedByServer()
void delayedCloseFinished () delayedCloseFinished()
void readyRead () onReceive()
void error ( int ) onError( int)

void connectionClosed() 시그널에 해당하는 슬롯의 이름이 좀 이상합니다. onDisconnected()가 옳을 것 같은데 onDisconnectedByServer() 로, 슬롯 이름이 마치 서버로부터의 연결이 끊김을 처리하는 것처럼 보입니다.

이는 내가 먼저 끊기를 하지 않고 끊김이 발생햇다면 상대방이 먼저 끊기를 수행한 것입니다. 그러므로 슬롯 이름이 onDisconnectedByServer()이고 상대방의 연결 끊김에 대한 처리를 합니다.

그러내 내가 먼저 끊기를 실행하면, 그리고 바로 끊어 졌다면 연결 끊김 시그널은 아예 발생하지 않습니다. 그러므로 void connectionClosed() 시그널처리 스롯 이름을 onDisconnectedByServer()로 했습니다.

void delayedCloseFinished ()

내가 서버와의 연결을 끊기를 요청하면 바로 연결 끊김으로 처리할 수 있습니다만 TCP/IP는 연결지향 프로토콜로 연결이 바로 끊기지 않고 아직 전송이 완료되지 않은 전문을 위해서라도 연결을 끊기 위해 four-way handshaking을 처리합니다. 그러므로 이 과정에서 지연이 생길 수 있는데, QSocket은 이렇게 지연과정 후에 연결이 정말 끊기면 void delayedCloseFinished() 시그널을 발생합니다.

그러므로 종료 요청 후에 void delayedCloseFinished()가 발생하면 완전히 연결이 끊겼음을 알 수 있습니다.

프로그램 작성

  1. 프로제트 파일 생성, "C++ Project"를 선택합니다.


  2. 적당한 프로젝트 이름을 입력합니다.


  3. 프로젝트 파일을 저장합니다.


  4. 폼 이름을 지정합니다. 이 이름이 클래스 이름이 되므로 적당한 이름을 지정해 줍니다. 여기서는 frmMain으로 입력하겠습니다.


  5. 필요한 위젯을 배치합니다.


  6. QSocket을 사용하기 위해 QSocket sock;을 선언하겠습니다. 또한 디자이너가 QSocket을 알아야 하므로 qsocket.h를 등록합니다.


  7. 위에 열거한 슬롯은 마우스 오른쪽 버튼 클릭 메뉴의 New를 이용하여 등록합니다.


  8. 버튼의 클릭 시그널은 디자이너에서 직접 연결해 줍니다.

    btnConnect-clicked() ->  btnConnectClick()
    btnDisonnect-clicked() ->  btnDisonnectClick()
    btnSend-clicked() ->  btnSendClick()

  9. QSocket sock의 시그널은 코드에서 직접 연결하여 줍니다. 역시 폼이 생성되고 난 후에 자동으로 호출되는 init()를 이용하겠습니다.


  10. init()에 아래의 코드를 입력합니다.
void frmMain::init()
{
    sock = new QSocket( this);

    connect( sock, SIGNAL( connected()), this, SLOT( onConnected()));
    connect( sock, SIGNAL( connectionClosed()), this, SLOT( onDisconnectedByServer()));
    connect( sock, SIGNAL( error( int)), this, SLOT( onError( int)));
    connect( sock, SIGNAL( readyRead()), this, SLOT( onReceive()));
}
  1. 자료 전송을 위한 함수를 등록합니다. 뭐, 이 함수는, 만들지 않고 btnSendClick()에서 처리하셔도 되겠죠. ^^


  2. sendData()의 내용은 아래와 같습니다.
void frmMain::sendData( QString strData )
{
    QTextStream os(sock);
    os << strData << "n";
}
  1. 자 이제 말씀 드리지 않은 슬롯 함수의 코드를 입력하면 되겠습니다.
void frmMain::onConnected()
{
    setCaption( tr( "Connected"));
}

void frmMain::onDisconnected()
{
    setCaption( tr( "Disconnected"));
    QMessageBox::information( this, "client", "Disconnected!!");
}

void frmMain::onDisconnectedByServer()
{
    setCaption( tr( "Disconnected by server"));
}

void frmMain::onError( int error )
{
   setCaption( tr("Error number %1 occurredn").arg(error));
}
  1. 그리고 버튼 관련 슬롯 함수입니다.
void frmMain::btnSendClick()
{
    sendData( edtInput->text());
}

void frmMain::btnConnectClick()
{
    QString host_ip   = edtIp->text();

    setCaption(  "try to connect");
    sock->connectToHost( host_ip, 9527);
}

// 연결 종료는 앞서 말씀드린 바와 같이 종료 실행 후 바로 종료되었다면
// onDisconnected()를 호출하지만
// 연결 종료를 위해 지연시간을 갖는다면 언제 연결이 완전히 끊기는지 알기 위해
// delayedCloseFinished() 시그널에 onDisconnected() 슬롯을 연결했습니다.

void frmMain::btnDisconnectClick()
{
   setCaption(  "try to disconnect");
   sock->close();
   if ( sock->state() == QSocket::Closing )
   { // We have a delayed close.
       connect( sock, SIGNAL(delayedCloseFinished()), SLOT(onDisconnected()) );
   }
   else
   { // The socket is closed.
      onDisconnected();
   }
}
  1. 이제 실행해 보겠습니다. 이 프로그램을 테스트하기 위해 MS 윈도우용으로 테스터 프로그램을 첨부했습니다. 이 프로그램을 먼저 실행하십시오.


  2. 윈도우 프로그램을 클라이언트가 전송한 데이터를 그대로 반송해주는 매우 간단한 프로그램입니다.

  3. QT 프로그램을 컴파일하고 실행합니다.


  4. Server IP를 윈도우용 테스터 프로그램이 실행되고 있는 PC의 IP를 입력하고 connect 버튼을 클릭합니다.

  5. Server와 연결되면 캡션에 연결되었음이 표시됩니다.


  6. 이제 send에 있는 Edit 박스에 문자열을 넣고 send 버튼을 클릭합니다.

  7. 테스터 프로그램에 문자열의 길이와 함께 문자열이 보이고,


  8. 테스터 프로그램이 다시 반송하므로 QT 프로그램에서도 수신한 모습을 보실 수 있습니다.

어떻게 강좌의 내용이 이해 되시나요? 혹 틀린점이 있으시면 지적해 주시고, 참고할 내용이 있으셔서 올려 주시면 감사하겠습니다. 오늘도 즐거운 하루 되세요. ^^

frmMain.ui.h

앞에서 부분별로 나누어 말씀을 드렸습니다만 이해를 돕기 위해 frmMain.ui.h의 전체 내용을 올립니다. 또한 소스를 압축하여 첨부하니 참고하세요. ^^

/****************************************************************************
** ui.h extension file, included from the uic-generated form implementation.
**
** If you want to add, delete, or rename functions or slots, use
** Qt Designer to update this file, preserving your code.
**
** You should not define a constructor or destructor in this file.
** Instead, write your code in functions called init() and destroy().
** These will automatically be called by the form's constructor and
** destructor.
*****************************************************************************/

#include <qmessagebox.h>

///// 서버와 연결에 관한 슬롯

void frmMain::onConnected()
{
    setCaption( tr( "Connected"));
}

void frmMain::onDisconnected()
{
    setCaption( tr( "Disconnected"));
    QMessageBox::information( this, "client", "Disconnected!!");
}

void frmMain::onDisconnectedByServer()
{
    setCaption( tr( "Disconnected by server"));
}

void frmMain::onError( int error )
{
   setCaption( tr("Error number %1 occurredn").arg(error));
}

/////  데이터 송수신을 위한 함수 및 슬롯

void frmMain::sendData( QString strData )
{
    QTextStream os(sock);
    os << strData << "n";
}

void frmMain::onReceive()
{
   while ( sock->canReadLine() )
   {
      labReceive->setText( sock->readLine() );
   }
}

/////  버튼 처리 슬롯

void frmMain::btnSendClick()
{
    sendData( edtInput->text());
}

void frmMain::btnConnectClick()
{
    QString host_ip   = edtIp->text();

    setCaption(  "try to connect");
    sock->connectToHost( host_ip, 9527);
}

void frmMain::btnDisconnectClick()
{
   setCaption(  "try to disconnect");
   sock->close();
   if ( sock->state() == QSocket::Closing )
   { // We have a delayed close.
       connect( sock, SIGNAL(delayedCloseFinished()), SLOT(onDisconnected()) );
   }
   else
   { // The socket is closed.
      onDisconnected();
   }
}

/////  initialization

void frmMain::init()
{
    sock = new QSocket( this);

    connect( sock, SIGNAL( connected()), this, SLOT( onConnected()));
    connect( sock, SIGNAL( connectionClosed()), this, SLOT( onDisconnectedByServer()));
    connect( sock, SIGNAL( error( int)), this, SLOT( onError( int)));
    connect( sock, SIGNAL( readyRead()), this, SLOT( onReceive()));
}

태그: *QT *소켓프로그래밍 *tcp/ip