Qt 로 만들자: 인터넷 시계

이번에 만들어 볼 프로그램은 <인터넷 시계> 이다. <인터넷 시계> 만들어 봄으로써 Qt 에서 날짜 및 시간을 다루는 방법과 UDP 소켓을 이용하는 방법을 알 수 있게 될 것이다.

1. 요구 사항

  • 시간을 로컬 시간으로 나타낸다
  • 인터넷 시간과 시스템 시간을 비교한다

2. 코드 작성

2.1 프로젝트 작성

  • 프로젝트 이름: Clock
  • 메인 클래스 이름: Clock
  • 메인 클래스 유형: QMainWindow

2.2 헤더 분석(clock.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#ifndef CLOCK_H
#define CLOCK_H

#include <QMainWindow>

#include <QtWidgets>
#include <QtNetwork>

// 비트 필드 사용 여부
#define USE_BITFIELDS   0

/**
 * @brief 인터넷 시계
 */

class Clock : public QMainWindow
{
    Q_OBJECT

public:
    explicit Clock(QWidget *parent = 0);
    ~Clock();

signals:
    void udpReadFinished(); // udpRead() 가 끝나면 발생

private:
    /**
     * @brief SNTP 패킷 구조체
     */

    struct __attribute__((packed)) SntpPacket
    {
#if USE_BITFIELDS
        quint8 mode : 3;            ///< 작동 모드
        quint8 vn   : 3;            ///< 버전 번호
        quint8 li   : 2;            ///< 윤초 정보
#else
        quint8 li_vn_mode;          ///< 작동 모드/버전번호/윤초정보
#endif
        quint8 stratum;             ///< 서버 층위
        qint8  poll;                ///< 대기 시간, 2의 거듭제곱(초)
        quint8 precision;           ///< 시스템 클럭의 정밀도, 2의 거듭제곱(초)
        qint32 rootDelay;           ///< 1차 표준 소스에 대한 왕복 지연(초)
        quint32 rootDispersion;     ///< 최대 오차(초)
        quint32 refId;              ///< 표준 소스의 ID
        quint32 refTimeSec;         ///< 시스템 클럭이 수정된 시각(초)
        quint32 refTimeFrac;        ///< 시스템 클럭이 수정된 시각
        quint32 orgTimeSec;         ///< 메시지 생성 시간(초)
        quint32 orgTimeFrac;        ///< 메시지 생성 시간(소수)
        quint32 recvTimeSec;        ///< 메시지 수신 시간(초)
        quint32 recvTimeFrac;       ///< 메시지 수신 시간(소수)
        quint32 transTimeSec;       ///< 메시지 전송 시간(초)
        quint32 transTimeFrac;      ///< 메시지 전송 시간(소수)
        /*quint32 keyId;*/          ///< Key Identifier(선택적)
        /*quint8 md[ 16 ];*/        ///< Message Digest(선택적)
    };

    QLCDNumber *_systemTimeLCD;     ///< '시스템 시간'
    QLCDNumber *_internetTimeLCD;   ///< '인터넷 시간'
    QLCDNumber *_roundTripTimeLCD;  ///< '왕복 시간'
    QLabel     *_offsetTimeLabel;   ///< '오차'
    QPushButton *_timePush;         ///< '시간확인'

    QUdpSocket _udp;        ///< UDP 소켓
    QHostInfo _hostInfo;    ///< 호스트 정보
    quint16 _port;          ///< 포토 번호
    QByteArray _datagram;   ///< UDP 수신 데이터

    QTimer _timer;  ///< 타이머

    void initMenus();
    void initWidgets();
    void displayTime();

private slots:
    void lookedUp(const QHostInfo &hostInfo);
    void getTime();
    void udpRead();
    void timeOver();
};

#endif // CLOCK_H


7 번째 줄: 네트워크 관련 기능을 이용하기 위해서는 <QtNetowkr> 헤더 가 필요하다. 아울러 프로젝트 파일(Clock.pro) 도 수정해야 한다. 다음에 알아보도록 하자.

23 번째 줄: signals 는 클래스에서 발생시킬 시그널을 선언하는 곳이다. 그동안 슬롯에 대해서만 다루었는데, 이번에는 시그널도 쓰인다는 뜻이다.

30 번째 줄: __attribute__((packed)) 는 1 바이트 단위로 정렬하라는 것으로 gcc 에서만 쓰인다. 다른 컴파일러를 쓴다면 다른 방법을 써야 한다. MS 계열이라면 #pragma pack() 을 쓰면 된다. 네트워크 패킷들은 1 바이트 정렬을 필요로 하므로 패킷으로 사용할 구조체의 경우 1바이트로 정렬해주어야한다.

31~55 번째 줄: quint8quint32 는 각각 8 비트와 32 비트 부호없는 자료형을 나타낸다. 각각의 요소들은 주석을 참고하고, 자세한 것은 RFC 4330 문서를 보자.

57~59 번째 줄: QLCDNumber 는 숫자를 전광판처럼 나타내는 위젯이다.

출처: Qt 도움말(QLCDNumber), 왼쪽부터 Windows, Windows Vista, Macintoshi, Fusion


63 번째 줄: QUdpSocket 은 Qt 에서 UDP 를 구현한 클래스이다.

64 번째 줄: QHostInfo 는 호스트를 찾거나, 찾은 호스트에 주소 따위의 정보를 담고 있다.

65 번째 줄: quint16 은 16 비트 부호없는 정수형이다.

66 번째 줄: QByteArray 는 바이트 배열을 구현한 클래스이다.

68 번째 줄: QTimer 는 타이머 관련 기능을 제공하며, _timer 로 일정 시간 안에 UDP 데이터그램이 도착하는지를 확인한다.

2.3 프로젝트 파일 수정(Clock.pro)

Qt 에서 네트워크 기능을 쓰기 위해서는 다음처럼 network 를 QT 에 추가할 필요가 있다.

QT += core gui network

2.4 소스 분석(clock.cpp)

2.4.1 생성자와 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * @brief Clock 생성자
 * @param parent 부모 위젯
 */

Clock::Clock(QWidget *parent)
    : QMainWindow(parent)
    , _port(123)
{
    initMenus();    // 메뉴 초기화
    initWidgets();  // 위젯 초기화

    // 서버 찾기
    QHostInfo::lookupHost("time.nist.gov"this, SLOT(lookedUp(QHostInfo)));

    // 한 번만 타이머 실행
    _timer.setSingleShot(true);
    connect(&_timer, SIGNAL(timeout()), this, SLOT(timeOver()));
    connect(this, SIGNAL(udpReadFinished()), &_timer, SLOT(stop()));

    connect(&_udp, SIGNAL(readyRead()), this, SLOT(udpRead()));
}

/**
 * @brief Clock 소멸자
 */

Clock::~Clock()
{
}


7 번째 줄: 인터넷 시계는 SNTP 프로토콜을 이용하는데, 포트번호는 123 이다.

13 번째 줄: QHostInfo::lookupHost() 는 주어진 호스트 이름을 찾고, 결과를 주어진 슬롯으로 전달하는 정적함수이다.

16 번째 줄: QTimer::setSingleShot() 는 타이머의 반복 실행 여부를 지정한다. true 라면 한 번만 슬롯을 호출하고 타이머는 멈추지만, false 이면 타임 아웃될 때마다 매번 슬롯을 호출한다.

17 번째 줄: QTimer::timeout() 시그널은 주어진 시간이 경과하면 발생한다.

18 번째 줄: QTimer::stop() 은 타이머를 멈추는 슬롯이다.

20 번째 줄: QIODevice::readyRead() 는 장치로부터 읽을 데이터가 있으면 발생하는 시그널이다. 따라서 QUdpSocket 의 경우 데이터그램이 도착하면 발생한다.

2.4.2 initMenus()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @brief 메뉴를 초기화한다
 */

void Clock::initMenus()
{
    // '파일' 메뉴 생성
    QMenu *file = new QMenu(tr("파일(&F)"));

    // '끝내기' 항목 추가
    file->addAction(tr("끝내기(&x)"), this, SLOT(close()),
                    QKeySequence(tr("Ctrl+Q")));

    // '파일' 메뉴 추가
    menuBar()->addMenu(file);
}


메뉴를 초기화한다. 특별한 내용은 없다.

2.4.3 initWidgets()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * @brief 위젯을 초기화한다
 */

void Clock::initWidgets()
{
    // 표시 자리 19 자리
    _systemTimeLCD = new QLCDNumber(19);
    _systemTimeLCD->setSegmentStyle(QLCDNumber::Flat);

    // 표시 자리 19 자리
    _internetTimeLCD = new QLCDNumber(19);
    _internetTimeLCD->setSegmentStyle(QLCDNumber::Flat);

    // 표시 자리 19 자리
    _roundTripTimeLCD = new QLCDNumber(19);
    _roundTripTimeLCD->setSegmentStyle(QLCDNumber::Flat);

    _offsetTimeLabel = new QLabel;

    _timePush = new QPushButton(tr("시간확인(&T)"));
    _timePush->setDisabled(true);
    connect(_timePush, SIGNAL(clicked(bool)), this, SLOT(getTime()));

    QFormLayout *form = new QFormLayout;
    form->addRow(tr("시스템 시간:"), _systemTimeLCD);
    form->addRow(tr("인터넷 시간:"), _internetTimeLCD);
    form->addRow(tr("왕복 시간:"), _roundTripTimeLCD);
    form->addRow(tr("오차 시간:"), _offsetTimeLabel);
    form->addRow(_timePush);

    QWidget *w = new QWidget;
    w->setLayout(form);

    setCentralWidget(w);
}


7 번째 줄: QLCDNumber 생성자는 표시할 자릿수를 넘겨 받는다.
8 번째 줄: QLCDNumber::setSegmentStyle() 은 표시 스타일을 설정한다. QLCDNumber::Flat 은 숫자를 windowTextColor 로 표시한다.

21 번째 줄: QWidget::setDisabled() 는 해당 위젯의 비활성화 여부를 결정한다.

2.4.4 lookedupHost()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * @brief lookupHost() 결과 처리
 * @param hostInfo 호스트 정보
 */

void Clock::lookedUp(const QHostInfo &hostInfo)
{
    if (hostInfo.error() != QHostInfo::NoError) {
        QMessageBox::warning(this, windowTitle(),
                             tr("호스트를 찾지 못했습니다: %1")
                                .arg(hostInfo.errorString()));

        return;
    }

    // 호스트 정보 저장
    _hostInfo = hostInfo;

    // '시간확인' 버튼 활성화
    _timePush->setEnabled(true);
}


QHostInfo::lookupHost() 으로부터 받은 결과를 나중에 쓰기 위해 _hostInfo 에 저장한다. 그리고 호스트를 찾았을 때 '시간확인' 버튼을 활성화한다.

7~13 번째 줄: QHostInfo::error() 는 호스트 이름을 찾지 못했을 때 에러 유형을 알려준다. 에러가 없으면 QHostInfo::NoError 를 돌려준다. QHostINfo::errorString() 으로 에러 문자열을 얻을 수 있다.

19 번째 줄: QWidget::setEnabled() 로 위젯의 활성화 여부를 설정할 수 있다.

2.4.5 getTime()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * @brief 인터넷 시간을 얻는다
 */

void Clock::getTime()
{
    _datagram.clear();

    // 진행 대화상자 생성
    QProgressDialog *progressDlg = new QProgressDialog(this);
    // 대화상자가 닫히면 메모리에서 해제함
    progressDlg->setAttribute(Qt::WA_DeleteOnClose);
    progressDlg->setLabelText(tr("시간을 확인하고 있습니다..."));
    progressDlg->setMinimumDuration(1 * 1000);
    // 진행중 표시
    progressDlg->setRange(0, 0);
    // 값 초기화
    progressDlg->setValue(0);
    connect(this, SIGNAL(udpReadFinished()), progressDlg, SLOT(close()));

    connect(&_timer, SIGNAL(timeout()), progressDlg, SLOT(close()));
    // 타이머 작동, 시간은 10초
    _timer.start(10 * 1000);


'시간확인' 버튼을 눌렀을 때 호출된다. 필요하다면 진행상황을 알려주는 대화상자를보여주고, 데이터그램을 타임 서버에 보낸다.

6 번째 줄: QByteArray::clear() 는 바이트 배열의 내용을 지우고, 비운다.

11 번째 줄: QWidget::setAttribute() 는 위젯의 속성을 설정한다. Qt::WA_DeleteOnClose 는 위젯이 닫히면, 메모리에서 해제하라는 것을 뜻한다. 이 속성은 별도의 변수를 유지하지 않고, 위젯을 해제할 수 있도록 해주는 유용한 속성이다. 게다가 이 속성으로 메모리 누수의 위험도 방지할 수 있다.

15 번째 줄: QProgressDialog::setRange() 의 최솟값과 최댓값을 동일하게 하면 진행 막대가조금씩 길어지는 대신, 조그만 막대가 처음과 끝을 왔다 갔다한다.

17 번째 줄: QProgressDialog::setValue() 는 진행막대의 값을 설정하는데, 이 함수를 한 번이라도 호출하지 않으면 QProgressDialog::setMinimumDuration() 에서 지정한 시간은 작동하지 않는다.

22 번째 줄: QTimer::start() 는 타이머를 작동시킨다. 시간이 주어지면, 주어진 시간이 지난 후 QTimer::timeout() 시그널을 발생시킨다. 타이머가 single shot 이라면 한 번만 시그널이 발생하고, 아니면 주어진 시간이 지날 때마다 시그널이 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    // 패킷 설정
    SntpPacket pkt;
    memset(&pkt, 0, sizeof(pkt));
#if USE_BITFIELDS
    pkt.mode = 3;   // 클라이언트 모드
    pkt.vn = 4;     // 현재 버전 번호
#else
    pkt.li_vn_mode = 3/*클라이언트 모드*/ | (4/*현재 버전 번호*/ << 3);
#endif

    // 전송 시간 설정, 이후 생성 시간으로 돌아옴
    pkt.transTimeSec =
            qToBigEndian(
                toSntp(QDateTime::currentDateTime().toTime_t()));
    // 패킷 전송
    if (_udp.writeDatagram(reinterpret_cast<char *>(&pkt), sizeof(pkt),
                           _hostInfo.addresses().first(), _port) == -1)
        QMessageBox::warning(this, windowTitle(),
                             tr("데이터그램을 보내지 못했습니다: %1")
                                .arg(_udp.errorString()));
}


2~9 번째 줄: 패킷을 초기화하고, 필요한 값을 설정한다. SntpPacket 구조체는 리틀 엔디안으로 작성되어 있다. 빅 엔디안이라면 비트 순서에 따라 값을 조정할 필요가 있다.

우리는 서버에 시간을 확인하고 있으므로 '클라이언트 모드' 로 설정하고, SNTP 버전을 최신 버전인 v4 로 맞춘다.

12 번째 줄: 전송 시간을 설정한다. 이 시간은 서버측에서 생성 시간으로 저장되어 돌아온다. 중요한 것은 네트워크 프로토콜은 빅 엔디언 기준이므로, 빅 엔디언으로 바꾸어서 저장한다. qToBigEndian() 는 주어진 값을 빅 엔디언으로 바꾼다. toSntp() 는 Qt 시간을 SNTP 시간으로 바꾸는 함수이다. 나중에 살펴 볼 것이다. QDateTime 은 Qt 에서 날짜와 시간을 동시에 다루는 클래스이다. QDateTime::currentDateTime() 은 현재 날짜와 시간을 알려준다. QDateTime::toTime_t() 는 현재 시간을 1970 년 1월 1일 자정 UTC 를 기준으로 초 단위로 돌려준다.

16~20 번째 줄: QUdpSocket::writeDatagram() 은 데이터를 주어진 주소와 포트로 보낸다. QHostInfo::addresses() 는 호스트에 해당하는 주소를 QList<QHostAddress> 로 돌려준다. QHostAddress 는 IP 주소를 나타낸다. 만일 QUdpSocket 이 QUdpSocket::bind() 를 이용해서 연결된 상태라면 QUdpSocket::writeDatagram() 대신에 QIODevice::write() 를 써야 한다.

2.4.6 fromSntp() 와 toSntp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * @brief SNTP 시간을 Qt 시간으로 바꾼다
 * @param secs SNTP 시간(초)
 * @return Qt 시간(초)
 * @remakr SNTP 는 1900년 1월 1일 UTC 를 기준으로 하고, Qt 는 1970년 1월 1일
 * UTC 를 기준으로 한다
 */

static inline quint32 fromSntp(quint32 secs)
{
    // 1900년 1월 1일 UTC 와 1970년 1월 1일 UTC 의 차이를 계산한다. 단, 1900년
    // 은 윤년이 아니지만, 2000년은 윤년이므로, 2000년 1월 2일 UTC 부터 계산한
    // 다
    QDateTime dt1(QDate(2000, 1, 2), QTime(0, 0), Qt::UTC);
    QDateTime dt2(QDate(2070, 1, 1), QTime(0, 0), Qt::UTC);

    return secs - dt1.secsTo(dt2);
}

/**
 * @brief Qt 시간을 SNTP 시간으로 바꾼다
 * @param secs Qt 시간(초)
 * @return SNTP 시간(초)
 */

static inline quint32 toSntp(quint32 secs)
{
    // 1900년 1월 1일 UTC 와 1970년 1월 1일 UTC 의 차이를 계산한다. 단, 1900년
    // 은 윤년이 아니지만, 2000년은 윤년이므로, 2000년 1월 2일 UTC 부터 계산한
    // 다
    QDateTime dt1(QDate(2000, 1, 2), QTime(0, 0), Qt::UTC);
    QDateTime dt2(QDate(2070, 1, 1), QTime(0, 0), Qt::UTC);

    return secs + dt1.secsTo(dt2);
}


SNTP 와 Qt 의 시간 체계는 다르다. SNTP 는 1900년 1월 1일 자정 UTC 를 기준으로 삼지만, Qt 는 1970년 1월 1일 자정 UTC 를 기준으로 삼는다. 따라서 이를 변환해줄 필요가 있다. 가능하다면 1900년 1월 1일 자정 UTC 와 1970년 1월 1일 자정 UTC 의 차이를 직접 계산하면 좋겠지만, 앞서 말했듯이 Qt 는 1970년 1월 1일 자정 UTC 이전은 지원하지 않는다. 따라서 1900년 1월 1일 자정 UTC 와 1970년 1월 1일 자정 UTC 대신에 2000년 1월 1일 자정 UTC 와 2070년 1월 1일 자정 UTC 의 차이를 계산한다. 하지만 1900년은 윤년이 아니지만, 2000년은 윤년이므로, 하루가 더 많다. 이에 따라, 2000년 1월 1일 UTC 대신에 하루 늦은 2000년 1월 2일 자정 UTC 부터 계산한다.

13~14 번째 줄: QDate 는 날짜를 나타내고, QTime 은 시간을 나타낸다. Qt::UTC 는 시간을 UTC 로 표현할 때 쓰인다.

16 번째 줄: QDateTime::secsTo() 는 주어진 날짜까지의 시간을 초 단위로 계산한다.

2.4.7 udpRead()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * @brief _udp.readyRead() 시그널이 발생하면 데이터그램을 읽는다
 */

void Clock::udpRead()
{
    // 데이터그램이 있으면
    while (_udp.hasPendingDatagrams())
    {
        QByteArray datagram;
        datagram.resize(_udp.pendingDatagramSize());

        QHostAddress addr(_hostInfo.addresses().first());

        // 데이터그램을 읽음
        if (_udp.readDatagram(datagram.data(), datagram.size(), &addr, &_port)
                == -1)
        {
            QMessageBox::warning(this, windowTitle(),
                                 tr("데이터그램을 받지 못했습니다: %1")
                                    .arg(_udp.errorString()));

            break;
        }

        _datagram.append(datagram);
    }

    displayTime();

    // udpRead() 끝났음
    emit udpReadFinished();
}


QIODevice::readyRead() 시그널이 발생하면, 전송된 데이터그램을 읽어들이고, 데이터그램을 분석해서 시간을 표시한다.

7 번째 줄: QUdpSocket::hasPendingDatagrams() 는 대기하고 있는 데이터그램이 있으면 true 를 돌려주고, 아니면 false 를 돌려준다.

10 번째 줄: QByteArray::resize() 는 바이트 배열을 주어진 크기로 조정한다. 주어진 크기 이후의 바이트는 사라진다. QUdpSocket::pendingDatagramSize() 는 대기하고 있는 첫번째 데이터그램의 크기를 돌려준다.

15 번째 줄: QUdpSocket:readDatagram() 는 대기하고 있는 데이터그램을 읽는다. 주어진 크기가 작으면 나머지 데이터그램은 사라진다. 이를 피하고 싶다면 QUdpSocket::pendingDatagramSize() 로 데이터그램의 크기를 반드시 먼저 확인하자. QByteArray::data() 는 바이트 배열의 데이터에 대한 문자 포인터를 돌려준다. QByteArray::size() 는 바이트 배열의 크기를 돌려준다.

20 번째 줄: QIODevice::errorString() 은 에러에 대한 문자열을 돌려준다.

25 번째 줄: QByteArray::append() 는 주어진 바이트 배열을 추가한다.

28 번째 줄: displayTime()  은 현재 시간을 표시하는 함수이다. 나중에 살펴볼 것이다.

31 번째 줄: emit 은 시그널을 발생시킨다.

2.4.8 displayTime()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * @brief 시간을 표시한다
 */

void Clock::displayTime()
{
    SntpPacket pkt;

    // 수신한 데이터그램의 크기가  패킷 크기 이상이어야 한다
    if (static_cast<unsigned>(_datagram.size()) < sizeof(pkt))
        return;

    memcpy(&pkt, _datagram.data(), sizeof(pkt));

    // 시간 계산
    QDateTime now(QDateTime::currentDateTime());
    quint32 orgTime = qFromBigEndian(pkt.orgTimeSec);
    quint32 recvTime = qFromBigEndian(pkt.recvTimeSec);
    quint32 transTime = qFromBigEndian(pkt.transTimeSec);
    quint32 destTime = toSntp(now.toTime_t());

    // 왕복 시간
    qint32 d = (destTime - orgTime) - (transTime - recvTime);
    // 오차 시간
    qint32 t = ((qint32)(recvTime - orgTime) +
                (qint32)(transTime - destTime)) / 2;

    QString format("yyyy-MM-dd HH:mm:ss");

    // 시간 표시
    _systemTimeLCD->display(now.toString(format));
    _internetTimeLCD->display(now.addSecs(t).toString(format));
    _roundTripTimeLCD->display(d);

    // 오차 시간 표시
    QString text;
    if (t > 0)
        text = tr("시스템 시간이 %1 초 느립니다.").arg(t);
    else if (t < 0)
        text = tr("시스템 시간이 %1 초 빠릅니다.").arg(-t);
    else
        text = tr("시스템 시간이 인터넷 시간과 같습니다.");

    _offsetTimeLabel->setText(text);
}


받은 데이터그램을 분석해서 시간을 표시한다.

9 번째 줄: 데이터그램의 크기를 확인한다. 일반적으로 분석할 패킷의 크기와 받은 데이터의 크기를 비교하는 것이 필요하다. 받은 데이터의 크기가 더 작음에도 불구하고, 데이터를 읽으려고 하면 문제가 생길 수 있다. 여기에서는 데이터그램 버퍼를 도입해서 다소 완화하고 있다. 그리고 TCP 소켓을 쓸 경우에는 이 문제에 더욱 신경써야 한다.

15~19 번째 줄: 위에서도 말했듯이, 일반적으로 네트워크 프로토콜은 빅 엔디언을 쓰기 때문에 시스템에서 사용하는 엔디언으로 바꿀 필요가 있다. qFromBigEndian() 은 빅 엔디언을 시스템 엔디언으로 바꾼다.

21~25 번째 줄: 왕복 지연 시간을 계산하고, 인터넷 시간과 시스템 시간의 차이를 계산한다. 이에 대한 자세한 내용은 RFC 4330위키 문서를 보기 바란다. 포맷에 대한 자세한 설명은 Qt 도움말을 참고하자.

27~32 번째 줄: 계산한 시간을 표시한다. QLCNumber::display() 는 주어진 문자열을 표시한다. QDateTime::toString() 은 주어진 포맷에 따라 날짜와 시간을 문자열로 바꾼다.

2.4.9 timeOver()

1
2
3
4
5
6
7
8
/**
 * @brief _timer.timeout() 시그널을 처리한다
 */

void Clock::timeOver()
{
    QMessageBox::warning(this, windowTitle(),
                         tr("시간을 확인하지 못했습니다."));
}


일정 시간 동안 데이터그램을 받지 못하면, 이에 대한 메세지를 보여준다.

3. 마무리하면서...

<인터넷 시계> 를 만들면서 Qt 에서 날짜와 시간을 다루는 방법과 UDP 소켓을 다루는 방법에 대해서 살펴보았다. 처리하기가 까다로울 수도 있는 부분들이지만, Qt 에서는 비교적 쉽게 처리할 수 있도록 만들어져 있다.

다음은<인터넷 시계> 의 실행 모습이다.


전체 소스는 여기에서 확인하자.

댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