콘텐츠로 이동
무료 평가 시작

Qt 클라이언트 작업자

  • pdcomm_opcua_qt.hpp의 qt_client_worker로 네이티브 클라이언트를 작업자 스레드에서 실행합니다.
  • ready, completed, failed, stopped 신호를 수신 객체의 스레드에서 처리합니다.
  • 작업자를 정상 종료(drain)하고 결과를 확인합니다.
  • PDComm OPC UA SDK qt 패키지(pdcomm-opcua-qt-darwin-arm64.tar.gz). native 패키지의 내용을 모두 포함하고 Qt 도우미 헤더와 Qt 예제를 더합니다.
  • Qt 6 Core. Qt는 패키지에 들어 있지 않습니다. 사용하는 Qt와 그 라이선스로 빌드합니다. 이 페이지는 Homebrew Qt 6.11.2로 실행했습니다.
  • PKI와 신뢰 설정에서 만든 $PKI의 인증서. 터미널 두 개.

실습 폴더에서 qt 패키지를 풀고 SDK 위치를 환경 변수로 지정합니다.

터미널 창
tar -xzf pdcomm-opcua-qt-darwin-arm64.tar.gz
export OPCUA_QT_SDK="$PWD/pdcomm-opcua-qt-darwin-arm64"

Qt 예제를 켜고 Qt 6 설치 위치를 CMAKE_PREFIX_PATH로 넘겨 빌드합니다.

터미널 창
cd "$OPCUA_QT_SDK"
cmake -S examples/opcua -B build -DCMAKE_BUILD_TYPE=Release -DPDCOMM_OPCUA_EXAMPLES_QT=ON \
-DCMAKE_PREFIX_PATH="$(qmake -query QT_INSTALL_PREFIX)"
cmake --build build --config Release

build/opcua_qt_client와 C·C++ 예제가 생깁니다. pdcomm_opcua_qt.hpp는 QObject를 선언하므로 AUTOMOC가 처리하도록 빌드 대상의 소스 목록에 넣어야 합니다. 예제의 CMakeLists.txt가 그렇게 합니다.

examples/opcua/qt/opcua_qt_client.cpp
// PDComm OPC UA QtCore client example.
// The worker thread owns the native client; results arrive in the main thread
// through queued signals. Usage:
// opcua_qt_client ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [NODE_ID]
// Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 failure.
#include <pdcomm/opcua/pdcomm_opcua_qt.hpp>
#include <QCoreApplication>
#include <QFile>
#include <QTimer>
#include <cstdio>
#include <memory>
#include "../ua_binary.h"
using namespace pdcomm::opcua;
static bool load(const QString& path, QByteArray& data) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly) || file.size() <= 0 || file.size() > 64 * 1024) return false;
data = file.readAll();
return !data.isEmpty();
}
int main(int argc, char** argv) {
QCoreApplication application(argc, argv);
const auto arguments = application.arguments();
bool numeric = true;
const uint32_t node = arguments.size() == 6 ? arguments[5].toUInt(&numeric) : 1001;
if ((arguments.size() != 5 && arguments.size() != 6) || !numeric || node == 0) {
std::fputs("Usage: opcua_qt_client ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [NODE_ID]\n",
stderr);
return 2;
}
qt_client_configuration configuration;
configuration.endpoint = arguments[1].toUtf8();
configuration.anonymous = true;
if (!load(arguments[2], configuration.certificate_der) ||
!load(arguments[3], configuration.private_key_pem) ||
!load(arguments[4], configuration.trusted_server_der)) {
std::fputs("Error: cannot read the certificate, private key or trusted server certificate\n", stderr);
return 3;
}
int result = 3;
QObject receiver; // Stable receiver context for every queued connection.
auto worker = std::make_unique<qt_client_worker>(configuration);
// The worker copied the configuration; wipe the caller copy of the key.
configuration.private_key_pem.fill('\0');
configuration.private_key_pem.clear();
QObject::connect(worker.get(), &qt_client_worker::ready, &receiver, [&](quintptr) {
uint8_t request[256];
const auto size = ua_encode_read(request, sizeof(request), 2, node);
const auto status = worker->submit(QByteArray(reinterpret_cast<const char*>(request),
static_cast<qsizetype>(size)), 1);
if (status != PDCOMM_OPCUA_OK) {
std::fprintf(stderr, "Error: submit rejected (SDK status %d)\n", static_cast<int>(status));
worker->request_stop(qt_client_stop::abort);
}
}, Qt::QueuedConnection);
QObject::connect(worker.get(), &qt_client_worker::completed, &receiver,
[&](qt_client_response_ptr response) {
const auto body = response->value->copy();
uint32_t service = 0;
ua_value value;
if (ua_decode_read(body.data(), body.size(), &service, &value)) {
std::fputs("Error: malformed ReadResponse\n", stderr);
} else {
std::printf("Read ns=2;i=%u value=%s service=0x%08x status=0x%08x\n", static_cast<unsigned>(node),
value.has_int32 ? QByteArray::number(value.int32).constData() : "none",
static_cast<unsigned>(service), static_cast<unsigned>(value.status));
result = ((service | value.status) & 0x80000000u) ? 1 : 0;
}
worker->request_stop(qt_client_stop::drain);
}, Qt::QueuedConnection);
QObject::connect(worker.get(), &qt_client_worker::failed, &receiver, [&](qt_client_failure failure) {
std::fprintf(stderr, "Error: %s failed (SDK status %d): %s\n", failure.phase.toUtf8().constData(),
static_cast<int>(failure.status), failure.text.toUtf8().constData());
result = 3;
worker->request_stop(qt_client_stop::abort);
}, Qt::QueuedConnection);
QObject::connect(worker.get(), &qt_client_worker::stopped, &receiver, [&](qt_client_terminal terminal) {
std::printf("Worker stopped orderly=%s\n", terminal.orderly ? "true" : "false");
std::fflush(stdout);
application.quit();
}, Qt::QueuedConnection);
worker->start();
application.exec();
worker->wait();
worker.reset();
return result;
}
  1. 구성. qt_client_configuration에 endpoint URL, 인증서, 개인 키, 신뢰할 서버 인증서를 넣습니다. anonymous를 참으로 하지 않으면 user_name과 user_secret으로 활성화합니다. 기본 제한 시간은 5000 ms입니다.
  2. 작업자 생성. 생성자가 구성을 복사하므로 호출자의 키를 바로 지웁니다.
  3. 신호 연결. 모든 신호를 수신 객체(receiver)와 Qt::QueuedConnection으로 연결합니다. 처리기는 수신 객체의 스레드(여기서는 주 스레드)에서 실행되고, 수신 객체가 사라지면 Qt가 연결을 끊습니다.
  4. ready. 세션이 활성화되면 옵니다. 예제는 여기서 ReadRequest를 qt_client_worker::submit으로 보냅니다. 두 번째 인자는 응답과 짝을 맞출 상관 번호(correlation)입니다.
  5. completed. 응답이 오면 상관 번호, 요청 ID, 응답 정보, 응답 버퍼를 받습니다. 응답은 ServiceFault일 수 있으므로 metadata.service_status와 본문의 항목 결과를 확인합니다.
  6. 종료 요청. qt_client_worker::request_stop에 drain을 주면 받은 요청을 모두 끝낸 뒤 세션과 채널을 닫습니다. abort는 남은 작업을 버리고 프로토콜 종료 없이 끝냅니다.
  7. stopped. 항상 마지막 신호입니다. orderly가 참이면 drain이 CloseSession까지 마쳤다는 뜻입니다.
  8. 정리. 작업자를 wait()로 기다린 뒤 해제합니다. 작업자 스레드 안에서 작업자를 지우지 않습니다.

