opcua-client · v4.6.x
Docs · Connection

Opening and closing

The connection lifecycle has three states and three transitions. Get the state machine right and most "the client just hung" bugs disappear.

The client's connection state is exposed as a three-case enum, ConnectionState:

text state machine
        ┌──────────────────────┐
        │     Disconnected     │ ← initial state
        └──────────────────────┘
                 │   ▲
        connect()│   │ disconnect()
                 ▼   │
        ┌──────────────────────┐
        │      Connected       │
        └──────────────────────┘
                 │   ▲
   I/O error,    │   │ reconnect()
   server reset, │   │  succeeds
   stale token   │   │
                 ▼   │
        ┌──────────────────────┐
        │       Broken         │
        └──────────────────────┘

Three states, three transitions:

From Transition To
Disconnected connect() Connected
Connected I/O error Broken
Broken reconnect() Connected
any disconnect() Disconnected

Read the state with $client->getConnectionState(). The boolean shortcut $client->isConnected() returns true only when the state is Connected.

Opening

There is no separate open() call. ClientBuilder::connect() returns a Client already in ConnectionState::Connected. Under the hood it performs:

  1. TCP open + HEL/ACK handshake
  2. GetEndpoints discovery, if needed
  3. OpenSecureChannel (OPN)
  4. CreateSession + ActivateSession

If any step fails, no Client is returned — connect() throws and the caller never sees a half-initialised object.

php examples/connect.php
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Exception\SecurityException;

try {
    $client = ClientBuilder::create()
        ->setTimeout(10.0)
        ->connect('opc.tcp://plc.local:4840');
} catch (ConnectionException $e) {
    // TCP failure, DNS error, HEL/ACK timeout, server refused, etc.
    throw $e;
} catch (SecurityException $e) {
    // Certificate validation, key load, OpenSSL primitive failure
    throw $e;
}

Configure the builder before calling connect() — the builder is the only configuration surface. Once you have a Client, the connection parameters (security, timeout, batching, …) are frozen until you build a new one.

Closing

Always disconnect() when you are done. The call sends CloseSession and CloseSecureChannel to the server and frees the TCP socket. Both messages can fail silently — disconnect() does not raise on the return trip, so it is safe in finally blocks.

php examples/scoped-client.php
$client = ClientBuilder::create()->connect('opc.tcp://plc.local:4840');

try {
    $client->read('i=2261');
    // … real work …
} finally {
    $client->disconnect();
}

Warning

A Client that has been disconnect()ed cannot be reused for normal operations — every service call will raise ConnectionException("Not connected: call connect() first"). To revive it, call reconnect(). The builder is a one-shot factory by design; if you need a second connection, build a second client.

Reconnecting

When a connection drops, the client transitions to ConnectionState::Broken on the next service call. Call reconnect() to rebuild the channel and session against the same endpoint, with the same configuration:

reconnect() opens a new secure channel and reactivates the existing session on it, so its subscriptions and monitored items keep running and SessionReactivated is dispatched. Notifications the server sent but you did not acknowledge are listed in availableSequenceNumbers on the next PublishResult; fetch them with republish(). If the server no longer knows the session, a new one is created and the subscriptions must be recreated — see Recipes · Recovering from disconnection. setReactivateSession(false) on the builder always creates a new session.

Resuming a session in another process

A session outlives the process that created it until its session timeout expires. Save it and reactivate it after a restart:

php resume after restart
use PhpOpcua\Client\Types\SessionState;

// before exiting (or periodically, to survive a crash)
file_put_contents($path, json_encode($client->suspend()));

// after the restart
$state = SessionState::fromArray(json_decode(file_get_contents($path), true));
$client = ClientBuilder::create()
    ->resumeSession($state)
    ->connect($state->endpointUrl);

suspend() closes the secure channel and the socket without closing the session; getSessionState() returns the same state without disconnecting. The state holds the session's authentication token and server nonce: store it with restrictive permissions. When the server rejects it, connect() creates a new session. Against UA-.NETStandard, an anonymous session resumed 15 s after the process crashed delivers every value its subscription sampled in the meantime.

Detecting a broken connection

A connection only flips to Broken after the client tries to use it. There is no background heartbeat in this library. The detection points are:

  • A blocking read or write on the socket fails or times out.
  • The server returns a session-invalid status (BadSessionIdInvalid, BadSessionNotActivated).
  • The secure channel is rejected (BadSecureChannelClosed).

When that happens, the client dispatches ClientDisconnected, sets the state to Broken, and raises the underlying exception to the caller. The next call into the client will raise ConnectionException("Connection broken: call reconnect()") — the state remains stuck on Broken until reconnect() succeeds or disconnect() is called.

Auto-retry

For transient failures inside a single call, the client supports an opt-in retry loop. Configure it on the builder:

php builder.setAutoRetry
$client = ClientBuilder::create()
    ->setAutoRetry(maxRetries: 3)
    ->connect('opc.tcp://plc.local:4840');

When maxRetries > 0, every service method routes through executeWithRetry. On a recoverable error it calls reconnect() and re-issues the request, up to maxRetries total tries. See Connection · Timeouts and retry for the classification of recoverable vs fatal failures.

Security token renewal

The server grants the secure channel a security token with a limited lifetime. At 75% of that lifetime the client renews it on the first request that follows, on the same channel and session, and dispatches SecureChannelRenewed. A connection idle past the lifetime renews on its next request. Renewal is on by default; setRenewSecurityToken(false) on the builder turns it off, and the server then rejects requests once the token expires.

Lifecycle events

The connection lifecycle is fully observable via PSR-14 events. Wire a dispatcher with setEventDispatcher() and listen for:

Event When
ClientConnecting connect() started
ClientConnected Session activated; client is usable
ConnectionFailed connect() raised — payload carries the exception
ClientDisconnecting disconnect() started
ClientDisconnected Disconnected (clean or broken — distinguish via prior ConnectionFailed / ClientDisconnecting)
ClientReconnecting reconnect() started
SecureChannelOpened / SecureChannelClosed OPN / CLO frames
SecureChannelRenewed Security token renewed
SessionCreated / SessionActivated / SessionClosed CreateSession / ActivateSession / CloseSession

See Observability · Event reference.