C++ 클라이언트와 서버
pdcomm_opcua.hpp의 RAII 클래스로 서버를 만들고 변수 세 개를 공개합니다.- 같은 헤더로 클라이언트를 만들어 값을 쓰고 읽습니다.
- 오류가 예외로 전달되는 방식을 확인합니다.
준비물
섹션 제목: “준비물”- 실습 환경을 마친 실습 폴더:
native패키지, 환경 변수,$PKI의 인증서, 빌드한 예제 - C++17 컴파일러. 터미널 두 개.
C++ 래퍼의 구성
섹션 제목: “C++ 래퍼의 구성”pdcomm_opcua.hpp는 C ABI를 감싸는 헤더 전용 래퍼입니다. 별도 라이브러리가 없고 같은 pdcomm_opcua_ffi 라이브러리를 씁니다.
| 클래스·함수 | 역할 |
|---|---|
options, server_options |
ABI 버전을 확인하고 기본값을 채운 옵션 구조체를 돌려줍니다. |
client |
클라이언트 핸들을 소유합니다. connect, activate_anonymous, begin, poll, take, close를 제공합니다. |
server |
서버 핸들을 소유합니다. create, add_variable, set_value, read_value, local_address, poll, close를 제공합니다. |
response, buffer |
응답과 버퍼 핸들을 소유합니다. copy()로 바이트를, info()로 응답 정보를 얻습니다. |
error |
C ABI 호출이 실패하면 던지는 예외입니다. status()가 C ABI 상태, what()이 오류 문장입니다. |
모든 핸들은 객체가 사라질 때 자동으로 해제됩니다. 객체는 복사할 수 없고 이동만 할 수 있습니다.
1. 서버 코드 읽기
섹션 제목: “1. 서버 코드 읽기”// PDComm OPC UA C++ server example using the header-only RAII wrapper.// Hosts ns=2;i=1001 Value (public, writable), ns=2;i=1002 Limit (public,// read-only) and ns=2;i=1003 Protected (authenticated only) until Ctrl+C,// SIGTERM or the optional --seconds limit.#include <pdcomm/opcua/pdcomm_opcua.hpp>#include <algorithm>#include <chrono>#include <csignal>#include <fstream>#include <iostream>#include <iterator>#include <string>#include <thread>#include <vector>#include "example_io.h"#include "ua_binary.h"
namespace ua = pdcomm::opcua;static volatile std::sig_atomic_t stop_requested = 0;extern "C" void on_signal(int) { stop_requested = 1; }
static std::vector<uint8_t> load(const std::string& path) { std::ifstream file(path, std::ios::binary); std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()}; if (data.empty() || data.size() > EXAMPLE_MAX_FILE) throw std::runtime_error("Invalid file " + path); return data;}static std::vector<uint8_t> node_id(uint32_t id) { uint8_t out[7]; return {out, out + ua_encode_node_id(out, 2, id)};}static std::vector<uint8_t> int32_variant(int32_t value) { uint8_t out[5]; return {out, out + ua_encode_int32_variant(out, value)};}
int main(int argc, char** argv) { unsigned long seconds = 0; if (argc != 6 && !(argc == 8 && std::string(argv[6]) == "--seconds")) { std::cerr << "Usage: opcua_server_cpp BIND_ADDRESS ENDPOINT_URL SERVER_CERT_DER SERVER_KEY_PEM " "TRUSTED_CLIENT_DER [--seconds N]\n"; return 2; } try { if (argc == 8) seconds = std::stoul(argv[7]); } catch (const std::exception&) { seconds = 0; } if (argc == 8 && (seconds == 0 || seconds > 86400)) { std::cerr << "Error: --seconds must be between 1 and 86400\n"; return 2; } try { const std::string endpoint = argv[2]; const auto certificate = load(argv[3]); auto key = load(argv[4]); const auto trust = load(argv[5]); auto config = ua::server_options(); config.endpoint = ua::bytes(endpoint); config.certificate_der = ua::bytes(certificate); config.private_key_pem = ua::bytes(key); config.trusted_client_der = ua::bytes(trust); auto server = ua::server::create(argv[1], config); std::fill(key.begin(), key.end(), static_cast<unsigned char>(0)); // The SDK copied the identity. server.add_variable(node_id(1001), "Value", int32_variant(42), PDCOMM_OPCUA_VARIABLE_WRITABLE | PDCOMM_OPCUA_VARIABLE_PUBLIC); server.add_variable(node_id(1002), "Limit", int32_variant(100), PDCOMM_OPCUA_VARIABLE_PUBLIC); server.add_variable(node_id(1003), "Protected", int32_variant(7), PDCOMM_OPCUA_VARIABLE_WRITABLE); const auto address = server.local_address().copy(); // Install the stop handlers before announcing readiness. std::signal(SIGINT, on_signal); std::signal(SIGTERM, on_signal);#ifdef SIGBREAK std::signal(SIGBREAK, on_signal); // Windows Ctrl+Break, e.g. from a service manager#endif std::cout << "Listening on " << std::string(address.begin(), address.end()) << std::endl; const auto started = std::chrono::steady_clock::now(); while (!stop_requested && (!seconds || std::chrono::steady_clock::now() - started < std::chrono::seconds(seconds))) { server.poll(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } const auto snapshot = server.read_value(node_id(1001)).copy(); ua_value value; if (ua_decode_data_value(snapshot.data(), snapshot.size(), &value) || !value.has_int32) throw std::runtime_error("Unexpected DataValue"); server.close(); std::cout << "Final Value=" << value.int32 << "\nServer stopped" << std::endl; return 0; } catch (const ua::error& e) { std::cerr << "Error: SDK status " << e.status() << ": " << e.what() << std::endl; return 3; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 3; }}ua::server_options()로 옵션을 만들고 endpoint URL, 서버 인증서, 개인 키, 신뢰할 클라이언트 인증서를 넣습니다. 옵션을 넘기지 않은 플래그는 0이므로 보안 엔드포인트만 광고합니다.ua::server::create가 주소에 바인드합니다. 개인 키는 이때 복사되므로 바로 지웁니다.add_variable의 NodeId와 값은 UA Binary로 인코딩한 바이트입니다. 예제의node_id(1001)은ns=2;i=1001,int32_variant(42)는 Int32 Variant 42입니다.PDCOMM_OPCUA_VARIABLE_PUBLIC이 없는 변수는 인증된 사용자만 접근합니다.- 서버는
poll을 부를 때만 클라이언트를 처리합니다. 예제는 1 ms마다 부릅니다. - 종료할 때
read_value로 현재 DataValue를 읽고close를 부릅니다.
2. 클라이언트 코드 읽기
섹션 제목: “2. 클라이언트 코드 읽기”// PDComm OPC UA C++ client example using the header-only RAII wrapper.// Opens a secure anonymous session, optionally writes an Int32 and reads it.// Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 failure.#include <pdcomm/opcua/pdcomm_opcua.hpp>#include <algorithm>#include <chrono>#include <cstdio>#include <fstream>#include <iostream>#include <iterator>#include <map>#include <string>#include <thread>#include <vector>#include "example_io.h"#include "ua_binary.h"
namespace ua = pdcomm::opcua;
static std::vector<uint8_t> load(const std::string& path) { std::ifstream file(path, std::ios::binary); std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()}; if (!file.good() && !file.eof()) throw std::runtime_error("Cannot read " + path); if (data.empty() || data.size() > EXAMPLE_MAX_FILE) throw std::runtime_error("Invalid file " + path); return data;}
static std::vector<uint8_t> exchange(ua::client& client, const uint8_t* request, size_t size, std::chrono::milliseconds timeout) { const auto id = client.begin(std::vector<uint8_t>(request, request + size)); const auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { client.poll(); if (auto response = client.take(id)) return response->copy(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } throw ua::error(PDCOMM_OPCUA_RUNTIME_ERROR, "Response timed out");}
static std::string hex(uint32_t value) { char text[11]; std::snprintf(text, sizeof(text), "0x%08x", static_cast<unsigned>(value)); return text;}
int main(int argc, char** argv) { if (argc < 5 || (argc - 5) % 2) { std::cerr << "Usage: opcua_client_cpp ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM " "TRUSTED_SERVER_DER [--policy aes256sha256rsapss|aes128sha256rsaoaep|" "basic256sha256] [--mode signandencrypt|sign] [--node ID] [--write VALUE]\n"; return 2; } const std::map<std::string, uint32_t> policies{{"aes256sha256rsapss", PDCOMM_OPCUA_AES256SHA256RSAPSS}, {"aes128sha256rsaoaep", PDCOMM_OPCUA_AES128SHA256RSAOAEP}, {"basic256sha256", PDCOMM_OPCUA_BASIC256SHA256}}; const std::map<std::string, uint32_t> modes{{"signandencrypt", PDCOMM_OPCUA_SIGN_AND_ENCRYPT}, {"sign", PDCOMM_OPCUA_SIGN}}; uint32_t policy = PDCOMM_OPCUA_AES256SHA256RSAPSS, mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT, node = 1001; bool write = false; int32_t write_value = 0; try { for (int i = 5; i < argc; i += 2) { const std::string name = argv[i], text = argv[i + 1]; if (name == "--policy" && policies.count(text)) policy = policies.at(text); else if (name == "--mode" && modes.count(text)) mode = modes.at(text); else if (name == "--node") node = static_cast<uint32_t>(std::stoul(text)); else if (name == "--write") { write = true; write_value = static_cast<int32_t>(std::stol(text)); } else throw std::invalid_argument(name); } } catch (const std::exception&) { std::cerr << "Error: invalid option\n"; return 2; } try { auto config = ua::options(); const auto certificate = load(argv[2]); auto key = load(argv[3]); const auto trust = load(argv[4]); config.policy = policy; config.mode = mode; config.timeout_ms = 5000; config.certificate_der = ua::bytes(certificate); config.private_key_pem = ua::bytes(key); config.trusted_server_der = ua::bytes(trust); auto client = ua::client::connect(argv[1], config); std::fill(key.begin(), key.end(), static_cast<unsigned char>(0)); // The SDK copied the identity. client.activate_anonymous(); std::cout << "Session activated (anonymous)" << std::endl; int result = 0; uint8_t request[256]; uint32_t service = 0, status = 0; if (write) { const auto size = ua_encode_write_int32(request, sizeof(request), 2, node, write_value); const auto body = exchange(client, request, size, std::chrono::seconds(5)); if (ua_decode_write(body.data(), body.size(), &service, &status)) throw std::runtime_error("Malformed WriteResponse"); std::cout << "Write ns=2;i=" << node << " value=" << write_value << " service=" << hex(service) << " status=" << hex(status) << std::endl; if ((service | status) & 0x80000000u) result = 1; } const auto size = ua_encode_read(request, sizeof(request), 2, node); const auto body = exchange(client, request, size, std::chrono::seconds(5)); ua_value value; if (ua_decode_read(body.data(), body.size(), &service, &value)) throw std::runtime_error("Malformed ReadResponse"); std::cout << "Read ns=2;i=" << node << " value=" << (value.has_int32 ? std::to_string(value.int32) : std::string("none")) << " service=" << hex(service) << " status=" << hex(value.status) << std::endl; if ((service | value.status) & 0x80000000u) result = 1; client.close(); std::cout << "Session closed" << std::endl; return result; } catch (const ua::error& e) { std::cerr << "Error: SDK status " << e.status() << ": " << e.what() << std::endl; return 3; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 3; }}ua::client::connect가 보안 채널과 세션을 엽니다. 반환된 객체가 클라이언트를 소유합니다.exchange는begin으로 요청을 넣고,poll과take를 반복합니다.take는 응답이 없으면 빈std::optional을 돌려줍니다.- C ABI 오류는 모두
ua::error로 옵니다. 예제는Error: SDK status ...로 출력하고 종료 코드 3을 돌려줍니다.
3. 서버 실행
섹션 제목: “3. 서버 실행”첫 번째 터미널에서 C++ 서버를 실행합니다.
cd "$OPCUA_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정해진 시간 뒤에 끝내려면 마지막에 --seconds N을 붙입니다. 포트 0을 쓰면 운영 체제가 빈 포트를 고르고, 실제 주소가 Listening on 줄에 나옵니다.
4. 값 쓰고 읽기
섹션 제목: “4. 값 쓰고 읽기”두 번째 터미널에서 C++ 클라이언트로 값 11을 씁니다.
cd "$OPCUA_SDK"build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --write 11Session activated (anonymous)Write ns=2;i=1001 value=11 service=0x00000000 status=0x00000000Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000Session closedAes128_Sha256_RsaOaep 정책과 Sign 모드로 다시 읽습니다.
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy aes128sha256rsaoaep --mode signSession activated (anonymous)Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000Session closed읽기 전용 변수에 쓰면 항목 결과가 BadNotWritable이고 종료 코드는 1입니다.
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --node 1002 --write 5Session activated (anonymous)Write ns=2;i=1002 value=5 service=0x00000000 status=0x803b0000Read ns=2;i=1002 value=100 service=0x00000000 status=0x00000000Session closed5. 예외 확인
섹션 제목: “5. 예외 확인”신뢰할 서버 인증서 자리에 잘못된 인증서(client.der)를 넘깁니다.
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/client.der"Error: SDK status 5: OPC UA security CertificateUntrustedua::client::connect가 ua::error를 던졌고, status()는 5(PDCOMM_OPCUA_RUNTIME_ERROR)입니다. 오류 문장은 기록과 사람이 읽는 용도로만 쓰고, 프로그램의 분기는 status()로 판단합니다.
C++ 예제는 --policy에 aes256sha256rsapss, aes128sha256rsaoaep, basic256sha256만 받습니다. none을 주면 Error: invalid option을 출력하고 종료 코드 2로 끝납니다.
6. 정리
섹션 제목: “6. 정리”서버 터미널에서 Ctrl+C를 누릅니다. 서버는 SIGINT, SIGTERM, Windows의 Ctrl+Break에서 멈춥니다.
Final Value=11Server stopped다음 단계
섹션 제목: “다음 단계”- Qt 클라이언트 작업자: UI 응용 프로그램에서 같은 클라이언트를 별도 스레드로 실행합니다.
- C++ API 레퍼런스