# Clients für den Dauerbetrieb

> Anleitung: Clients für den Dauerbetrieb: wie das SDK Verbindungsabbrüche erkennt, Sitzungen und Subscriptions wiederherstellt und was die Anwendung selbst übernimmt.

**Beispielanwendungen:**

- Client.DynamicNamespaces ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Client.DynamicNamespaces), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Client.DynamicNamespaces)): Adressiert Nodes über die Namespace-URI statt den Index und erneuert die Subscriptions nach jedem Wiederverbinden, sodass wechselnde Namespaces des Servers keine Rolle spielen.
- Server.DynamicNamespaces ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.DynamicNamespaces), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.DynamicNamespaces)): Stellt jeden Namespace über einen eigenen Node-Manager bereit und startet den Server mit einer wechselnden Auswahl davon neu.

Ein Client, der dauerhaft mit einer Anlage verbunden ist, muss Netzwerkausfälle, Neustarts des Servers und Wartungsfenster überstehen. Den größten Teil davon übernimmt das SDK: Es erkennt den Abbruch, verbindet sich selbstständig neu und stellt Subscriptions wieder her. Diese Seite fasst zusammen, was dabei automatisch geschieht und welche Aufgaben bei der Anwendung bleiben. Die einzelnen Einstellungen beschreibt der Abschnitt [Verbindungsüberwachung](https://www.traeger.de/docs/development/opcua/client/configuration#verbindungsuberwachung) der Konfiguration.

## Was das SDK bei einem Abbruch übernimmt

Beteiligte Typen: `OpcClient`, `OpcClientState`, `OpcClientKeepAlive`

Solange `UseBreakDetection` aktiviert ist (Standard), prüft der Client die Verbindung mit KeepAlive-Anfragen im Abstand von `KeepAlive.Interval` (Standard 5000 ms). Wenn eine KeepAlive-Anfrage fehlschlägt oder ein Service-Aufruf einen Verbindungsverlust meldet (zum Beispiel mit `BadConnectionClosed` oder `BadSessionIdInvalid`), läuft sofort der folgende Ablauf ab:

| Schritt | Was das SDK tut | Zustand und Ereignis |
|---|---|---|
| 1 | Der Client erkennt den Abbruch. | `Reconnecting`, Ereignis `Reconnecting` |
| 2 | Im Abstand von `ReconnectInterval` (Standard 10000 ms) versucht der Client, die bestehende Sitzung wiederherzustellen, längstens für `MaxReconnectDuration` (Standard 0, unbegrenzt). | bleibt `Reconnecting` |
| 3a | Die Sitzung lässt sich wiederherstellen: Der Client fordert die ausstehenden Benachrichtigungen seiner Subscriptions erneut an. | `Reconnected`, Ereignis `Reconnected` |
| 3b | Die Sitzung ist auf dem Server abgelaufen: Der Client erzeugt eine neue Sitzung und legt alle Subscriptions mit ihren MonitoredItems neu an. | `Connected`, Ereignis `Connected` |
| 4 | In beiden Fällen lädt der Client die Namespace-Tabelle des Servers neu. Wenn `UseDynamic` oder die Typauflösung aktiv ist, lädt er auch das Typsystem neu. | |

Für die Anwendung heißt das: Subscriptions, MonitoredItems und ihre Ereignishandler bleiben erhalten. Sie müssen nach einem Abbruch nichts neu abonnieren.

## Verbindungsereignisse im Überblick

Beteiligte Typen: `OpcClient`, `OpcClientState`

Ob die Verbindung zum Server besteht, ermittelt der Client über die KeepAlive-Logik und über die Ergebnisse seiner Service-Aufrufe. Wenn ein Aufruf wie `ReadNode` als Ganzes mit einem Status fehlschlägt, der auf eine verlorene Verbindung hinweist (zum Beispiel `BadConnectionClosed` oder `BadSessionIdInvalid`), ändert der Client seinen Zustand sofort und wartet nicht auf das nächste KeepAlive. Andere Fehler einzelner Aufrufe und ein schlechter Status einzelner Werte im Ergebnis ändern den Zustand nicht. Jede Zustandsänderung meldet der Client über die folgenden Ereignisse der Klasse `OpcClient`:

| Ereignis | Wann der Client es auslöst | Bedeutung |
|---|---|---|
| `Connecting`, `Connected` | beim Aufruf von `Connect` und nach einem Verbindungsverlust, wenn der Client dabei eine neue Sitzung erstellt hat | `Connecting` meldet den Beginn eines Verbindungsversuchs, `Connected` seinen erfolgreichen Abschluss. |
| `Reconnecting`, `Reconnected` | nach einem Verbindungsverlust, wenn `UseBreakDetection` aktiviert und `ReconnectInterval` größer als 0 ist | Nach `Reconnecting` versucht der Client im Abstand von `ReconnectInterval`, die Verbindung wiederherzustellen. `Reconnected` meldet, dass er die bestehende Sitzung weiterverwenden konnte; musste er eine neue Sitzung erstellen, löst er stattdessen `Connected` aus. |
| `Disconnecting`, `Disconnected` | beim Aufruf von `Disconnect` oder implizit von `Dispose`; außerdem nach einem Verbindungsverlust, wenn `ReconnectInterval` 0 ist oder `MaxReconnectDuration` abgelaufen ist (dann nur `Disconnected`, ohne vorheriges `Disconnecting`) | Die Verbindung ist manuell getrennt, oder der Client hat einen Verbindungsverlust festgestellt, den er nicht selbst behebt. |
| `BreakDetected` | nach einem Verbindungsverlust, wenn `UseBreakDetection` aktiviert und `ReconnectInterval` 0 ist oder `MaxReconnectDuration` abgelaufen ist, im Anschluss an `Disconnected` | Anlass, die Verbindung selbst wiederherzustellen (siehe [Selbst wiederverbinden](https://www.traeger.de/docs/development/opcua/client/long-running-clients#selbst-wiederverbinden)). |
| `StateChanged` | bei jeder Änderung der Eigenschaft `State`, jeweils vor den Ereignissen oben | allgemeine Überwachung aller Zustandswechsel (siehe [Zustandswechsel protokollieren](https://www.traeger.de/docs/development/opcua/client/long-running-clients#zustandswechsel-protokollieren)) |

Wenn `UseBreakDetection` deaktiviert ist, stellt der Client die Verbindung nicht automatisch wieder her. Solche Situationen muss dann Ihre Anwendung selbst behandeln.

## Beim Start verbinden

Beteiligte Typen: `OpcClient`, `OpcException`

Das automatische Wiederverbinden greift erst, wenn eine Verbindung einmal bestanden hat. Wenn der Server beim Start der Anwendung nicht erreichbar ist, löst `Connect` eine `OpcException` aus, und der Client kehrt in seinen vorherigen Zustand zurück. Ein Client für den Dauerbetrieb wiederholt den Verbindungsaufbau deshalb selbst:

```csharp
var client = new OpcClient("opc.tcp://localhost:4840/");
client.ReconnectInterval = 5000;
 
while (true) {
    try {
        client.Connect();
        break;
    }
    catch (OpcException ex) {
        Console.WriteLine("Connect failed: {0}", ex.Code);
        Thread.Sleep(TimeSpan.FromSeconds(5));
    }
}
```

```vbnet
Dim client = New OpcClient("opc.tcp://localhost:4840/")
client.ReconnectInterval = 5000

Do
    Try
        client.Connect()
        Exit Do
    Catch ex As OpcException
        Console.WriteLine("Connect failed: {0}", ex.Code)
        Thread.Sleep(TimeSpan.FromSeconds(5))
    End Try
Loop

```

## Subscriptions einmal anlegen

Beteiligte Typen: `OpcClient`, `OpcSubscription`, `OpcDataChangeReceivedEventArgs`

Legen Sie Subscriptions einmal nach dem ersten erfolgreichen `Connect` an. Weil das SDK sie bei einer neuen Sitzung selbst wiederherstellt, würde ein erneutes Abonnieren im Handler des Ereignisses `Connected` jede Subscription nach einem Abbruch verdoppeln:

```csharp
// Subscribe once after Connect: the client recreates the subscription for a new session.
OpcSubscription subscription = client.SubscribeDataChange(
        "ns=2;s=Machine/Speed",
        HandleDataChanged);
...
private static void HandleDataChanged(
        object sender,
        OpcDataChangeReceivedEventArgs e)
{
    Console.WriteLine("Speed: {0}", e.Item.Value);
}
```

```vbnet
'Subscribe once after Connect: the client recreates the subscription for a new session.
Dim subscription As OpcSubscription = client.SubscribeDataChange( _
        "ns=2;s=Machine/Speed", _
        AddressOf HandleDataChanged)
...
Private Shared Sub HandleDataChanged( _
        ByVal sender As Object, _
        ByVal e As OpcDataChangeReceivedEventArgs)
    Console.WriteLine("Speed: {0}", e.Item.Value)
End Sub

```

Wenn Benachrichtigungen ankommen, die sich nicht dekodieren lassen, meldet das SDK das über das Ereignis `NotificationFailed`, siehe [Benachrichtigungen behandeln](https://www.traeger.de/docs/development/opcua/client/subscriptions#benachrichtigungen-behandeln).

## Zustandswechsel protokollieren

Beteiligte Typen: `OpcClient`, `OpcClientState`, `OpcClientStateChangedEventArgs`

Für Betrieb und Fehlersuche ist es hilfreich, jeden Zustandswechsel zu protokollieren. Das Ereignis `StateChanged` tritt vor dem jeweiligen Einzelereignis auf und nennt alten und neuen Zustand. Der Wechsel von `Reconnecting` nach `Connected` zeigt an, dass der Client eine neue Sitzung erstellt hat:

```csharp
client.StateChanged += HandleStateChanged;
...
private static void HandleStateChanged(
        object sender,
        OpcClientStateChangedEventArgs e)
{
    Console.WriteLine("{0} -> {1}", e.OldState, e.NewState);
 
    if (e.OldState == OpcClientState.Reconnecting && e.NewState == OpcClientState.Connected)
        Console.WriteLine("New session, the subscriptions have been recreated.");
}
```

```vbnet
AddHandler client.StateChanged, AddressOf HandleStateChanged
...
Private Shared Sub HandleStateChanged( _
        ByVal sender As Object, _
        ByVal e As OpcClientStateChangedEventArgs)
    Console.WriteLine("{0} -> {1}", e.OldState, e.NewState)

    If e.OldState = OpcClientState.Reconnecting AndAlso e.NewState = OpcClientState.Connected Then
        Console.WriteLine("New session, the subscriptions have been recreated.")
    End If
End Sub
```

## Was nach einer neuen Sitzung zu tun bleibt

Beteiligte Typen: `OpcClient`, `OpcNodeIdCollection`

Alles, was der Server einer einzelnen Sitzung zuordnet, gilt nach einer neuen Sitzung nicht mehr. Das betrifft vor allem registrierte Nodes (siehe [Häufig gelesene Nodes registrieren](https://www.traeger.de/docs/development/opcua/client/read-write-values#haufig-gelesene-nodes-registrieren)). Die von `RegisterNodes` gelieferten Identifier gelten nur in der Sitzung, in der der Client sie registriert hat. Registrieren Sie die Nodes deshalb im Handler des Ereignisses `Connected`. Das Ereignis tritt beim ersten Verbinden und nach jeder neuen Sitzung auf:

```csharp
client.Connected += HandleConnected;
...
private OpcNodeId[] nodeIds = { "ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature" };
private OpcNodeIdCollection registeredNodeIds;
 
private void HandleConnected(object sender, EventArgs e)
{
    // Registered node ids are only valid in the session they have been registered in.
    this.registeredNodeIds = ((OpcClient)sender).RegisterNodes(this.nodeIds);
}
```

```vbnet
AddHandler client.Connected, AddressOf HandleConnected
...
Private nodeIds As OpcNodeId() = {"ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature"}
Private registeredNodeIds As OpcNodeIdCollection

Private Sub HandleConnected(ByVal sender As Object, ByVal e As EventArgs)
    'Registered node ids are only valid in the session they have been registered in.
    Me.registeredNodeIds = CType(sender, OpcClient).RegisterNodes(Me.nodeIds)
End Sub

```

Verwenden Sie für Nodes fremder Namespaces außerdem die Namespace-URI statt eines fest eingetragenen Index, siehe [Nodes über die Namespace-URI adressieren](https://www.traeger.de/docs/development/opcua/client/read-write-values#nodes-uber-die-namespace-uri-adressieren). Nach einem Neustart des Servers kann sich die Reihenfolge seiner Namespaces ändern.

## Selbst wiederverbinden

Beteiligte Typen: `OpcClient`

Wenn `ReconnectInterval` 0 ist, verbindet sich der Client nicht selbst neu. Er wechselt in den Zustand `Disconnected` und löst das Ereignis `BreakDetected` aus. Die Anwendung entscheidet dann selbst, wann und wie sie die Verbindung wieder aufbaut, zum Beispiel nach einer eigenen Wartezeit:

```csharp
client.ReconnectInterval = 0;
client.BreakDetected += HandleBreakDetected;
...
private static void HandleBreakDetected(object sender, EventArgs e)
{
    OpcClient client = (OpcClient)sender;
 
    // Decide on your own when and how to connect again.
    Task.Run(() => {
        Thread.Sleep(TimeSpan.FromSeconds(10));
        client.Connect();
    });
}
```

```vbnet
client.ReconnectInterval = 0
AddHandler client.BreakDetected, AddressOf HandleBreakDetected
...
Private Shared Sub HandleBreakDetected(ByVal sender As Object, ByVal e As EventArgs)
    Dim client As OpcClient = CType(sender, OpcClient)

    'Decide on your own when and how to connect again.
    Task.Run( _
            Sub()
                Thread.Sleep(TimeSpan.FromSeconds(10))
                client.Connect()
            End Sub)
End Sub
```

## Aus mehreren Threads zugreifen

Beteiligte Typen: `OpcClient`, `OpcSubscription`, `OpcTaskSchedule`

Eine Instanz von `OpcClient` können Sie aus mehreren Threads verwenden. Ihre Service-Aufrufe führt der Client dabei nacheinander aus: Jeder Aufruf sperrt den Client, bis die Antwort des Servers vorliegt oder die Zeit abgelaufen ist. Das betrifft die Services Read, Write, Browse, BrowseNext, Call und TranslateBrowsePathsToNodeIds, den Zugriff auf historische Daten, das Hinzufügen und Löschen von Nodes und Referenzen sowie das Registrieren von Nodes. Ein langsamer Aufruf hält damit alle anderen Aufrufe desselben Clients auf. `Connect` und `Disconnect` verwenden dieselbe Sperre.

Die Methode `BrowseNodes` läuft dagegen ohne diese Sperre. Sie liest über den Node-Cache des Stacks, und dieser Cache sperrt gleichzeitige Zugriffe nicht. Wir empfehlen deshalb, `BrowseNodes` eines Clients nicht aus mehreren Threads gleichzeitig aufzurufen.

Subscriptions arbeiten parallel zu den Service-Aufrufen. Ihre Benachrichtigungen verteilt das SDK standardmäßig auf Threads des .NET-Thread-Pools; Einzelheiten beschreibt die Seite [Abläufe im SDK](https://www.traeger.de/docs/development/opcua/internals).

Solange der Client die Verbindung wiederherstellt, scheitern neue Aufrufe sofort mit dem Status `BadServerNotConnected`. Aufrufe, die zu diesem Zeitpunkt schon auf eine Antwort warten, enden mit `BadConnectionClosed` oder `BadRequestInterrupted`. Behandeln Sie diese Fälle wie andere fehlgeschlagene Aufrufe (siehe [Fehler behandeln](https://www.traeger.de/docs/development/opcua/client/error-handling)).

## Checkliste

| Thema | Empfehlung |
|---|---|
| Start | `Connect` in einer Schleife wiederholen, bis der Server erreichbar ist. |
| Abbrucherkennung | `UseBreakDetection` aktiviert lassen, `KeepAlive.Interval` und `ReconnectInterval` an die Anlage anpassen, bei Bedarf `MaxReconnectDuration` begrenzen. |
| Subscriptions | einmal nach dem ersten `Connect` anlegen, nicht im Handler von `Connected`. |
| Registrierte Nodes | im Handler von `Connected` registrieren. |
| Threads | Service-Aufrufe eines Clients laufen nacheinander; `BrowseNodes` eines Clients nicht aus mehreren Threads gleichzeitig aufrufen. |
| NodeIds | Namespaces über die URI adressieren (`nsu=`). |
| Fehlerhafte Benachrichtigungen | `NotificationFailed` behandeln. |
| Fehler einzelner Operationen | Status auswerten, siehe [Fehler behandeln](https://www.traeger.de/docs/development/opcua/client/error-handling). |
