这是用户在 2025-7-17 8:26 为 https://github.com/freeopcua/async-opcua/blob/HEAD/docs/client.md 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Skip to content

Files

Latest commit

Mar 26, 2025
b7b4e3d · Mar 26, 2025

History

History
279 lines (197 loc) · 12 KB

client.md

File metadata and controls

279 lines (197 loc) · 12 KB

Client 客户端

Work in progress 进行中

This is a small tutorial for using the OPC UA client library. It will assume you are familiar with OPC UA, Rust and tools such as cargo.
这是一个使用 OPC UA 客户端库的小教程。假设您已经熟悉 OPC UA、Rust 以及诸如 cargo 等工具。

  1. A small overview of OPC UA is here.
    OPC UA 的简要概述在此。
  2. Rust OPC UA's compatibility with the standard is described here.
    Rust OPC UA 与标准的兼容性在此处描述。

Introducing the OPC UA Client API
引入 OPC UA 客户端 API

The OPC UA for Rust client API supports calls for OPC UA services. Whether the server you are calling implements them is another matter but you can call them.
Rust 的 OPC UA 客户端 API 支持调用 OPC UA 服务。至于您调用的服务器是否实现了这些服务则是另一回事,但您可以进行调用。

For the most part it is synchronous - you call the function and it waits for the server to respond or a timeout to happen. Each function call returns a Result either containing the response to the call, or a status code.
大部分情况下它是同步的——你调用函数,它会等待服务器响应或超时发生。每次函数调用都会返回一个 Result ,其中包含调用的响应或状态码。

Data change notifications are asynchronous. When you create a subscription you supply a callback. The client API will automatically begin sending publish requests to the server on your behalf and will call your callback when a publish response contains notifications.
数据变更通知是异步的。创建订阅时,您需要提供一个回调函数。客户端 API 将自动代表您开始向服务器发送发布请求,并在发布响应包含通知时调用您的回调函数。

Clients generally require some knowledge of the server you are calling. You need to know its ip address, port, endpoints, security policy and also what services it supports. The client API provides different ways to connect to servers, by configuration file or ad hoc connections.
客户端通常需要了解所调用服务器的一些信息。您需要知道其 IP 地址、端口、端点、安全策略以及支持的服务。客户端 API 提供了通过配置文件或临时连接等多种方式连接到服务器。

In this sample, we're going to write a simple client that connects to the opcua/samples/simple-server, subscribes to some values and prints them out as they change.
在这个示例中,我们将编写一个简单的客户端,连接到 opcua/samples/simple-server ,订阅一些值并在它们变化时打印出来。

If you want to see a finished version of this, look at opcua/samples/simple-client.
如果你想看完成版本,请查看 opcua/samples/simple-client

Life cycle 生命周期

From a coding perspective a typical use would be this:
从编码角度来看,典型用法如下:

  1. Create a Client. The easiest way is with a ClientBuilder.
    创建一个 Client 。最简单的方法是使用 ClientBuilder
  2. Create a Session and SessionEventLoop from a server endpoint.
    从服务器端点创建一个 SessionSessionEventLoop
  3. Begin polling the event loop, either in a tokio Task or in a select! block.
    开始轮询事件循环,可以在 tokio Task 中或 select! 块中进行。
  4. Wait for the event loop to connect to the server.
    等待事件循环连接到服务器。
  5. Call functions on the session which make requests to the server, e.g. read a value, or monitor items
    在会话上调用向服务器发出请求的函数,例如读取值或监视项
  6. Run in a loop doing 5 repeatedly or exit
    循环运行,重复执行 5 次或退出

Most of the housekeeping and detail is handled by the API. You just need to point the client at the server, and set things up before calling stuff on the session.
大部分日常维护和细节工作都由 API 处理。您只需将客户端指向服务器,并在调用会话操作前完成相关设置即可。

Create a simple project 创建一个简单的项目

We're going to start with a blank project.
我们将从一个空白项目开始。

cargo init --bin test-client

Import the crate 导入该 crate

The opcua is the crate containing the client side API. So first edit your Cargo.toml to add that dependency. You will also need tokio:
opcua 是包含客户端 API 的 crate。因此,首先编辑您的 Cargo.toml 以添加该依赖项。您还需要 tokio

[dependencies]
async-opcua = { version = "0.14", features = ["client"] }
tokio = { version = "1", features = ["full"] }

Create your client 创建您的客户端

