Skip to content

Client Connecting

Kevin Herron edited this page Oct 5, 2026 · 9 revisions

Milo 1.2.0-SNAPSHOT, Java 17. Source baseline 7faaf4f0516c.

Your first client connected, read one value, and disconnected. A real application connects once at startup, keeps the client while it works, and disconnects when it stops. This page builds that connection for the tutorial server. It covers endpoint selection, user identity, a failed first attempt, and shutdown.

You need the client artifact from Getting started and FirstServer from Your first server running at opc.tcp://127.0.0.1:12686/wiki.

How a connection comes together

Connecting takes two calls. OpcUaClient.create(...) asks the server at the URL you pass for its endpoints and passes the list to a selection function you supply. Each endpoint combines a URL, transport profile, message security mode, security policy, server certificate, and user-token policies, so the selection function is where you decide how the application talks to this server. create then builds the client configuration from the selected endpoint and your settings. No Session exists yet.

connect() does the network work. It needs a successful transport connection, SecureChannel, Session creation, and Session activation with your user identity. From then on the client keeps that Session alive. When the connection drops, Milo reconnects and reactivates or replaces the Session in the background until you call disconnect().

Connect to the tutorial server

This method connects to the tutorial endpoint (security policy None, message security mode None, anonymous identity) and returns the connected client. All types come from the client artifact, and uint is a static import from Unsigned.

static OpcUaClient connect(String endpointUrl) throws UaException {
  OpcUaClient client =
      OpcUaClient.create(
          endpointUrl,
          endpoints -> selectEndpoint(endpoints),
          transport -> {},
          config ->
              config
                  .setApplicationName(LocalizedText.english("Thermostat client"))
                  .setApplicationUri("urn:eclipse:milo:wiki:client")
                  .setIdentityProvider(new AnonymousProvider())
                  .setRequestTimeout(uint(5_000)));

  try {
    client.connect();
  } catch (UaException e) {
    // A failed connect keeps retrying in the background until you disconnect.
    try {
      client.disconnect();
    } catch (UaException disconnectFailure) {
      e.addSuppressed(disconnectFailure);
    }
    throw e;
  }

  return client;
}

static Optional<EndpointDescription> selectEndpoint(List<EndpointDescription> endpoints) {
  return endpoints.stream()
      .filter(e -> SecurityPolicy.None.getUri().equals(e.getSecurityPolicyUri()))
      .filter(e -> e.getSecurityMode() == MessageSecurityMode.None)
      .findFirst();
}

The catch block matters because connect() leaves the client retrying connections and Sessions in the background even when the first attempt fails. Calling disconnect() before rethrowing stops those retries, so a failed startup leaves nothing running and the application decides whether to try again. If FirstServer is not running, discovery inside create fails first, so the method throws UaException with Bad_ConnectionRejected before any client exists.

Choose an endpoint deliberately

A server usually advertises several endpoints, and the first one is not necessarily compatible or secure. Match every property your application requires, including the user-token policy for the identity you plan to use. The tutorial selector checks only policy and mode because FirstServer offers a single anonymous None endpoint. For production security requirements, use an explicit selector for a secured endpoint, as Client security shows.

The shortcut OpcUaClient.create(url) selects a None endpoint supporting anonymous authentication. DiscoveryClient.getEndpoints(url) returns a future containing the advertised endpoints, without creating a client.

On any discovery or selection failure, create retries once with /discovery appended unless the URL already has that suffix. The exception you receive describes that second attempt, which can fail with a transport or endpoint-URL status instead and hide the original cause, as When connecting fails shows.

Configure addresses and timeouts

Discovery can succeed while the advertised connection URL is unreachable. This is common with gateways, container port mappings, and servers with several hostnames. Correct the server's advertisement where possible.

If routing requires a client-side override, call EndpointUtil.updateUrl(endpoint, hostname, port) inside the selection function passed to create(...) or EndpointResolver.create(...). It belongs there because the resolver reruns that function on every certificate refresh. The resolver rejects a refreshed URL that differs from the currently selected URL and keeps using the cached endpoint. Preserve the selected policy, mode, application URI, and token policies, and keep the original discovery list for Session endpoint validation.

Hostname validation then checks the overridden address. Enable ValidationCheck.HOSTNAME to reject a server certificate that does not cover it, because the two-argument DefaultClientCertificateValidator constructor only warns about the mismatch. See client trust checks.

