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의 인증서. 터미널 두 개.
1. 패키지 풀고 빌드하기
섹션 제목: “1. 패키지 풀고 빌드하기”실습 폴더에서 qt 패키지를 풀고 SDK 위치를 환경 변수로 지정합니다.
tar -xzf pdcomm-opcua-qt-darwin-arm64.tar.gzexport 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 Releasebuild/opcua_qt_client와 C·C++ 예제가 생깁니다. pdcomm_opcua_qt.hpp는 QObject를 선언하므로 AUTOMOC가 처리하도록 빌드 대상의 소스 목록에 넣어야 합니다. 예제의 CMakeLists.txt가 그렇게 합니다.
2. 코드 읽기
섹션 제목: “2. 코드 읽기”// 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;}- 구성.
qt_client_configuration에 endpoint URL, 인증서, 개인 키, 신뢰할 서버 인증서를 넣습니다.anonymous를 참으로 하지 않으면user_name과user_secret으로 활성화합니다. 기본 제한 시간은 5000 ms입니다. - 작업자 생성. 생성자가 구성을 복사하므로 호출자의 키를 바로 지웁니다.
- 신호 연결. 모든 신호를 수신 객체(
receiver)와Qt::QueuedConnection으로 연결합니다. 처리기는 수신 객체의 스레드(여기서는 주 스레드)에서 실행되고, 수신 객체가 사라지면 Qt가 연결을 끊습니다. ready. 세션이 활성화되면 옵니다. 예제는 여기서 ReadRequest를qt_client_worker::submit으로 보냅니다. 두 번째 인자는 응답과 짝을 맞출 상관 번호(correlation)입니다.completed. 응답이 오면 상관 번호, 요청 ID, 응답 정보, 응답 버퍼를 받습니다. 응답은 ServiceFault일 수 있으므로metadata.service_status와 본문의 항목 결과를 확인합니다.- 종료 요청.
qt_client_worker::request_stop에drain을 주면 받은 요청을 모두 끝낸 뒤 세션과 채널을 닫습니다.abort는 남은 작업을 버리고 프로토콜 종료 없이 끝냅니다. stopped. 항상 마지막 신호입니다.orderly가 참이면 drain이 CloseSession까지 마쳤다는 뜻입니다.- 정리. 작업자를
wait()로 기다린 뒤 해제합니다. 작업자 스레드 안에서 작업자를 지우지 않습니다.
submit과 request_stop은 어느 스레드에서 불러도 됩니다. 다른 멤버 함수는 작업자를 소유한 스레드에서만 부릅니다.
3. 서버 실행
섹션 제목: “3. 서버 실행”첫 번째 터미널에서 같은 패키지의 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:48404. Qt 클라이언트 실행
섹션 제목: “4. Qt 클라이언트 실행”두 번째 터미널에서 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=0x00000000Worker stopped orderly=true인증 사용자 전용 변수를 읽으면 응답은 정상이지만 항목 결과가 BadUserAccessDenied이고 종료 코드는 1입니다.
build/opcua_qt_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" 1003Read ns=2;i=1003 value=none service=0x00000000 status=0x801f0000Worker stopped orderly=true5. 실패 확인
섹션 제목: “5. 실패 확인”신뢰할 서버 인증서를 잘못 넘기면 연결 단계에서 작업자가 실패합니다.
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 CertificateUntrustedWorker stopped orderly=falsefailed 신호의 phase는 실패한 위치를 알려 줍니다.
phase |
의미 | 작업자 |
|---|---|---|
begin |
begin이 요청을 거부해 보내지 않았습니다. |
PDCOMM_OPCUA_INVALID_INPUT이나 PDCOMM_OPCUA_LIMIT이면 그 요청만 실패하고 계속 동작합니다. |
response |
요청을 보냈지만 서버가 응답을 중단했거나 응답이 잘못되었습니다. | 그 요청만 실패합니다. 잘못된 응답은 연결도 닫으므로 이어서 worker 실패로 끝납니다. |
abort |
요청이 끝나기 전에 작업자가 멈췄습니다. | 남은 요청마다 옵니다. |
worker |
연결, 활성화, 입출력, 종료가 실패했습니다. 상관 번호는 0입니다. | 끝납니다. stopped의 reason은 native-error입니다. |
6. 정리
섹션 제목: “6. 정리”서버 터미널에서 Ctrl+C를 누릅니다. 서버가 멈춘 뒤 클라이언트를 실행하면 연결 거부로 끝납니다.
Error: worker failed (SDK status 5): OPC UA I/O: connection refused (os error 61)Worker stopped orderly=false