The Client object represents a configured client describing its identity and set of behaviours.
Client 对象代表一个配置好的客户端,描述其身份和行为集合。

There are three ways we can create one.
我们可以通过三种方式来创建一个。

  1. Via a ClientBuilder 通过 ClientBuilder
  2. Externally by loading a a configuration file.
    通过加载配置文件从外部实现。
  3. Hybrid approach, load some defaults from a configuration file and override them from a ClientBuilder.
    混合方法,从配置文件中加载一些默认设置,并通过 ClientBuilder 覆盖它们。

We'll use a pure ClientBuilder approach below because it's the simplest to understand without worrying about file paths or file formats.
下面我们将采用纯粹的 ClientBuilder 方法,因为它最简单易懂,无需担心文件路径或格式问题。

A builder pattern in Rust consists of a number of configuration calls chained together that eventually yield the object we are building.
Rust 中的构建器模式由一系列链式配置调用组成,最终生成我们正在构建的对象。

use opcua::client::prelude::*;

fn main() {
    let mut client = ClientBuilder::new()
        .application_name("My First Client")
        .application_uri("urn:MyFirstClient")
        .create_sample_keypair(true)
        .trust_server_certs(true)
        .session_retry_limit(3)
        .client().unwrap();

    //... connect to server
}

So here we use ClientBuilder to construct a Client that will:
所以我们在这里使用 ClientBuilder 来构建一个 Client ,它将:

  • Be called "My First Client" and a uri of "urn:MyFirstClient"
    命名为“我的第一个客户端”,uri 为“urn:MyFirstClient”
  • Automatically create a private key and public certificate (if none already exists)
    自动生成私钥和公钥证书(如果尚不存在)
  • Automatically trust the server's cert during handshake.
    在握手过程中自动信任服务器的证书。
  • Retry up to 3 times to reconnect if the connection goes down.
    如果连接断开,最多重试 3 次以重新连接。

Security 安全

Security is an important feature of OPC UA. Because the builder has called create_sample_keypair(true) it will automatically create a self-signed private key and public cert if the files do not already exist. If we did not set this line then something else would have to install a key & cert, e.g. an external script.
安全性是 OPC UA 的一个重要特性。由于构建器已调用 create_sample_keypair(true) ,如果文件尚不存在,它将自动创建自签名的私钥和公钥证书。如果我们不设置这一行,那么其他方式(如外部脚本)将需要安装密钥和证书。

But as it is set, the first time it starts it will create a directory called ./pki (relative to the working directory) and create a private key and public certificate files.
但按照当前设置,首次启动时它会在工作目录下创建一个名为 ./pki 的文件夹,并生成私钥和公钥证书文件。

./pki/own/cert.der
./pki/private/private.pem

These files are X509 (cert.der) and private key (private.pem) files respectively. The X509 is a certificate containing information about the client "My First Client" and the public key. The private key is just the private key.
这些文件分别是 X509( cert.der )和私钥( private.pem )文件。X509 是一个包含客户端“My First Client”信息和公钥的证书。私钥则仅为私钥本身。

For security purposes, clients are required to trust server certificates (and servers are required to trust clients), but for demo purposes we've told the client to automatically trust the server by calling trust_server_certs(true). When this setting is true, the client will automatically trust the server regardless of the key it presents.
出于安全考虑,客户端需要信任服务器证书(服务器也需要信任客户端),但为了演示目的,我们通过调用 trust_server_certs(true) 让客户端自动信任服务器。当此设置启用时,无论服务器提供何种密钥,客户端都会自动信任它。

In production you should NOT disable the trust checks.
在生产环境中,您不应禁用信任检查。

When we connect to a server for the first you will see some more entries added under ./pki resembling this:
当我们首次连接到服务器时,你会看到类似这样的更多条目被添加到 ./pki 下:

./pki/rejected/
./pki/trusted/ServerFoo [f5baa2ed3896ef3048a148ea69a516a92a222fcc].der

The server's .der file was automatically stored in ./pki/trusted because we told the client to automatically trust the server. The name of this file is derived from information in the certificate and its thumbprint to make a unique file.
服务器的.der 文件被自动存储在 ./pki/trusted 中,因为我们指示客户端自动信任该服务器。此文件的名称源自证书中的信息及其指纹,以确保生成唯一的文件。

If we had told the client not to trust the server, the cert would have appeared under /pki/rejected and we would need to move it manually into the /pki/trusted folder. This is what you should do in production.
如果我们告诉客户不要信任服务器,证书就会出现在 /pki/rejected 下,我们需要手动将其移动到 /pki/trusted 文件夹中。在生产环境中,您应该这样做。

