OPC UA协议栈开源项目分析和核心代码实现
一、开源 OPC UA 协议栈选择
1.1 主要开源实现对比
| 项目 | 语言 | 许可证 | 特点 | 活跃度 |
|---|---|---|---|---|
| open62541 | C/C++ | MPL-2.0 | 最完整,工业级 | ★★★★★ |
| FreeOpcUa | C++/Python | LGPL | 功能丰富,Python绑定 | ★★★★☆ |
| UA-.NET | C# | MIT | .NET平台,微软支持 | ★★★★☆ |
| libopcua | C | LGPL | 轻量级,适合嵌入式 | ★★★☆☆ |
推荐 :open62541 - 最成熟的开源实现,符合OPC UA Part 4-6标准。
二、open62541 核心架构
2.1 项目结构
open62541/
├── src/ # 核心源代码
│ ├── client/ # 客户端实现
│ ├── server/ # 服务器实现
│ ├── plugin/ # 插件系统
│ ├── core/ # 核心模块
│ │ ├── types.h # 数据类型定义
│ │ ├── message.h # 消息编码
│ │ ├── transport.h # 传输层
│ │ ├── security.h # 安全模块
│ │ └── subscription.h # 订阅管理
├── examples/ # 示例代码
├── tests/ # 测试代码
└── plugins/ # 可选插件
三、核心模块 C 语言实现
3.1 基本数据类型定义
c
/**
* @file opcua_types.h
* @brief OPC UA 基本数据类型定义
*/
#ifndef OPCUA_TYPES_H
#define OPCUA_TYPES_H
#include <stdint.h>
#include <stdbool.h>
/* 基本数据类型 */
typedef uint8_t UA_Byte;
typedef int8_t UA_SByte;
typedef uint16_t UA_UInt16;
typedef int16_t UA_Int16;
typedef uint32_t UA_UInt32;
typedef int32_t UA_Int32;
typedef uint64_t UA_UInt64;
typedef int64_t UA_Int64;
typedef float UA_Float;
typedef double UA_Double;
typedef bool UA_Boolean;
typedef char* UA_String;
/* 扩展数据类型 */
typedef struct {
UA_UInt32 length;
UA_Byte *data;
} UA_ByteString;
typedef struct {
UA_UInt32 length;
UA_UInt16 *data;
} UA_String;
/* 节点ID类型 */
typedef enum {
UA_NODEIDTYPE_NUMERIC = 0,
UA_NODEIDTYPE_STRING = 1,
UA_NODEIDTYPE_GUID = 2,
UA_NODEIDTYPE_BYTESTRING = 3
} UA_NodeIdType;
typedef struct {
UA_NodeIdType identifierType;
UA_UInt16 namespaceIndex;
union {
UA_UInt32 numeric;
UA_String string;
UA_ByteString byteString;
/* GUID 省略简化 */
} identifier;
} UA_NodeId;
/* 扩展对象节点ID */
#define UA_NODEID_NUMERIC(ns, id) {UA_NODEIDTYPE_NUMERIC, ns, {.numeric = id}}
#define UA_NODEID_STRING(ns, str) {UA_NODEIDTYPE_STRING, ns, {.string = str}}
/* 变体类型 */
typedef enum {
UA_VARIANT_DATA_NULL = 0,
UA_VARIANT_DATA_BOOLEAN,
UA_VARIANT_DATA_SBYTE,
UA_VARIANT_DATA_BYTE,
UA_VARIANT_DATA_INT16,
UA_VARIANT_DATA_UINT16,
UA_VARIANT_DATA_INT32,
UA_VARIANT_DATA_UINT32,
UA_VARIANT_DATA_INT64,
UA_VARIANT_DATA_UINT64,
UA_VARIANT_DATA_FLOAT,
UA_VARIANT_DATA_DOUBLE,
UA_VARIANT_DATA_STRING,
UA_VARIANT_DATA_DATETIME,
UA_VARIANT_DATA_GUID,
UA_VARIANT_DATA_BYTESTRING,
UA_VARIANT_DATA_ARRAY
} UA_VariantType;
typedef struct {
UA_VariantType type;
union {
UA_Boolean boolean;
UA_SByte sbyte;
UA_Byte byte;
UA_Int16 int16;
UA_UInt16 uint16;
UA_Int32 int32;
UA_UInt32 uint32;
UA_Int64 int64;
UA_UInt64 uint64;
UA_Float floatVal;
UA_Double doubleVal;
UA_String string;
UA_ByteString byteString;
struct {
void *data;
UA_UInt32 size;
} array;
} data;
} UA_Variant;
/* 数据值结构 */
typedef struct {
UA_Variant value;
UA_Byte status;
UA_UInt64 sourceTimestamp;
UA_UInt16 sourcePicoseconds;
UA_UInt64 serverTimestamp;
UA_UInt16 serverPicoseconds;
} UA_DataValue;
#endif /* OPCUA_TYPES_H */
3.2 消息编码/解码
c
/**
* @file opcua_binary.h
* @brief OPC UA 二进制编码实现
*/
#include "opcua_types.h"
#include <string.h>
/* 消息头定义 */
typedef struct {
UA_UInt32 messageType; // 消息类型 "HEL" "ACK" "ERR" "MSG"
UA_UInt32 messageSize; // 包括头部的总大小
UA_UInt32 chunkType; // 分块类型
} UA_MessageHeader;
/* 编码上下文 */
typedef struct {
UA_Byte *buffer;
UA_UInt32 position;
UA_UInt32 capacity;
} UA_Encoder;
typedef struct {
const UA_Byte *buffer;
UA_UInt32 position;
UA_UInt32 length;
} UA_Decoder;
/* 编码函数 */
UA_StatusCode UA_Encoder_init(UA_Encoder *enc, UA_Byte *buffer, UA_UInt32 capacity) {
if(!buffer || capacity == 0)
return UA_STATUSCODE_BADINTERNALERROR;
enc->buffer = buffer;
enc->position = 0;
enc->capacity = capacity;
return UA_STATUSCODE_GOOD;
}
/* 编码基本类型 */
void UA_Encoder_writeByte(UA_Encoder *enc, UA_Byte value) {
if(enc->position < enc->capacity) {
enc->buffer[enc->position++] = value;
}
}
void UA_Encoder_writeUInt16(UA_Encoder *enc, UA_UInt16 value) {
UA_Encoder_writeByte(enc, (UA_Byte)(value & 0xFF));
UA_Encoder_writeByte(enc, (UA_Byte)((value >> 8) & 0xFF));
}
void UA_Encoder_writeUInt32(UA_Encoder *enc, UA_UInt32 value) {
UA_Encoder_writeByte(enc, (UA_Byte)(value & 0xFF));
UA_Encoder_writeByte(enc, (UA_Byte)((value >> 8) & 0xFF));
UA_Encoder_writeByte(enc, (UA_Byte)((value >> 16) & 0xFF));
UA_Encoder_writeByte(enc, (UA_Byte)((value >> 24) & 0xFF));
}
void UA_Encoder_writeInt32(UA_Encoder *enc, UA_Int32 value) {
UA_Encoder_writeUInt32(enc, (UA_UInt32)value);
}
/* 编码字符串 */
void UA_Encoder_writeString(UA_Encoder *enc, const UA_String *str) {
if(!str || str->length == 0) {
UA_Encoder_writeUInt32(enc, 0xFFFFFFFF); // 空字符串
return;
}
UA_Encoder_writeUInt32(enc, str->length);
for(UA_UInt32 i = 0; i < str->length; i++) {
UA_Encoder_writeByte(enc, (UA_Byte)str->data[i]);
}
}
/* 编码节点ID */
void UA_Encoder_writeNodeId(UA_Encoder *enc, const UA_NodeId *nodeId) {
UA_Encoder_writeByte(enc, (UA_Byte)nodeId->identifierType);
UA_Encoder_writeUInt16(enc, nodeId->namespaceIndex);
switch(nodeId->identifierType) {
case UA_NODEIDTYPE_NUMERIC:
UA_Encoder_writeUInt32(enc, nodeId->identifier.numeric);
break;
case UA_NODEIDTYPE_STRING:
UA_Encoder_writeString(enc, &nodeId->identifier.string);
break;
case UA_NODEIDTYPE_BYTESTRING:
UA_Encoder_writeUInt32(enc, nodeId->identifier.byteString.length);
for(UA_UInt32 i = 0; i < nodeId->identifier.byteString.length; i++) {
UA_Encoder_writeByte(enc, nodeId->identifier.byteString.data[i]);
}
break;
default:
// GUID类型简化处理
break;
}
}
/* 解码函数 */
UA_StatusCode UA_Decoder_init(UA_Decoder *dec, const UA_Byte *buffer, UA_UInt32 length) {
if(!buffer || length == 0)
return UA_STATUSCODE_BADINTERNALERROR;
dec->buffer = buffer;
dec->position = 0;
dec->length = length;
return UA_STATUSCODE_GOOD;
}
UA_Byte UA_Decoder_readByte(UA_Decoder *dec) {
if(dec->position < dec->length) {
return dec->buffer[dec->position++];
}
return 0;
}
UA_UInt16 UA_Decoder_readUInt16(UA_Decoder *dec) {
UA_UInt16 value = UA_Decoder_readByte(dec);
value |= (UA_UInt16)UA_Decoder_readByte(dec) << 8;
return value;
}
UA_UInt32 UA_Decoder_readUInt32(UA_Decoder *dec) {
UA_UInt32 value = UA_Decoder_readByte(dec);
value |= (UA_UInt32)UA_Decoder_readByte(dec) << 8;
value |= (UA_UInt32)UA_Decoder_readByte(dec) << 16;
value |= (UA_UInt32)UA_Decoder_readByte(dec) << 24;
return value;
}
UA_StatusCode UA_Decoder_readString(UA_Decoder *dec, UA_String *str) {
UA_UInt32 length = UA_Decoder_readUInt32(dec);
if(length == 0xFFFFFFFF) {
str->length = 0;
str->data = NULL;
return UA_STATUSCODE_GOOD;
}
if(dec->position + length > dec->length) {
return UA_STATUSCODE_BADDECODINGERROR;
}
str->length = length;
str->data = (UA_UInt16*)malloc(length * sizeof(UA_UInt16));
for(UA_UInt32 i = 0; i < length; i++) {
str->data[i] = UA_Decoder_readByte(dec);
}
return UA_STATUSCODE_GOOD;
}
3.3 服务器核心实现
c
/**
* @file opcua_server.h
* @brief OPC UA 服务器核心实现
*/
#ifndef OPCUA_SERVER_H
#define OPCUA_SERVER_H
#include "opcua_types.h"
#include "opcua_binary.h"
/* 服务器配置 */
typedef struct {
UA_UInt16 port;
UA_UInt32 maxConnections;
UA_UInt32 maxWorkerThreads;
UA_Boolean enableSecurity;
UA_String applicationUri;
UA_String productUri;
UA_String applicationName;
} UA_ServerConfig;
/* 节点属性 */
typedef struct {
UA_NodeId nodeId;
UA_NodeClass nodeClass;
UA_QualifiedName browseName;
UA_LocalizedText displayName;
UA_LocalizedText description;
UA_UInt32 writeMask;
UA_UInt32 userWriteMask;
} UA_NodeAttributes;
/* 服务器会话 */
typedef struct {
UA_UInt32 sessionId;
UA_String sessionName;
UA_UInt32 authenticationToken;
UA_UInt64 timeout;
UA_UInt64 validTill;
void *userContext;
} UA_Session;
/* 服务器结构 */
typedef struct {
UA_ServerConfig config;
/* 连接管理 */
struct {
int listenSocket;
UA_UInt32 connectionCount;
void **connections;
} network;
/* 节点管理 */
struct {
UA_UInt32 nodeCount;
UA_NodeAttributes **nodes;
} addressSpace;
/* 会话管理 */
struct {
UA_UInt32 sessionCount;
UA_Session **sessions;
} sessionManager;
/* 订阅管理 */
struct {
UA_UInt32 subscriptionCount;
void **subscriptions;
} subscriptionManager;
/* 回调函数 */
struct {
void (*onRead)(UA_NodeId nodeId, UA_Variant *value);
void (*onWrite)(UA_NodeId nodeId, UA_Variant *value);
void (*onCall)(UA_NodeId methodId, UA_Variant *input, UA_Variant *output);
} callbacks;
} UA_Server;
/* 服务器API */
UA_Server* UA_Server_new(const UA_ServerConfig *config);
UA_StatusCode UA_Server_run(UA_Server *server);
UA_StatusCode UA_Server_stop(UA_Server *server);
void UA_Server_delete(UA_Server *server);
/* 节点管理 */
UA_StatusCode UA_Server_addVariableNode(
UA_Server *server,
const UA_NodeId *parentNodeId,
const UA_NodeId *referenceTypeId,
const UA_QualifiedName *browseName,
const UA_NodeId *typeDefinition,
const UA_VariableAttributes *attr,
void *nodeContext,
UA_NodeId *outNodeId
);
/* 读写操作 */
UA_StatusCode UA_Server_readValue(
UA_Server *server,
const UA_NodeId *nodeId,
UA_Variant *value
);
UA_StatusCode UA_Server_writeValue(
UA_Server *server,
const UA_NodeId *nodeId,
const UA_Variant *value
);
#endif /* OPCUA_SERVER_H */
3.4 服务器实现文件
c
/**
* @file opcua_server.c
* @brief OPC UA 服务器实现
*/
#include "opcua_server.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* 创建新服务器 */
UA_Server* UA_Server_new(const UA_ServerConfig *config) {
UA_Server *server = (UA_Server*)calloc(1, sizeof(UA_Server));
if(!server) return NULL;
memcpy(&server->config, config, sizeof(UA_ServerConfig));
/* 初始化地址空间 */
server->addressSpace.nodes = (UA_NodeAttributes**)calloc(100, sizeof(UA_NodeAttributes*));
server->addressSpace.nodeCount = 0;
/* 初始化会话管理器 */
server->sessionManager.sessions = (UA_Session**)calloc(10, sizeof(UA_Session*));
server->sessionManager.sessionCount = 0;
/* 创建监听socket */
server->network.listenSocket = socket(AF_INET, SOCK_STREAM, 0);
if(server->network.listenSocket < 0) {
free(server);
return NULL;
}
int opt = 1;
setsockopt(server->network.listenSocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(config->port);
if(bind(server->network.listenSocket, (struct sockaddr*)&address, sizeof(address)) < 0) {
close(server->network.listenSocket);
free(server);
return NULL;
}
listen(server->network.listenSocket, 5);
return server;
}
/* 运行服务器 */
UA_StatusCode UA_Server_run(UA_Server *server) {
if(!server) return UA_STATUSCODE_BADINTERNALERROR;
printf("OPC UA Server starting on port %d\n", server->config.port);
socklen_t addrlen = sizeof(struct sockaddr_in);
while(1) {
struct sockaddr_in client_addr;
int client_socket = accept(server->network.listenSocket,
(struct sockaddr*)&client_addr, &addrlen);
if(client_socket < 0) {
continue;
}
printf("New connection from %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
/* 创建新线程处理连接 */
pthread_t thread;
UA_ConnectionContext *ctx = (UA_ConnectionContext*)malloc(sizeof(UA_ConnectionContext));
ctx->server = server;
ctx->socket = client_socket;
ctx->client_addr = client_addr;
pthread_create(&thread, NULL, connection_handler, ctx);
pthread_detach(thread);
}
return UA_STATUSCODE_GOOD;
}
/* 连接处理线程 */
void* connection_handler(void *arg) {
UA_ConnectionContext *ctx = (UA_ConnectionContext*)arg;
UA_Server *server = ctx->server;
int client_socket = ctx->socket;
UA_Byte buffer[4096];
ssize_t bytes_received;
while((bytes_received = recv(client_socket, buffer, sizeof(buffer), 0)) > 0) {
/* 解码消息头 */
UA_MessageHeader header;
UA_Decoder decoder;
UA_Decoder_init(&decoder, buffer, bytes_received);
/* 解析消息类型 */
header.messageType = UA_Decoder_readUInt32(&decoder);
header.messageSize = UA_Decoder_readUInt32(&decoder);
header.chunkType = UA_Decoder_readUInt32(&decoder);
/* 处理不同类型的消息 */
if(header.messageType == 0x4D534748) { // "HEL"
handle_hello_message(server, client_socket, &decoder);
} else if(header.messageType == 0x4D534747) { // "MSG"
handle_request_message(server, client_socket, &decoder);
}
}
close(client_socket);
free(ctx);
return NULL;
}
/* 处理Hello消息 */
void handle_hello_message(UA_Server *server, int socket, UA_Decoder *decoder) {
/* 读取Hello消息内容 */
UA_UInt32 protocolVersion = UA_Decoder_readUInt32(decoder);
UA_UInt32 receiveBufferSize = UA_Decoder_readUInt32(decoder);
UA_UInt32 sendBufferSize = UA_Decoder_readUInt32(decoder);
UA_UInt32 maxMessageSize = UA_Decoder_readUInt32(decoder);
UA_UInt32 maxChunkCount = UA_Decoder_readUInt32(decoder);
printf("Received Hello: ProtocolVersion=%u, ReceiveBuffer=%u, SendBuffer=%u\n",
protocolVersion, receiveBufferSize, sendBufferSize);
/* 发送Acknowledge消息 */
UA_Byte response[28] = {0};
UA_Encoder encoder;
UA_Encoder_init(&encoder, response, sizeof(response));
/* 消息头 */
UA_Encoder_writeUInt32(&encoder, 0x41434B47); // "ACK"
UA_Encoder_writeUInt32(&encoder, 28); // 消息大小
UA_Encoder_writeUInt32(&encoder, 0); // 分块类型
/* 消息体 */
UA_Encoder_writeUInt32(&encoder, 0); // 协议版本
UA_Encoder_writeUInt32(&encoder, 8192); // 接收缓冲区大小
UA_Encoder_writeUInt32(&encoder, 8192); // 发送缓冲区大小
UA_Encoder_writeUInt32(&encoder, 65536); // 最大消息大小
UA_Encoder_writeUInt32(&encoder, 0); // 最大分块数
send(socket, response, 28, 0);
}
/* 处理请求消息 */
void handle_request_message(UA_Server *server, int socket, UA_Decoder *decoder) {
/* 读取请求类型 */
UA_UInt32 requestType = UA_Decoder_readUInt32(decoder);
switch(requestType) {
case 0x4D534743: // "C" - CreateSession
handle_create_session(server, socket, decoder);
break;
case 0x4D534741: // "A" - ActivateSession
handle_activate_session(server, socket, decoder);
break;
case 0x4D534752: // "R" - Read
handle_read_request(server, socket, decoder);
break;
case 0x4D534757: // "W" - Write
handle_write_request(server, socket, decoder);
break;
case 0x4D534742: // "B" - Browse
handle_browse_request(server, socket, decoder);
break;
default:
printf("Unknown request type: 0x%08X\n", requestType);
break;
}
}
/* 处理读取请求 */
void handle_read_request(UA_Server *server, int socket, UA_Decoder *decoder) {
/* 解码读取参数 */
UA_Decoder_readUInt32(decoder); // RequestId
UA_Decoder_readUInt32(decoder); // TimestampsToReturn
UA_UInt32 nodesToReadCount = UA_Decoder_readUInt32(decoder);
UA_NodeId *nodes = (UA_NodeId*)malloc(nodesToReadCount * sizeof(UA_NodeId));
for(UA_UInt32 i = 0; i < nodesToReadCount; i++) {
UA_Decoder_readNodeId(decoder, &nodes[i]);
}
/* 准备响应 */
UA_Byte response[1024];
UA_Encoder encoder;
UA_Encoder_init(&encoder, response, sizeof(response));
/* 消息头 */
UA_Encoder_writeUInt32(&encoder, 0x4D534747); // "MSG"
UA_Encoder_writeUInt32(&encoder, 0); // 临时大小
UA_Encoder_writeUInt32(&encoder, 0); // 分块类型
/* 响应体 */
UA_Encoder_writeUInt32(&encoder, 0x4D534752); // "R" - ReadResponse
UA_Encoder_writeUInt32(&encoder, 1); // RequestId
/* 结果数组 */
UA_Encoder_writeUInt32(&encoder, nodesToReadCount);
for(UA_UInt32 i = 0; i < nodesToReadCount; i++) {
UA_Variant value;
UA_StatusCode status = UA_Server_readValue(server, &nodes[i], &value);
/* 写入状态码 */
UA_Encoder_writeUInt32(&encoder, status);
if(status == UA_STATUSCODE_GOOD) {
/* 写入数据值 */
UA_Encoder_writeVariant(&encoder, &value);
}
}
/* 更新消息大小 */
UA_UInt32 messageSize = encoder.position;
memcpy(&response[4], &messageSize, 4);
send(socket, response, messageSize, 0);
free(nodes);
}
3.5 客户端实现
c
/**
* @file opcua_client.h
* @brief OPC UA 客户端实现
*/
#ifndef OPCUA_CLIENT_H
#define OPCUA_CLIENT_H
#include "opcua_types.h"
typedef struct {
int socket;
char *endpointUrl;
UA_UInt32 requestId;
UA_Session *session;
} UA_Client;
/* 客户端API */
UA_Client* UA_Client_new(void);
UA_StatusCode UA_Client_connect(UA_Client *client, const char *endpointUrl);
UA_StatusCode UA_Client_disconnect(UA_Client *client);
void UA_Client_delete(UA_Client *client);
/* 服务调用 */
UA_StatusCode UA_Client_read(
UA_Client *client,
const UA_NodeId *nodeId,
UA_Variant *value
);
UA_StatusCode UA_Client_write(
UA_Client *client,
const UA_NodeId *nodeId,
const UA_Variant *value
);
UA_StatusCode UA_Client_browse(
UA_Client *client,
const UA_NodeId *nodeId,
UA_BrowseResult *result
);
UA_StatusCode UA_Client_call(
UA_Client *client,
const UA_NodeId *objectId,
const UA_NodeId *methodId,
const UA_Variant *inputArguments,
size_t inputArgumentsSize,
UA_Variant *outputArguments,
size_t *outputArgumentsSize
);
#endif /* OPCUA_CLIENT_H */
c
/**
* @file opcua_client.c
* @brief OPC UA 客户端实现
*/
#include "opcua_client.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
/* 创建新客户端 */
UA_Client* UA_Client_new(void) {
UA_Client *client = (UA_Client*)calloc(1, sizeof(UA_Client));
if(!client) return NULL;
client->socket = -1;
client->requestId = 1;
client->session = NULL;
return client;
}
/* 连接到服务器 */
UA_StatusCode UA_Client_connect(UA_Client *client, const char *endpointUrl) {
if(!client || !endpointUrl) {
return UA_STATUSCODE_BADINTERNALERROR;
}
/* 解析URL */
char hostname[256];
int port = 4840; // 默认端口
if(sscanf(endpointUrl, "opc.tcp://%255[^:]:%d", hostname, &port) < 1) {
strcpy(hostname, endpointUrl);
}
/* 创建socket */
client->socket = socket(AF_INET, SOCK_STREAM, 0);
if(client->socket < 0) {
return UA_STATUSCODE_BADCOMMUNICATIONERROR;
}
/* 解析主机名 */
struct hostent *server = gethostbyname(hostname);
if(!server) {
close(client->socket);
return UA_STATUSCODE_BADHOSTUNKNOWN;
}
/* 连接服务器 */
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
memcpy(&server_addr.sin_addr.s_addr, server->h_addr, server->h_length);
server_addr.sin_port = htons(port);
if(connect(client->socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
close(client->socket);
return UA_STATUSCODE_BADCONNECTIONCLOSED;
}
client->endpointUrl = strdup(endpointUrl);
/* 发送Hello消息 */
UA_Byte hello[28] = {0};
UA_Encoder encoder;
UA_Encoder_init(&encoder, hello, sizeof(hello));
/* 消息头 */
UA_Encoder_writeUInt32(&encoder, 0x48454C47); // "HEL"
UA_Encoder_writeUInt32(&encoder, 28); // 消息大小
UA_Encoder_writeUInt32(&encoder, 0); // 分块类型
/* 消息体 */
UA_Encoder_writeUInt32(&encoder, 0); // 协议版本
UA_Encoder_writeUInt32(&encoder, 65536); // 接收缓冲区大小
UA_Encoder_writeUInt32(&encoder, 65536); // 发送缓冲区大小
UA_Encoder_writeUInt32(&encoder, 65536); // 最大消息大小
UA_Encoder_writeUInt32(&encoder, 0); // 最大分块数
send(client->socket, hello, sizeof(hello), 0);
/* 接收Acknowledge消息 */
UA_Byte ack[28];
ssize_t bytes = recv(client->socket, ack, sizeof(ack), 0);
if(bytes != 28) {
close(client->socket);
return UA_STATUSCODE_BADCOMMUNICATIONERROR;
}
printf("Connected to %s:%d\n", hostname, port);
return UA_STATUSCODE_GOOD;
}
/* 读取节点值 */
UA_StatusCode UA_Client_read(UA_Client *client, const UA_NodeId *nodeId, UA_Variant *value) {
if(!client || client->socket < 0) {
return UA_STATUSCODE_BADNOTCONNECTED;
}
/* 准备读取请求 */
UA_Byte request[256];
UA_Encoder encoder;
UA_Encoder_init(&encoder, request, sizeof(request));
/* 消息头 */
UA_Encoder_writeUInt32(&encoder, 0x4D534747); // "MSG"
UA_Encoder_writeUInt32(&encoder, 0); // 临时大小
UA_Encoder_writeUInt32(&encoder, 0); // 分块类型
/* 请求体 */
UA_Encoder_writeUInt32(&encoder, 0x4D534752); // "R" - ReadRequest
UA_Encoder_writeUInt32(&encoder, client->requestId++);
UA_Encoder_writeUInt32(&encoder, 0); // TimestampsToReturn
/* 要读取的节点 */
UA_Encoder_writeUInt32(&encoder, 1); // NodesToRead数量
UA_Encoder_writeNodeId(&encoder, nodeId);
/* 更新消息大小 */
UA_UInt32 messageSize = encoder.position;
memcpy(&request[4], &messageSize, 4);
/* 发送请求 */
send(client->socket, request, messageSize, 0);
/* 接收响应 */
UA_Byte response[1024];
ssize_t bytes = recv(client->socket, response, sizeof(response), 0);
if(bytes <= 0) {
return UA_STATUSCODE_BADCOMMUNICATIONERROR;
}
/* 解码响应 */
UA_Decoder decoder;
UA_Decoder_init(&decoder, response, bytes);
/* 跳过消息头 */
decoder.position += 12; // 消息头大小
/* 读取响应类型和RequestId */
UA_UInt32 responseType = UA_Decoder_readUInt32(&decoder);
UA_UInt32 requestId = UA_Decoder_readUInt32(&decoder);
if(responseType != 0x4D534752) { // 不是ReadResponse
return UA_STATUSCODE_BADUNEXPECTEDERROR;
}
/* 读取结果数量 */
UA_UInt32 resultsCount = UA_Decoder_readUInt32(&decoder);
if(resultsCount != 1) {
return UA_STATUSCODE_BADUNEXPECTEDERROR;
}
/* 读取状态码 */
UA_StatusCode status = UA_Decoder_readUInt32(&decoder);
if(status != UA_STATUSCODE_GOOD) {
return status;
}
/* 读取数据值 */
return UA_Decoder_readVariant(&decoder, value);
}
/* 写入节点值 */
UA_StatusCode UA_Client_write(UA_Client *client, const UA_NodeId *nodeId, const UA_Variant *value) {
if(!client || client->socket < 0) {
return UA_STATUSCODE_BADNOTCONNECTED;
}
/* 准备写入请求 */
UA_Byte request[512];
UA_Encoder encoder;
UA_Encoder_init(&encoder, request, sizeof(request));
/* 消息头 */
UA_Encoder_writeUInt32(&encoder, 0x4D534747); // "MSG"
UA_Encoder_writeUInt32(&encoder, 0); // 临时大小
UA_Encoder_writeUInt32(&encoder, 0); // 分块类型
/* 请求体 */
UA_Encoder_writeUInt32(&encoder, 0x4D534757); // "W" - WriteRequest
UA_Encoder_writeUInt32(&encoder, client->requestId++);
/* 要写入的节点 */
UA_Encoder_writeUInt32(&encoder, 1); // NodesToWrite数量
/* 写入节点ID */
UA_Encoder_writeNodeId(&encoder, nodeId);
/* 写入值 */
UA_Encoder_writeUInt32(&encoder, 13); // AttributeId: Value
UA_Encoder_writeVariant(&encoder, value);
/* 更新消息大小 */
UA_UInt32 messageSize = encoder.position;
memcpy(&request[4], &messageSize, 4);
/* 发送请求 */
send(client->socket, request, messageSize, 0);
/* 接收响应 */
UA_Byte response[256];
ssize_t bytes = recv(client->socket, response, sizeof(response), 0);
if(bytes <= 0) {
return UA_STATUSCODE_BADCOMMUNICATIONERROR;
}
/* 解码响应 */
UA_Decoder decoder;
UA_Decoder_init(&decoder, response, bytes);
/* 跳过消息头 */
decoder.position += 12;
/* 读取响应类型和RequestId */
UA_UInt32 responseType = UA_Decoder_readUInt32(&decoder);
UA_UInt32 requestId = UA_Decoder_readUInt32(&decoder);
if(responseType != 0x4D534757) { // 不是WriteResponse
return UA_STATUSCODE_BADUNEXPECTEDERROR;
}
/* 读取结果数量 */
UA_UInt32 resultsCount = UA_Decoder_readUInt32(&decoder);
if(resultsCount != 1) {
return UA_STATUSCODE_BADUNEXPECTEDERROR;
}
/* 返回状态码 */
return UA_Decoder_readUInt32(&decoder);
}
四、使用示例
4.1 服务器示例
c
/**
* @file server_example.c
* @brief OPC UA 服务器示例
*/
#include "opcua_server.h"
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
UA_Server *g_server = NULL;
/* 信号处理 */
void signal_handler(int sig) {
printf("Shutting down server...\n");
if(g_server) {
UA_Server_stop(g_server);
UA_Server_delete(g_server);
}
exit(0);
}
/* 读取回调 */
void on_read_callback(UA_NodeId nodeId, UA_Variant *value) {
static int counter = 0;
if(nodeId.identifierType == UA_NODEIDTYPE_NUMERIC &&
nodeId.identifier.numeric == 1001) {
value->type = UA_VARIANT_DATA_INT32;
value->data.int32 = ++counter;
}
}
/* 写入回调 */
void on_write_callback(UA_NodeId nodeId, UA_Variant *value) {
printf("Node 0x%X written with value: ", nodeId.identifier.numeric);
switch(value->type) {
case UA_VARIANT_DATA_INT32:
printf("%d\n", value->data.int32);
break;
case UA_VARIANT_DATA_DOUBLE:
printf("%f\n", value->data.doubleVal);
break;
case UA_VARIANT_DATA_STRING:
printf("%s\n", value->data.string);
break;
default:
printf("Unknown type\n");
break;
}
}
int main(int argc, char *argv[]) {
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
/* 服务器配置 */
UA_ServerConfig config = {
.port = 4840,
.maxConnections = 10,
.maxWorkerThreads = 4,
.enableSecurity = false,
.applicationUri = "urn:localhost:MyOPCUAServer",
.productUri = "urn:mycompany:MyProduct",
.applicationName = "My OPC UA Server"
};
/* 创建服务器 */
g_server = UA_Server_new(&config);
if(!g_server) {
fprintf(stderr, "Failed to create server\n");
return 1;
}
/* 设置回调 */
g_server->callbacks.onRead = on_read_callback;
g_server->callbacks.onWrite = on_write_callback;
/* 添加示例节点 */
UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
UA_NodeId referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
UA_QualifiedName browseName = {"Counter", 1};
UA_VariableAttributes attr = {0};
attr.displayName = "Counter";
attr.description = "A simple counter variable";
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
attr.userAccessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
attr.minimumSamplingInterval = 1000.0;
attr.historizing = false;
UA_Variant_init(&attr.value);
attr.value.type = UA_VARIANT_DATA_INT32;
attr.value.data.int32 = 0;
UA_NodeId variableNodeId;
UA_StatusCode status = UA_Server_addVariableNode(
g_server,
&parentNodeId,
&referenceTypeId,
&browseName,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
&attr,
NULL,
&variableNodeId
);
if(status != UA_STATUSCODE_GOOD) {
fprintf(stderr, "Failed to add variable node: 0x%08X\n", status);
UA_Server_delete(g_server);
return 1;
}
printf("Added variable node: ns=%d, id=%d\n",
variableNodeId.namespaceIndex, variableNodeId.identifier.numeric);
/* 运行服务器 */
printf("Server is running on opc.tcp://localhost:4840\n");
printf("Press Ctrl+C to exit\n");
status = UA_Server_run(g_server);
if(status != UA_STATUSCODE_GOOD) {
fprintf(stderr, "Server error: 0x%08X\n", status);
}
UA_Server_delete(g_server);
return 0;
}
4.2 客户端示例
c
/**
* @file client_example.c
* @brief OPC UA 客户端示例
*/
#include "opcua_client.h"
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
const char *endpointUrl = "opc.tcp://localhost:4840";
if(argc > 1) {
endpointUrl = argv[1];
}
/* 创建客户端 */
UA_Client *client = UA_Client_new();
if(!client) {
fprintf(stderr, "Failed to create client\n");
return 1;
}
/* 连接服务器 */
UA_StatusCode status = UA_Client_connect(client, endpointUrl);
if(status != UA_STATUSCODE_GOOD) {
fprintf(stderr, "Failed to connect: 0x%08X\n", status);
UA_Client_delete(client);
return 1;
}
printf("Connected to %s\n", endpointUrl);
/* 读取节点值 */
UA_NodeId nodeId = UA_NODEID_NUMERIC(1, 1001); // 假设的节点ID
for(int i = 0; i < 10; i++) {
UA_Variant value;
status = UA_Client_read(client, &nodeId, &value);
if(status == UA_STATUSCODE_GOOD) {
if(value.type == UA_VARIANT_DATA_INT32) {
printf("Read value: %d\n", value.data.int32);
} else {
printf("Unexpected data type: %d\n", value.type);
}
} else {
printf("Read failed: 0x%08X\n", status);
}
sleep(1);
}
/* 写入节点值 */
UA_Variant writeValue;
writeValue.type = UA_VARIANT_DATA_INT32;
writeValue.data.int32 = 42;
status = UA_Client_write(client, &nodeId, &writeValue);
if(status == UA_STATUSCODE_GOOD) {
printf("Write successful\n");
} else {
printf("Write failed: 0x%08X\n", status);
}
/* 断开连接 */
UA_Client_disconnect(client);
UA_Client_delete(client);
return 0;
}
五、编译与构建
5.1 CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.10)
project(opcua_stack C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 包含目录
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
# 源文件
set(OPCUA_SOURCES
src/opcua_types.c
src/opcua_binary.c
src/opcua_server.c
src/opcua_client.c
src/opcua_security.c
src/opcua_subscription.c
)
# 服务器可执行文件
add_executable(opcua_server
examples/server_example.c
${OPCUA_SOURCES}
)
# 客户端可执行文件
add_executable(opcua_client
examples/client_example.c
${OPCUA_SOURCES}
)
# 库文件
add_library(opcua STATIC ${OPCUA_SOURCES})
# 链接库
target_link_libraries(opcua_server opcua pthread)
target_link_libraries(opcua_client opcua pthread)
5.2 构建脚本
bash
#!/bin/bash
# build.sh
# 创建构建目录
mkdir -p build
cd build
# 使用CMake生成Makefile
cmake -DCMAKE_BUILD_TYPE=Release ..
# 编译
make -j4
echo "构建完成!"
echo "可执行文件在 build/ 目录中"
参考代码 OPC UA 协议栈C语言实现 www.youwenfan.com/contentcsv/70675.html
六、高级特性实现
6.1 安全与加密
c
/**
* @file opcua_security.c
* @brief OPC UA 安全模块
*/
#include "opcua_security.h"
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
/* 安全策略 */
typedef enum {
UA_SECURITYPOLICY_NONE = 0,
UA_SECURITYPOLICY_BASIC128RSA15,
UA_SECURITYPOLICY_BASIC256,
UA_SECURITYPOLICY_BASIC256SHA256
} UA_SecurityPolicy;
/* 安全令牌 */
typedef struct {
UA_UInt32 tokenId;
UA_DateTime createdAt;
UA_DateTime revisedLifetime;
UA_ByteString serverNonce;
} UA_ChannelSecurityToken;
/* 消息安全 */
typedef struct {
UA_SecurityPolicy policy;
RSA *localPrivateKey;
RSA *remotePublicKey;
UA_ByteString localNonce;
UA_ByteString remoteNonce;
} UA_SecurityContext;
/* 初始化安全上下文 */
UA_StatusCode UA_SecurityContext_init(UA_SecurityContext *ctx,
UA_SecurityPolicy policy) {
ctx->policy = policy;
ctx->localPrivateKey = NULL;
ctx->remotePublicKey = NULL;
/* 生成本地Nonce */
UA_ByteString_allocBuffer(&ctx->localNonce, 32);
RAND_bytes(ctx->localNonce.data, ctx->localNonce.length);
return UA_STATUSCODE_GOOD;
}
/* 消息签名 */
UA_StatusCode UA_SecurityContext_sign(UA_SecurityContext *ctx,
const UA_ByteString *message,
UA_ByteString *signature) {
if(ctx->policy == UA_SECURITYPOLICY_NONE || !ctx->localPrivateKey) {
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
unsigned int siglen = RSA_size(ctx->localPrivateKey);
UA_ByteString_allocBuffer(signature, siglen);
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(message->data, message->length, hash);
int result = RSA_sign(NID_sha256, hash, SHA256_DIGEST_LENGTH,
signature->data, &siglen, ctx->localPrivateKey);
if(result != 1) {
UA_ByteString_clear(signature);
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
signature->length = siglen;
return UA_STATUSCODE_GOOD;
}
/* 消息验证 */
UA_StatusCode UA_SecurityContext_verify(UA_SecurityContext *ctx,
const UA_ByteString *message,
const UA_ByteString *signature) {
if(ctx->policy == UA_SECURITYPOLICY_NONE || !ctx->remotePublicKey) {
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(message->data, message->length, hash);
int result = RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH,
signature->data, signature->length,
ctx->remotePublicKey);
return (result == 1) ? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
七、测试与验证
7.1 单元测试
c
/**
* @file test_opcua.c
* @brief OPC UA 单元测试
*/
#include "opcua_types.h"
#include "opcua_binary.h"
#include <assert.h>
#include <string.h>
void test_binary_encoding(void) {
printf("Testing binary encoding...\n");
UA_Byte buffer[256];
UA_Encoder enc;
UA_Decoder dec;
/* 测试基本类型 */
UA_Encoder_init(&enc, buffer, sizeof(buffer));
UA_Encoder_writeUInt32(&enc, 0x12345678);
UA_Encoder_writeInt32(&enc, -123456);
UA_Decoder_init(&dec, buffer, enc.position);
assert(UA_Decoder_readUInt32(&dec) == 0x12345678);
assert(UA_Decoder_readInt32(&dec) == -123456);
/* 测试字符串 */
UA_String str = {5, (UA_UInt16*)"Hello"};
UA_Encoder_init(&enc, buffer, sizeof(buffer));
UA_Encoder_writeString(&enc, &str);
UA_Decoder_init(&dec, buffer, enc.position);
UA_String decodedStr;
UA_Decoder_readString(&dec, &decodedStr);
assert(decodedStr.length == 5);
assert(memcmp(decodedStr.data, "Hello", 5) == 0);
printf("All tests passed!\n");
}
void test_node_id(void) {
printf("Testing NodeId encoding...\n");
UA_NodeId node1 = UA_NODEID_NUMERIC(1, 1234);
UA_NodeId node2 = UA_NODEID_STRING(2, "MyVariable");
UA_Byte buffer[256];
UA_Encoder enc;
UA_Decoder dec;
/* 编码和解码数字节点ID */
UA_Encoder_init(&enc, buffer, sizeof(buffer));
UA_Encoder_writeNodeId(&enc, &node1);
UA_Decoder_init(&dec, buffer, enc.position);
UA_NodeId decodedNode1;
UA_Decoder_readNodeId(&dec, &decodedNode1);
assert(decodedNode1.identifierType == UA_NODEIDTYPE_NUMERIC);
assert(decodedNode1.namespaceIndex == 1);
assert(decodedNode1.identifier.numeric == 1234);
printf("NodeId tests passed!\n");
}
int main(void) {
test_binary_encoding();
test_node_id();
return 0;
}
八、使用开源项目的最佳实践
8.1 使用 open62541
c
/* 使用 open62541 的简单示例 */
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.h>
UA_Boolean running = true;
static void stopHandler(int sig) {
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
/* 创建服务器 */
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
/* 添加变量节点 */
UA_VariableAttributes attr = UA_VariableAttributes_default;
UA_Int32 myInteger = 42;
UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
attr.description = UA_LOCALIZEDTEXT("en-US", "A sample variable");
attr.displayName = UA_LOCALIZEDTEXT("en-US", "SampleVariable");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "sample-integer");
UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "Sample Variable");
UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
UA_NodeId variableType = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);
UA_Server_addVariableNode(server, myIntegerNodeId,
parentNodeId, parentReferenceNodeId,
myIntegerName, variableType,
attr, NULL, NULL);
/* 运行服务器 */
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
九、总结
9.1 实现要点
- 分层架构:传输层、编码层、服务层
- 内存管理:合理的内存分配和释放策略
- 线程安全:多线程环境下的并发控制
- 错误处理:完善的错误码和异常处理
- 扩展性:插件化架构支持功能扩展
9.2 生产建议
- 使用开源实现:推荐 open62541 作为基础
- 安全第一:实现完整的安全机制
- 性能优化:考虑内存池、连接池等技术
- 测试覆盖:全面的单元测试和集成测试
- 标准兼容:通过 OPC UA 兼容性测试