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

C++ 클라이언트와 서버

  • pdcomm_opcua.hpp의 RAII 클래스로 서버를 만들고 변수 세 개를 공개합니다.
  • 같은 헤더로 클라이언트를 만들어 값을 쓰고 읽습니다.
  • 오류가 예외로 전달되는 방식을 확인합니다.
  • 실습 환경을 마친 실습 폴더: native 패키지, 환경 변수, $PKI의 인증서, 빌드한 예제
  • C++17 컴파일러. 터미널 두 개.

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()이 오류 문장입니다.

모든 핸들은 객체가 사라질 때 자동으로 해제됩니다. 객체는 복사할 수 없고 이동만 할 수 있습니다.

examples/opcua/opcua_server.cpp
// 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를 부릅니다.
examples/opcua/opcua_client.cpp
// 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을 돌려줍니다.

첫 번째 터미널에서 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 줄에 나옵니다.

두 번째 터미널에서 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 11
Session activated (anonymous)
Write ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Session closed

Aes128_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 sign
Session activated (anonymous)
Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Session 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 5
Session activated (anonymous)
Write ns=2;i=1002 value=5 service=0x00000000 status=0x803b0000
Read ns=2;i=1002 value=100 service=0x00000000 status=0x00000000
Session closed

신뢰할 서버 인증서 자리에 잘못된 인증서(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 CertificateUntrusted

ua::client::connect가 ua::error를 던졌고, status()는 5(PDCOMM_OPCUA_RUNTIME_ERROR)입니다. 오류 문장은 기록과 사람이 읽는 용도로만 쓰고, 프로그램의 분기는 status()로 판단합니다.

C++ 예제는 --policy에 aes256sha256rsapss, aes128sha256rsaoaep, basic256sha256만 받습니다. none을 주면 Error: invalid option을 출력하고 종료 코드 2로 끝납니다.

서버 터미널에서 Ctrl+C를 누릅니다. 서버는 SIGINT, SIGTERM, Windows의 Ctrl+Break에서 멈춥니다.

Final Value=11
Server stopped