Make your server trust your client
让你的服务器信任你的客户端

Even though we have told the client to automatically trust the server, it does not mean the server will trust the client. Both will need to trust on another for the handshake to succeed. Therefore the next step is make the server trust the client.
尽管我们已经告知客户端自动信任服务器,但这并不意味着服务器会信任客户端。双方需要相互信任才能成功完成握手。因此,下一步是让服务器信任客户端。

Refer to the documentation in your server to see how to do this. In many OPC UA servers this will involve moving the client's cert from a /rejected to a /trusted folder much as you did in OPC UA for Rust. Other servers may require you do this some other way, e.g. through a web interface or configuration.
请参考您服务器中的文档了解如何操作。在许多 OPC UA 服务器中,这涉及将客户端的证书从 /rejected 文件夹移动到 /trusted 文件夹,就像您在 OPC UA for Rust 中所做的那样。其他服务器可能要求您通过其他方式完成此操作,例如通过网页界面或配置。

Retry policy 重试策略

We also set a retry policy, so that if the client cannot connect to the server or is disconnected from the server, it will try to connect up to 3 times before giving up. If a connection succeeds the retry counter is reset so it's 3 tries for any one reconnection attempt, not total. Setting the limit to -1 would retry continuously forever.
我们还设置了重试策略,这样如果客户端无法连接到服务器或与服务器断开连接,它将尝试最多连接 3 次才会放弃。如果连接成功,重试计数器将被重置,因此每次重新连接尝试最多有 3 次机会,而非总计。将限制设置为-1 则会无限持续重试。

There are also settings to control the retry reconnection rate, i.e. the interval to wait from one failed attempt to the next. It is not advisable to make retries too fast.
还有设置可以控制重试连接的速率,即从一次失败尝试到下一次尝试之间的等待间隔。不建议将重试设置得过快。

Create the Client 创建客户端

Finally we called client() to produce a Client. Now we have a client we can start calling it.
最后我们调用 client() 生成了一个 Client 。现在有了客户端,我们可以开始调用它了。

Connect to a server 连接到服务器

A Client can connect to any server it likes. There are a number of ways to do this:
一个 Client 可以连接到任何它喜欢的服务器。有多种方法可以实现这一点:

  1. Predefined endpoints set up by the ClientBuilder
    ClientBuilder 设置的预定义端点
  2. Ad hoc via a url, security policy and identity token.
    通过 URL 临时访问,附带安全策略和身份令牌。

We'll go ad hoc. So in your client code you will have some code like this.
我们将临时处理。因此,在你的客户端代码中,会有类似这样的代码。

#[tokio::main]
async fn main() {
    //... create Client
    
    // Create an endpoint. The EndpointDescription can be made from a tuple consisting of
    // the endpoint url, security policy, message security mode and user token policy.
    let endpoint: EndpointDescription = (
        "opc.tcp://localhost:4855/",
        "None",
        MessageSecurityMode::None,
        UserTokenPolicy::anonymous()
    ).into();

    // Create the session
    let (session, event_loop) = client.connect_to_matching_endpoint(endpoint, IdentityToken::Anonymous).await.unwrap();

    // Spawn the event loop on a tokio task.
    let mut handle = event_loop.spawn();
    tokio::select! {
        r = &mut handle => {
            println!("Session failed to connect! {r}");
            return;
        }
        _ = session.wait_for_connection().await => {}
    }
    
 
    //... use session
}

This command asks the API to connect to the server opc.tcp://localhost:4855/ with a security policy / message mode of None / None, and to connect as an anonymous user.
此命令要求 API 以无安全策略/消息模式(None/None)连接到服务器 opc.tcp://localhost:4855/ ,并以匿名用户身份进行连接。

Note that this does not connect to the server, only identify a server endpoint to connect to and create the necessary types to manage that connection.
请注意,这并不会连接到服务器,仅用于识别要连接的服务器端点并创建管理该连接所需的类型。

The event_loop is responsible for maintaining the connection to the server. We run it in a background thread for convenience. In this case, if the event loop terminates, we have failed to connect to the server, even after retries.
event_loop 负责维护与服务器的连接。为了方便起见,我们在后台线程中运行它。在这种情况下,如果事件循环终止,则表示即使经过多次重试,我们也未能成功连接到服务器。

In order to avoid waiting forever on a connection, we watch the handle in a select!.
为了避免在连接上无限等待,我们使用 select! 来监视句柄。

