客户端
创建连接
#include <open62541/client.h>
#include <open62541/types.h>
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <open62541/client_subscriptions.h>
#include <open62541/plugin/log_stdout.h>
int main()
{
UA_Client *client = UA_Client_new();
UA_ClientConfig *cc = UA_Client_getConfig(client);
char* serverurl = "opc.tcp://localhost:4840";
UA_StatusCode retval = UA_Client_connect(client, serverurl);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Could not connect");
UA_Client_delete(client);
return EXIT_SUCCESS;
}
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Connected!");
}
获取服务端节点
size_t endpointsSize = 0;
UA_EndpointDescription* endpoints = nullptr;
status = UA_Client_getEndpoints(client, endpointUrl.c_str(), &endpointsSize, &endpoints);
if (status != UA_STATUSCODE_GOOD || !endpoints) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "get end points fail...%d", status);
return false;
}
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "found %zu points:", endpointsSize);
for (size_t i = 0; i < endpointsSize; i++) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "point %zu:", i);
// 安全策略URI
std::string securityPolicy((char*)endpoints[i].securityPolicyUri.data, endpoints[i].securityPolicyUri.length);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, " SecurityPolicy: %s", securityPolicy.c_str());
// 安全模式
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, " SecurityMode: %d", endpoints[i].securityMode);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, " UserTokenTypes:");
for (size_t j = 0; j < endpoints[i].userIdentityTokensSize; j++) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, " - Type: %d", endpoints[i].userIdentityTokens[j].tokenType);
// 策略ID
std::string policyId((char*)endpoints[i].userIdentityTokens[j].policyId.data, endpoints[i].userIdentityTokens[j].policyId.length);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, " - PolicyId: %s", policyId.c_str());
}
}
UA_Array_delete(endpoints, endpointsSize, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
使用用户名与密码连接
明文连接,服务端没有设置加密
UA_Client_connectUsername(client,endUrl, username, password);
安全模式
有5种安全模式
typedef enum {
UA_MESSAGESECURITYMODE_INVALID = 0,
UA_MESSAGESECURITYMODE_NONE = 1,
UA_MESSAGESECURITYMODE_SIGN = 2,
UA_MESSAGESECURITYMODE_SIGNANDENCRYPT = 3,
__UA_MESSAGESECURITYMODE_FORCE32BIT = 0x7fffffff
} UA_MessageSecurityMode;
设置
config->securityMode = UA_MESSAGESECURITYMODE_NONE;
-
UA_MESSAGESECURITYMODE_INVALID
- 表示无效的安全模式。它通常用于初始化变量或者作为一种默认的、未被正确设置的状态标识。当程序中某个用于存储安全模式的变量的值是这个枚举值时,说明该安全模式没有被正确配置,可能是由于配置错误、未初始化,或者在处理安全模式相关逻辑时出现了异常情况,导致安全模式处于无效状态。在实际的通信建立过程中,如果检测到使用了这个无效的安全模式,连接操作大概率会失败。
-
UA_MESSAGESECURITYMODE_NONE
- 代表无安全模式。在这种模式下,OPC UA 通信过程中不会对消息进行任何加密或签名处理。消息以明文的形式在网络中传输,发送方不会对消息进行数字签名来保证其完整性和来源真实性,接收方也不会对消息进行加密验证。这种模式一般只适用于非常安全的测试环境,比如在完全信任的本地局域网内,且通信内容不涉及任何敏感信息,仅用于快速验证通信功能是否正常的场景。因为缺乏安全保护,在实际生产环境,尤其是涉及到工业控制、数据隐私等领域时,不建议使用该模式。
-
UA_MESSAGESECURITYMODE_SIGN
- 表示签名模式。在这种安全模式下,发送方会使用特定的加密算法(根据所采用的安全策略而定)对消息进行数字签名。数字签名能够保证消息在传输过程中不被篡改,因为任何对消息内容的修改都会导致签名验证失败;同时也能确认消息的来源,接收方可以通过验证签名来确定消息确实是由合法的发送方发出的。不过,需要注意的是,此模式下消息内容本身并没有被加密,第三方仍然有可能截获并读取消息的明文内容。它适用于对消息完整性和来源验证有要求,但对消息内容保密性要求不高的场景,例如一些公开的设备状态监控信息的传输。
-
UA_MESSAGESECURITYMODE_SIGNANDENCRYPT
- 这是签名并加密模式,是提供最高安全级别的模式。在该模式下,发送方首先会对消息进行数字签名,以确保消息的完整性和来源真实性,其原理和UA_MESSAGESECURITYMODE_SIGN模式中的签名机制相同。然后,会使用特定的加密算法(同样取决于所采用的安全策略,如 AES 等)对整个消息(包括签名部分)进行加密处理。加密后的消息在网络上传输,只有拥有正确解密密钥的接收方才能将其还原为原始消息并进行签名验证。这种模式适用于传输敏感数据的场景,比如工业控制系统中的控制指令、涉及用户隐私或商业机密的数据等,能有效防止数据被窃取和篡改。
-
__UA_MESSAGESECURITYMODE_FORCE32BIT
- 这并不是一种真正意义上的安全模式,而是一种用于确保枚举类型占用 32 位空间的特殊定义。在 C/C++ 中,枚举类型占用的空间大小由编译器决定,通过定义这个较大值的枚举成员,可以强制编译器为UA_MessageSecurityMode枚举类型分配 32 位的存储空间。这在一些需要与其他代码或系统进行精确的数据交互,且对数据类型的字节大小有严格要求的场景下非常重要,能保证枚举值在不同环境下的兼容性和一致性,避免因枚举类型大小不一致而导致的数据读取或传递错误。
安全策略
config->securityPolicyUri = UA_STRING("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
// "http://opcfoundation.org/UA/SecurityPolicy#None";
// "http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15"; 采用 128 位 RSA 算法进行签名
// "http://opcfoundation.org/UA/SecurityPolicy#Basic256"; 在 Basic128Rsa15 的基础上,增加了 128 位 AES(Advanced Encryption Standard)加密算法对数据进行加密。
// "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"; 使用 256 位 RSA 进行签名,256 位 AES 进行加密,并且签名过程采用了更安全的 SHA - 256 哈希算法。相比 Basic256 策略,它进一步增强了安全性,提高了抵抗密码分析和攻击的能力
加载证书
// 官方示例文件
static UA_INLINE UA_ByteString
loadFile(const char* const path) {
UA_ByteString fileContents = UA_STRING_NULL;
/* Open the file */
FILE* fp = fopen(path, "rb");
if (!fp) {
errno = 0; /* We read errno also from the tcp layer... */
return fileContents;
}
/* Get the file length, allocate the data and read */
fseek(fp, 0, SEEK_END);
fileContents.length = (size_t)ftell(fp);
fileContents.data = (UA_Byte*)UA_malloc(fileContents.length * sizeof(UA_Byte));
if (fileContents.data) {
fseek(fp, 0, SEEK_SET);
size_t read = fread(fileContents.data, sizeof(UA_Byte), fileContents.length, fp);
if (read != fileContents.length)
UA_ByteString_clear(&fileContents);
}
else {
fileContents.length = 0;
}
fclose(fp);
return fileContents;
}
UA_ByteString certificate = loadFile("client_cert.der");
UA_ByteString privateKey = loadFile("client_key.der");
UA_ByteString trustList[1];
trustList[0] = loadFile("server.der");
size_t list_size = 1;
UA_ClientConfig_setDefaultEncryption(config,
certificate, privateKey,
trustList, list_size,
NULL, 0);
使用openssl 生成证书与密钥
# 首先生成一个 2048 位的 RSA 私钥(PEM 格式)
openssl genrsa -out client_key.pem 2048
# 生成证书签名请求(CSR)
openssl req -new -key client_key.pem -out client_req.csr
# 生成自签名证书(.pem 格式)
openssl x509 -req -days 365 -in client_req.csr -signkey client_key.pem -out client_cert.pem
# 转换为 DER 格式
openssl x509 -in client_cert.pem -outform der -out client_cert.der
# 转换私钥为 DER 格式(PKCS#8 编码)
openssl pkcs8 -topk8 -inform PEM -outform DER -in client_key.pem -out client_key.der -nocrypt
浏览节点
UA_NodeId rootNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ROOTFOLDER);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "开始从根节点递归遍历...");
recursiveBrowse(client, rootNodeId, 0);
// 递归遍历节点
static void recursiveBrowse(UA_Client *client, UA_NodeId nodeId, int depth) {
// 1. 配置浏览请求
UA_BrowseRequest browseReq;
UA_BrowseRequest_init(&browseReq);
// 设置要浏览的节点
browseReq.nodesToBrowse = UA_BrowseDescription_new();
browseReq.nodesToBrowseSize = 1;
browseReq.nodesToBrowse[0].nodeId = nodeId;
// 浏览方向:正向(从父节点到子节点)
browseReq.nodesToBrowse[0].browseDirection = UA_BROWSEDIRECTION_FORWARD;
// 引用类型:全部引用
browseReq.nodesToBrowse[0].referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_REFERENCES);
// 包含 subtypes:是否包含子类型引用
browseReq.nodesToBrowse[0].includeSubtypes = UA_TRUE;
// 节点类筛选:不筛选(返回所有类型节点)
browseReq.nodesToBrowse[0].nodeClassMask = 0;
// 返回信息掩码:返回所有可用信息
browseReq.nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL;
// 2. 发送浏览请求
UA_BrowseResponse browseResp = UA_Client_Service_browse(client, browseReq);
// 3. 处理浏览结果
if(browseResp.responseHeader.serviceResult == UA_STATUSCODE_GOOD) {
for(size_t i = 0; i < browseResp.resultsSize; i++) {
UA_BrowseResult result = browseResp.results[i];
if(result.statusCode != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"浏览节点失败,状态码: 0x%x", result.statusCode);
continue;
}
// 遍历当前节点的所有引用
for(size_t j = 0; j < result.referencesSize; j++) {
UA_ReferenceDescription ref = result.references[j];
// 打印层级缩进
for(int d = 0; d < depth; d++)
printf(" ");
// 打印节点基本信息
printf("- 节点类型: ");
switch(ref.nodeClass) {
case UA_NODECLASS_OBJECT: printf("对象"); break;
case UA_NODECLASS_VARIABLE: printf("变量"); break;
case UA_NODECLASS_METHOD: printf("方法"); break;
case UA_NODECLASS_OBJECTTYPE: printf("对象类型"); break;
case UA_NODECLASS_VARIABLETYPE: printf("变量类型"); break;
case UA_NODECLASS_REFERENCETYPE: printf("引用类型"); break;
case UA_NODECLASS_DATATYPE: printf("数据类型"); break;
case UA_NODECLASS_VIEW: printf("视图"); break;
default: printf("未知");
}
printf(" | 节点ID: ");
printNodeId(&ref.nodeId.nodeId);
// 打印显示名
if(ref.displayName.text.length > 0)
printf(" | 显示名: %s\n", ref.displayName.text.data);
else
printf("\n");
// 递归浏览子节点(只对"对象"类型节点继续深入,避免循环)
if(ref.nodeClass == UA_NODECLASS_OBJECT) {
recursiveBrowse(client, ref.nodeId.nodeId, depth + 1);
}
}
}
} else {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"浏览请求失败,状态码: 0x%x", browseResp.responseHeader.serviceResult);
}
// 4. 清理资源
UA_BrowseRequest_clear(&browseReq);
UA_BrowseResponse_clear(&browseResp);
}
// 打印节点ID信息
static void printNodeId(UA_NodeId *nodeId) {
if(nodeId->identifierType == UA_IDENTIFIERTYPE_NUMERIC) {
printf("(命名空间: %u, ID: %u)", nodeId->namespaceIndex, nodeId->identifier.numeric);
} else if(nodeId->identifierType == UA_IDENTIFIERTYPE_STRING) {
printf("(命名空间: %u, ID: \"%s\")",
nodeId->namespaceIndex,
nodeId->identifier.string.data);
} else if(nodeId->identifierType == UA_IDENTIFIERTYPE_GUID) {
printf("(命名空间: %u, GUID: ...)", nodeId->namespaceIndex); // GUID格式较复杂,简化显示
} else if(nodeId->identifierType == UA_IDENTIFIERTYPE_BYTESTRING) {
printf("(命名空间: %u, 字节串: ...)", nodeId->namespaceIndex);
}
}
获取服务端时间
UA_NodeId serverTimeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
UA_Variant value;
UA_Variant_init(&value);
UA_StatusCode status = UA_Client_readValueAttribute(client, serverTimeNodeId, &value);
if (status != UA_STATUSCODE_GOOD || !UA_Variant_isScalar(&value) ||
value.type != &UA_TYPES[UA_TYPES_DATETIME]) {
UA_Variant_clear(&value);
return {}; // 返回空 SYSTEMTIME 表示失败
}
UA_DateTime dateTime = *(UA_DateTime*)value.data;
UA_DateTimeStruct dt = UA_DateTime_toStruct(dateTime);
订阅与监视
// 全局变量:用于控制程序退出
static volatile UA_Boolean running = UA_TRUE;
// 信号处理函数:捕获Ctrl+C退出信号
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "收到退出信号,正在关闭...");
running = UA_FALSE;
}
// 监视项回调函数:当节点值变化时触发
static void dataChangeNotificationCallback(UA_Client *client,
UA_UInt32 subId,
void *subContext,
UA_UInt32 monId,
void *monContext,
UA_DataValue *value) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "检测到节点值变化:");
// 打印变化后的值
if(value->hasValue) {
UA_Variant *var = &value->value;
if(var->type == &UA_TYPES[UA_TYPES_INT32]) {
UA_Int32 val = *(UA_Int32*)var->data;
printf(" 整数值: %d\n", val);
} else if(var->type == &UA_TYPES[UA_TYPES_DOUBLE]) {
UA_Double val = *(UA_Double*)var->data;
printf(" 浮点值: %.2f\n", val);
} else if(var->type == &UA_TYPES[UA_TYPES_STRING]) {
UA_String val = *(UA_String*)var->data;
printf(" 字符串值: %.*s\n", (int)val.length, val.data);
} else {
printf(" 未知类型值\n");
}
}
}
void createSubscription()
{
UA_CreateSubscriptionRequest subRequest = UA_CreateSubscriptionRequest_default();
subRequest.requestedPublishingInterval = 100; // 发布间隔(毫秒)
subRequest.requestedLifetimeCount = 1000;
subRequest.requestedMaxKeepAliveCount = 10;
UA_CreateSubscriptionResponse subResponse =
UA_Client_Subscription_create(client, subRequest, NULL, NULL, NULL);
if(subResponse.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "创建订阅失败");
UA_Client_disconnect(client);
UA_Client_delete(client);
return EXIT_FAILURE;
}
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"订阅创建成功,订阅ID: %u", subResponse.subscriptionId);
}
bool monitoredItem(UA_UInt32& subscriptionId, UA_NodeId& nodeId)
{
try {
UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default(nodeId);
monRequest.requestedParameters.samplingInterval = 1000; // 采样间隔1秒
monRequest.requestedParameters.queueSize = 10;
monRequest.requestedParameters.discardOldest = true;
void* context = reinterpret_cast<void*>(&nodeId); // 用于回调识别节点
UA_MonitoredItemCreateResult result =
UA_Client_MonitoredItems_createDataChange(
client, subscriptionId,
UA_TIMESTAMPSTORETURN_BOTH,
monRequest, context,
CallBackFunction::dataChangeCallback, NULL);
if (result.statusCode != UA_STATUSCODE_GOOD) {
cout << "Failed to create monitoring item for node: " << endl;
}
// 保存订阅ID与节点、列表索引之间的映射
m_subscriptionMap[result.monitoredItemId] = nodeId;
}
catch (_com_error e) {
return false;
}
return true;
}