Skip to content

First Server

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

Applies to Milo 1.2.0-SNAPSHOT. Source baseline 7faaf4f05, 2026-10-04.

This program exposes one read-only Double Variable with value 21.5. It binds only to IPv4 loopback at opc.tcp://127.0.0.1:12686/wiki. Complete Getting started first, including the client and server SDK dependencies.

The endpoint uses anonymous access and SecurityPolicy.None for a local exercise. It has no channel encryption or peer authentication. Keep that loopback boundary; provision certificates, trust, and authorization with Server security and Access control before adapting it for a network deployment.

Complete server

Save this as src/main/java/org/eclipse/milo/examples/wiki/FirstServer.java:

package org.eclipse.milo.examples.wiki;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.EndpointConfig;
import org.eclipse.milo.opcua.sdk.server.ManagedNamespaceWithLifecycle;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.Stack;
import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransport;
import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransportConfig;

/** A loopback-only server for the first-client tutorial. */
public final class FirstServer {

  public static final String NAMESPACE_URI = "urn:eclipse:milo:wiki";

  private FirstServer() {}

  /**
   * Create a server that owns its tutorial namespace.
   *
   * @param port the loopback TCP port to bind.
   * @return an unstarted server; its caller must shut it down, including after failed startup.
   */
  public static OpcUaServer create(int port) {
    var endpoint =
        EndpointConfig.newBuilder()
            .setBindAddress("127.0.0.1")
            .setHostname("127.0.0.1")
            .setBindPort(port)
            .setPath("/wiki")
            .setSecurityPolicy(SecurityPolicy.None)
            .setSecurityMode(MessageSecurityMode.None)
            .addTokenPolicy(OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS)
            .build();

    var config =
        OpcUaServerConfig.builder()
            .setApplicationUri("urn:eclipse:milo:wiki:server")
            .setApplicationName(LocalizedText.english("Wiki server"))
            .setProductUri("urn:eclipse:milo:wiki")
            .setCertificateManager(new DefaultCertificateManager())
            .setEndpoints(Set.of(endpoint))
            .build();

    var server =
        new OpcUaServer(
            config,
            profile -> new OpcTcpServerTransport(OpcTcpServerTransportConfig.newBuilder().build()));
    server.addLifecycleParticipant(new TutorialNamespace(server));
    return server;
  }

  public static void main(String[] args) throws Exception {
    int port = args.length == 0 ? 12686 : Integer.parseInt(args[0]);
    OpcUaServer server = create(port);
    try {
      server.startup().get(10, TimeUnit.SECONDS);
      System.out.println(
          "Listening on opc.tcp://127.0.0.1:" + port + "/wiki; press Enter to stop.");
      System.in.read();
    } finally {
      try {
        server.shutdown().get(10, TimeUnit.SECONDS);
      } finally {
        Stack.releaseSharedResources();
      }
    }
  }

  private static final class TutorialNamespace extends ManagedNamespaceWithLifecycle {
    TutorialNamespace(OpcUaServer server) {
      super(server, NAMESPACE_URI);

      var temperature =
          UaVariableNode.builder(getNodeContext())
              .setNodeId(newNodeId("Temperature"))
              .setBrowseName(newQualifiedName("Temperature"))
              .setDisplayName(LocalizedText.english("Temperature"))
              .setDataType(NodeIds.Double)
              .setTypeDefinition(NodeIds.BaseDataVariableType)
              .setAccessLevel(AccessLevel.READ_ONLY)
              .setUserAccessLevel(AccessLevel.READ_ONLY)
              .setValue(new DataValue(new Variant(21.5)))
              .build();
      getNodeManager().addNode(temperature);
      temperature.addReference(
          new Reference(
              temperature.getNodeId(), NodeIds.Organizes, NodeIds.ObjectsFolder.expanded(), false));
    }
  }
}

Start and verify

After compiling with the command in Getting started, run this in the application directory:

java -cp "target/classes:$(cat target/classpath.txt)" org.eclipse.milo.examples.wiki.FirstServer

Run in the foreground with standard input attached. The application prints its endpoint URL and waits for Enter; closed input also stops it. Keep it running, then run FirstClient in another terminal. The client reads the Variable and prints Temperature: 21.5. You can also browse the Objects folder with an OPC UA client using anonymous identity and SecurityPolicy None to find Temperature.

The namespace URI is urn:eclipse:milo:wiki and the string identifier is Temperature. The namespace index is assigned by the server; clients must resolve the URI rather than hard-code an index. The inverse Organizes reference from Temperature to Objects appears to browsing clients as Objects Organizes Temperature. A Value write is rejected with Bad_NotWritable because AccessLevel lacks CurrentWrite. A read-only UserAccessLevel alone returns Bad_UserAccessDenied. Reading an unknown identifier returns Bad_NodeIdUnknown.

Lifecycle and failures

create() returns an unstarted server. addLifecycleParticipant() gives the server ownership of the namespace lifecycle, including registration and sampling. Do not also start or shut down that namespace independently. The namespace keeps the Variable in its NodeManager; no separate device or scheduler is involved in this constant-value exercise.

Press Enter in the server terminal to stop gracefully. End of input follows the same path. Ctrl+C stops the JVM without running this finally block. The program registers no shutdown hook, so Milo's graceful shutdown does not run; the operating system still releases its sockets. The program awaits shutdown and then releases process-wide Milo resources. In an application with other Milo components, defer that final shared-resource release until all have stopped. A stopped or failed server instance is not restartable; create a new instance.

If another process owns port 12686, the server logs Failed to bind endpoint at WARN. The wait throws ExecutionException caused by UaException with Bad_ConfigurationError and No endpoints bound. Stop the conflicting process or pass a free port as the program's first argument and use the matching client URL. The finally path also shuts down after failed startup. Startup and shutdown execute their work on the calling thread in this sequence. The ten-second future waits do not bound a stalled bind or lifecycle callback, since those can block before the future is returned.

Examples and reference

Server configuration explains endpoint and identity settings. Address space explains namespace registration and references. Data access replaces the constant value with application/device data.

The source module and reproduction checks contain the complete programs, fixtures, and validation tools.

Related pages

Getting started · First client · Server SDK · Server configuration · Address space · Deployment

Clone this wiki locally