submit과 request_stop은 어느 스레드에서 불러도 됩니다. 다른 멤버 함수는 작업자를 소유한 스레드에서만 부릅니다.

첫 번째 터미널에서 같은 패키지의 C++ 서버를 실행합니다.

터미널 창
cd "$OPCUA_QT_SDK"
build/opcua_server_cpp 127.0.0.1:4840 opc.tcp://localhost:4840/pdcomm "$PKI/server.der" "$PKI/server.key.pem" "$PKI/client.der"
Listening on 127.0.0.1:4840

두 번째 터미널에서 Qt 클라이언트를 실행합니다. 마지막 인자로 네임스페이스 2의 숫자 NodeId를 줄 수 있습니다(기본값 1001).

터미널 창
cd "$OPCUA_QT_SDK"
build/opcua_qt_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der"
Read ns=2;i=1001 value=42 service=0x00000000 status=0x00000000
Worker stopped orderly=true

인증 사용자 전용 변수를 읽으면 응답은 정상이지만 항목 결과가 BadUserAccessDenied이고 종료 코드는 1입니다.

터미널 창
build/opcua_qt_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" 1003
Read ns=2;i=1003 value=none service=0x00000000 status=0x801f0000
Worker stopped orderly=true

신뢰할 서버 인증서를 잘못 넘기면 연결 단계에서 작업자가 실패합니다.

터미널 창
build/opcua_qt_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/client.der"
Error: worker failed (SDK status 5): OPC UA security CertificateUntrusted
Worker stopped orderly=false

failed 신호의 phase는 실패한 위치를 알려 줍니다.

phase 의미 작업자
begin begin이 요청을 거부해 보내지 않았습니다. PDCOMM_OPCUA_INVALID_INPUT이나 PDCOMM_OPCUA_LIMIT이면 그 요청만 실패하고 계속 동작합니다.
response 요청을 보냈지만 서버가 응답을 중단했거나 응답이 잘못되었습니다. 그 요청만 실패합니다. 잘못된 응답은 연결도 닫으므로 이어서 worker 실패로 끝납니다.
abort 요청이 끝나기 전에 작업자가 멈췄습니다. 남은 요청마다 옵니다.
worker 연결, 활성화, 입출력, 종료가 실패했습니다. 상관 번호는 0입니다. 끝납니다. stopped의 reason은 native-error입니다.

서버 터미널에서 Ctrl+C를 누릅니다. 서버가 멈춘 뒤 클라이언트를 실행하면 연결 거부로 끝납니다.

Error: worker failed (SDK status 5): OPC UA I/O: connection refused (os error 61)
Worker stopped orderly=false