Once wait_for_connection returns, if the event loop has not terminated, we have an open and activated session.
一旦 wait_for_connection 返回,如果事件循环尚未终止,我们就有一个开放且激活的会话。

Calling the server 调用服务器

Once we have a session we can ask the server to do things by sending requests to it. Requests correspond to services implemented by the server. Each request is answered by a response containing the answer, or a service fault if the service is in error.
一旦我们建立了会话,就可以通过向服务器发送请求来让它执行操作。这些请求对应着服务器实现的服务。每个请求都会收到一个包含答案的响应,如果服务出错,则会返回服务故障信息。

The API is asynchronous, and only requires a shared reference to the session. This means that you can make multiple independent requests concurrently. The session only keeps a single connection to the server, but OPC-UA supports pipelining meaning that you can send several requests to the server at the same time, then receive them out of order.
该 API 是异步的,仅需共享会话的引用即可。这意味着您可以同时发起多个独立的请求。会话仅维持与服务器的单一连接,但 OPC-UA 支持流水线操作,允许您同时向服务器发送多个请求,并以乱序接收响应。

Calling a service 调用服务

Each service call in the server has a corresponding client side function. For example to create a subscription there is a create_subscription() function in the client's Session. When this is called, the API will fill in a CreateSubscriptionRequest message, send it to the server, wait for the corresponding CreateSubscriptionResponse and return from the call with the contents of the response.
服务器中的每个服务调用都有一个对应的客户端函数。例如,要创建订阅,客户端 Session 中有一个 create_subscription() 函数。当调用此函数时,API 将填充 CreateSubscriptionRequest 消息,将其发送到服务器,等待对应的 CreateSubscriptionResponse ,并从调用返回响应内容。

Here is code that creates a subscription and adds a monitored item to the subscription.
以下是创建订阅并向订阅添加监控项的代码。

{
    let subscription_id = session.create_subscription(std::time::Duration::from_millis(2000), 10, 30, 0, 0, true, DataChangeCallback::new(|changed_monitored_items| {
        println!("Data change from server:");
        changed_monitored_items.iter().for_each(|item| print_value(item));
    })).await?;
    
    // Create some monitored items
    let items_to_create: Vec<MonitoredItemCreateRequest> = ["v1", "v2", "v3", "v4"].iter()
        .map(|v| NodeId::new(2, *v).into()).collect();
    let _ = session.create_monitored_items(subscription_id, TimestampsToReturn::Both, items_to_create).await?;
}

Note the call to create_subscription() requires an implementation of a callback. There is a DataChangeCallback helper for this purpose that calls your function with any changed items, but you can also implement it yourself for more complex use cases.
请注意,调用 create_subscription() 需要一个回调函数的实现。为此目的有一个 DataChangeCallback 辅助函数,它会用任何变更的项调用你的函数,但对于更复杂的用例,你也可以自行实现它。

Monitoring the event loop
监控事件循环

Using event_loop.spawn is convenient if you do not care what the session is doing, but in general you want to know what is happening so that your code can react to it. The event_loop drives the entire session including sending and receiving messages, monitoring subscriptions, and establishing and maintaining the connection.
使用 event_loop.spawn 很方便,如果你不关心会话的具体操作,但通常你会希望了解发生了什么,以便你的代码能够做出响应。 event_loop 驱动整个会话,包括发送和接收消息、监控订阅以及建立和维护连接。

You can watch these events yourself by using event_loop.enter, which returns a Stream of SessionPollResult items.
你可以通过使用 event_loop.enter 来自己观察这些事件,它会返回一个包含 SessionPollResult 项的 Stream

tokio::task::spawn(async move {
    // Using `next` requires the futures_util package.
    while let Some(evt) = event_loop.next() {
        match evt {
            Ok(SessionPollResult::ConnectionLost(status)) => { /* connection lost */ },
            Ok(SessionPollResult::Reconnected(mode)) => { /* connection established */ },
            Ok(SessionPollResult::ReconnectFailed(status)) => { /* connection attempt failed */ },
            Err(e) => { /* Exhausted connect retries, the stream will exit now */ },
            _ => { /* Other events */ }
        }
    }
})

That's it 就这样

Now you have created a simple client application. Look at the client examples under samples, starting with simple-client for a very basic client.
现在你已经创建了一个简单的客户端应用程序。查看 samples 下的客户端示例,从 simple-client 开始了解一个非常基础的客户端。