Both callbacks passed to create configure builders and return nothing. The transport callback sets TCP connection and handshake timeouts. It runs for the client transport and again for each discovery or refresh transport, so create shared executors, event loops, and timers outside it, pass them into each builder, and close them after disconnection.

The client callback sets the request timeout, requested Session timeout, application name and URI, certificate configuration, and identity provider. The server can revise the requested Session timeout.

Authenticate the Session

The identity provider decides who the Session user is. Set it with setIdentityProvider in the client callback. User credentials do not replace the application's certificate or its validation of the server, and a username may authenticate successfully yet lack permission to read or write a node.

Provider Prerequisites and result
AnonymousProvider Endpoint must advertise an anonymous token. The server still decides authorization.
UsernameProvider(username, password) Endpoint must offer a usable username token policy. On a None endpoint, both two-argument constructors (String password or Supplier<byte[]>) skip validation of the certificate that encrypts the username token. If you need None with encrypted tokens, use the matching three-argument overload with a real validator.
X509IdentityProvider(certificate, privateKey) Endpoint must offer a compatible certificate user-token policy. The server must accept the user certificate and proof of possession.

By default, UsernameProvider can select a username policy of None, which leaves the password unencrypted inside the token, so on a None or Sign-only channel the password crosses the wire in cleartext. Select SignAndEncrypt with certificate validation, or enforce an encrypted token policy and validate its encrypting certificate as described in token-policy selection.

With a Supplier<byte[]> secret, Milo calls the supplier for each Session activation, including reactivation after reconnect, and zeroes the returned array afterward. Return a fresh password array on each call, because reusing the same array supplies a zero-filled password on the next activation.

Connect asynchronously

connect() blocks and returns the connected OpcUaClient. connectAsync() returns a CompletableFuture<OpcUaClient> instead, for applications that must not block a startup thread. Do not append .get() to connect(). The matching shutdown methods are disconnect() and disconnectAsync().

To bound how long startup waits, wait on the future with a timeout, for example client.connectAsync().get(10, TimeUnit.SECONDS). A timeout on a Java future wait only limits how long the application waits. It does not mean the server cancelled the request, and the client keeps trying to connect, so disconnect after a timeout just as you would after a failure.

When connecting fails

Blocking methods throw UaException, and asynchronous methods complete exceptionally. Log the nested OPC UA status and the stage that failed (transport, SecureChannel, Session creation, or Session activation), because the status usually points at the cause and not every failure is a bad password.

Status Usual cause and what to do
Bad_ConnectionRejected Nothing accepts connections at that address, for example because the server is stopped. Start the server or correct the host and port.
Bad_TcpEndpointUrlInvalid naming a URL that ends in /discovery The first discovery or selection attempt failed, and the server rejected the /discovery retry URL. Look for the original cause, such as a selection function that matched no endpoint.
Bad_ConfigurationError ("no endpoint selected") The selection function returned an empty Optional. Compare its criteria with the endpoints the server advertises.
An identity status during Session activation The server rejected the user identity. For wrong credentials, a Milo server reports Bad_IdentityTokenInvalid when the password was encrypted under a legacy RSA token policy such as Basic256Sha256, and Bad_UserAccessDenied otherwise. Check the credentials and the token policy.

Recovery and limits describes recovery responsibilities after a successful start, and Troubleshooting covers collecting evidence for other failures.

Disconnect at shutdown

Keep one client for ongoing work. When the application stops, delete the subscriptions you created, then disconnect the client, which closes the Session and stops the background retries. runClient shows that lifetime in a standalone program that works until the user presses Enter.

static void runClient(String endpointUrl) throws Exception {
  try {
    OpcUaClient client = connect(endpointUrl);
    try {
      System.out.println("Connected. Press Enter to disconnect.");
      System.in.read();
    } finally {
      client.disconnect();
    }
  } finally {
    Stack.releaseSharedResources();
  }
}

The inner finally disconnects the client. The outer one releases the event loop, executors, and timers that Milo shares across every client and server in the process, and it runs even when connect fails, because discovery already started them. Release shared resources only at process shutdown, never from a component while another Milo client or server still uses them.

If you supplied executors, listeners, certificate stores, or a shared reverse-connect manager, you keep ownership. Close them at application shutdown after their users stop, because disconnecting a client leaves them open. Do not block transport callbacks or subscription listeners waiting for work that needs the same executors.

Examples and reference

Next steps

You have a connected client that the rest of the application can share. To find the thermostat's nodes in the server's address space, continue with Browsing.

Related: Client SDK · Client security · Recovery and limits · Your first client

Clone this wiki locally