Qt 로 만들자: 인터넷 시계
이번에 만들어 볼 프로그램은 <인터넷 시계> 이다. <인터넷 시계> 만들어 봄으로써 Qt 에서 날짜 및 시간을 다루는 방법과 UDP 소켓을 이용하는 방법을 알 수 있게 될 것이다.
1. 요구 사항
2. 코드 작성
2.1 프로젝트 작성
2.2 헤더 분석(clock.h)
7 번째 줄: 네트워크 관련 기능을 이용하기 위해서는 <QtNetowkr> 헤더 가 필요하다. 아울러 프로젝트 파일(Clock.pro) 도 수정해야 한다. 다음에 알아보도록 하자.
23 번째 줄: signals 는 클래스에서 발생시킬 시그널을 선언하는 곳이다. 그동안 슬롯에 대해서만 다루었는데, 이번에는 시그널도 쓰인다는 뜻이다.
30 번째 줄: __attribute__((packed)) 는 1 바이트 단위로 정렬하라는 것으로 gcc 에서만 쓰인다. 다른 컴파일러를 쓴다면 다른 방법을 써야 한다. MS 계열이라면 #pragma pack() 을 쓰면 된다. 네트워크 패킷들은 1 바이트 정렬을 필요로 하므로 패킷으로 사용할 구조체의 경우 1바이트로 정렬해주어야한다.
31~55 번째 줄: quint8 와 quint32 는 각각 8 비트와 32 비트 부호없는 자료형을 나타낸다. 각각의 요소들은 주석을 참고하고, 자세한 것은 RFC 4330 문서를 보자.
57~59 번째 줄: QLCDNumber 는 숫자를 전광판처럼 나타내는 위젯이다.
63 번째 줄: QUdpSocket 은 Qt 에서 UDP 를 구현한 클래스이다.
64 번째 줄: QHostInfo 는 호스트를 찾거나, 찾은 호스트에 주소 따위의 정보를 담고 있다.
65 번째 줄: quint16 은 16 비트 부호없는 정수형이다.
66 번째 줄: QByteArray 는 바이트 배열을 구현한 클래스이다.
68 번째 줄: QTimer 는 타이머 관련 기능을 제공하며, _timer 로 일정 시간 안에 UDP 데이터그램이 도착하는지를 확인한다.
2.3 프로젝트 파일 수정(Clock.pro)
Qt 에서 네트워크 기능을 쓰기 위해서는 다음처럼 network 를 QT 에 추가할 필요가 있다.
2.4 소스 분석(clock.cpp)
2.4.1 생성자와 소멸자
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()
메뉴를 초기화한다. 특별한 내용은 없다.
2.4.3 initWidgets()
7 번째 줄: QLCDNumber 생성자는 표시할 자릿수를 넘겨 받는다.
8 번째 줄: QLCDNumber::setSegmentStyle() 은 표시 스타일을 설정한다. QLCDNumber::Flat 은 숫자를 windowTextColor 로 표시한다.
21 번째 줄: QWidget::setDisabled() 는 해당 위젯의 비활성화 여부를 결정한다.
2.4.4 lookedupHost()
QHostInfo::lookupHost() 으로부터 받은 결과를 나중에 쓰기 위해 _hostInfo 에 저장한다. 그리고 호스트를 찾았을 때 '시간확인' 버튼을 활성화한다.
7~13 번째 줄: QHostInfo::error() 는 호스트 이름을 찾지 못했을 때 에러 유형을 알려준다. 에러가 없으면 QHostInfo::NoError 를 돌려준다. QHostINfo::errorString() 으로 에러 문자열을 얻을 수 있다.
19 번째 줄: QWidget::setEnabled() 로 위젯의 활성화 여부를 설정할 수 있다.
2.4.5 getTime()
'시간확인' 버튼을 눌렀을 때 호출된다. 필요하다면 진행상황을 알려주는 대화상자를보여주고, 데이터그램을 타임 서버에 보낸다.
6 번째 줄: QByteArray::clear() 는 바이트 배열의 내용을 지우고, 비운다.
11 번째 줄: QWidget::setAttribute() 는 위젯의 속성을 설정한다. Qt::WA_DeleteOnClose 는 위젯이 닫히면, 메모리에서 해제하라는 것을 뜻한다. 이 속성은 별도의 변수를 유지하지 않고, 위젯을 해제할 수 있도록 해주는 유용한 속성이다. 게다가 이 속성으로 메모리 누수의 위험도 방지할 수 있다.
15 번째 줄: QProgressDialog::setRange() 의 최솟값과 최댓값을 동일하게 하면 진행 막대가조금씩 길어지는 대신, 조그만 막대가 처음과 끝을 왔다 갔다한다.
17 번째 줄: QProgressDialog::setValue() 는 진행막대의 값을 설정하는데, 이 함수를 한 번이라도 호출하지 않으면 QProgressDialog::setMinimumDuration() 에서 지정한 시간은 작동하지 않는다.
22 번째 줄: QTimer::start() 는 타이머를 작동시킨다. 시간이 주어지면, 주어진 시간이 지난 후 QTimer::timeout() 시그널을 발생시킨다. 타이머가 single shot 이라면 한 번만 시그널이 발생하고, 아니면 주어진 시간이 지날 때마다 시그널이 발생한다.
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()
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()
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()
받은 데이터그램을 분석해서 시간을 표시한다.
9 번째 줄: 데이터그램의 크기를 확인한다. 일반적으로 분석할 패킷의 크기와 받은 데이터의 크기를 비교하는 것이 필요하다. 받은 데이터의 크기가 더 작음에도 불구하고, 데이터를 읽으려고 하면 문제가 생길 수 있다. 여기에서는 데이터그램 버퍼를 도입해서 다소 완화하고 있다. 그리고 TCP 소켓을 쓸 경우에는 이 문제에 더욱 신경써야 한다.
15~19 번째 줄: 위에서도 말했듯이, 일반적으로 네트워크 프로토콜은 빅 엔디언을 쓰기 때문에 시스템에서 사용하는 엔디언으로 바꿀 필요가 있다. qFromBigEndian() 은 빅 엔디언을 시스템 엔디언으로 바꾼다.
21~25 번째 줄: 왕복 지연 시간을 계산하고, 인터넷 시간과 시스템 시간의 차이를 계산한다. 이에 대한 자세한 내용은 RFC 4330 과 위키 문서를 보기 바란다. 포맷에 대한 자세한 설명은 Qt 도움말을 참고하자.
27~32 번째 줄: 계산한 시간을 표시한다. QLCNumber::display() 는 주어진 문자열을 표시한다. QDateTime::toString() 은 주어진 포맷에 따라 날짜와 시간을 문자열로 바꾼다.
2.4.9 timeOver()
일정 시간 동안 데이터그램을 받지 못하면, 이에 대한 메세지를 보여준다.
3. 마무리하면서...
<인터넷 시계> 를 만들면서 Qt 에서 날짜와 시간을 다루는 방법과 UDP 소켓을 다루는 방법에 대해서 살펴보았다. 처리하기가 까다로울 수도 있는 부분들이지만, Qt 에서는 비교적 쉽게 처리할 수 있도록 만들어져 있다.
다음은<인터넷 시계> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
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 번째 줄: quint8 와 quint32 는 각각 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 에서는 비교적 쉽게 처리할 수 있도록 만들어져 있다.
다음은<인터넷 시계> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
댓글
댓글 쓰기