# Anfragen überwachen

> Anleitung: Anfrage- und Antwort-Nachrichten des Servers überwachen, etwa zur Protokollierung oder für eigene Prüfungen, und Kennzahlen des Servers abfragen.

Beteiligte Typen: `OpcServer`, `OpcRequestValidatingEventArgs`, `OpcRequestValidatingEventHandler`, `OpcRequestProcessingEventArgs`, `OpcRequestProcessingEventHandler`, `OpcRequestProcessedEventArgs`, `OpcRequestProcessedEventHandler`, `OpcRequestValidatedEventArgs`, `OpcRequestValidatedEventHandler`, `IOpcServiceRequest`, `IOpcServiceResponse`

## Ablauf der Anfrageverarbeitung

Der Server verarbeitet und validiert die Anfragen der Clients als Instanzen der Schnittstelle `IOpcServiceRequest`. Er beantwortet sie mit Instanzen der Schnittstelle `IOpcServiceResponse`. Über die Ereignisse `RequestProcessing`, `RequestValidating`, `RequestValidated` und `RequestProcessed` können Sie die empfangenen Anfragen mit eigenen Methoden zusätzlich überwachen, protokollieren, lenken oder verweigern. Das ist besonders dann sinnvoll, wenn die Mechanismen des OPC UA .NET SDK den Anforderungen des Projekts nicht genügen, vor allem bei bestimmten Einschränkungen. Von der Anfrage (engl. Request) bis zur Antwort (engl. Response) durchläuft die Verarbeitung diese Schritte:
1. Empfang der Rohdaten einer Anfrage (Protokollebene des SDK)
1. Deserialisierung der Rohdaten zu einer Anfrage (Nachrichtenebene des SDK)
1. Vorverarbeitung der Anfrage: **Ereignis `RequestProcessing`**
1. Zuteilung der Anfrage zum zuständigen Dienst (Dienstebene des SDK)
1. Validierung der Anfrage
   1. Standardvalidierungen (Sitzung, Identität usw.)
   1. benutzerdefinierte Validierung: **Ereignis `RequestValidating`**
   1. abschließende Validierung (Prüfung der benutzerdefinierten Validierung)
   1. benutzerdefinierter Abschluss der Validierung: **Ereignis `RequestValidated`**
1. Verarbeitung der Anfrage (Anwendungsebene des SDK)
1. Erzeugen der Antwort über den zuständigen Dienst (Dienstebene des SDK)
1. Nachverarbeitung der Anfrage und ihrer Antwort: **Ereignis `RequestProcessed`**
1. Serialisierung der Antwort zu Rohdaten (Nachrichtenebene des SDK)
1. Senden der Rohdaten der Antwort (Protokollebene des SDK)

## Anfragen vorverarbeiten

Mit den Ereignissen aus den Schritten 3, 5.2, 5.4 und 8 überwachen oder beeinflussen Sie den Ablauf der Anfrageverarbeitung zum jeweiligen Zeitpunkt mit eigenem Code. Direkt nachdem der Server die Nutzdaten empfangen und als Instanz von `IOpcServiceRequest` aufbereitet hat, führt er den Code des Ereignisses `RequestProcessing` aus. Die Informationen dieses Ereignisses dienen vor allem der Diagnose des Nachrichtenverkehrs zwischen Client und Server.

Ein Eventhandler dieses Ereignisses sollte keine Exception auslösen:

```csharp
private static void HandleRequestProcessing(object sender, OpcRequestProcessingEventArgs e)
{
    Console.Write("Processing: " + e.Request.ToString());
}

// ...

server.RequestProcessing += HandleRequestProcessing;

```

```vbnet
Private Shared Sub HandleRequestProcessing(sender As Object, e As OpcRequestProcessingEventArgs)
    Console.Write("Processing: " & e.Request.ToString())
End Sub

' ...

AddHandler server.RequestProcessing, AddressOf HandleRequestProcessing

```

## Anfragen validieren

Der Kontext in den `OpcRequestProcessingEventArgs` ist stets eine Instanz der Klasse `OpcContext`. Sie beschreibt nur die allgemeine Umgebung der Nachrichtenverarbeitung. Im darauf folgenden Ereignis `RequestValidating` ergänzt der Server diese Informationen um Angaben zur Sitzung (engl. Session) und zur Identität (engl. Identity). Bei Anfragen, die eine Sitzung voraussetzen, ist der bereitgestellte `OpcContext` die Spezialisierung `OpcOperationContext`.

Über den `OpcOperationContext` können Sie dann zusätzlich sitzungsbezogen validieren:

```csharp
server.RequestValidating += HandleRequestValidating;
...
private static readonly Dictionary<OpcNodeId, int> nodesPerSession
        = new Dictionary<OpcNodeId, int>();
 
private static void HandleRequestValidating(
        object sender,
        OpcRequestValidatingEventArgs e)
{
    Console.Write(" -> Validating: " + e.Request.ToString());
 
    if (e.RequestType == OpcRequestType.AddNodes) {
        var sessionId = e.Context.SessionId;
        var request = (OpcAddNodesRequest)e.Request;
 
        lock (nodesPerSession) {
            int count;
            nodesPerSession.TryGetValue(sessionId, out count);
            count += request.Commands.Count;
 
            // Reject the request in case it would exceed 100 AddNode commands per session.
            if (count > 100)
                e.Cancel = true;
            else
                nodesPerSession[sessionId] = count;
        }
    }
}
```

```vbnet
AddHandler server.RequestValidating, AddressOf HandleRequestValidating
...
Private Shared ReadOnly nodesPerSession As New Dictionary(Of OpcNodeId, Integer)()

Private Shared Sub HandleRequestValidating( _
        ByVal sender As Object, _
        ByVal e As OpcRequestValidatingEventArgs)
    Console.Write(" -> Validating: " & e.Request.ToString())

    If e.RequestType = OpcRequestType.AddNodes Then
        Dim sessionId = e.Context.SessionId
        Dim request = CType(e.Request, OpcAddNodesRequest)

        SyncLock nodesPerSession
            Dim count As Integer
            nodesPerSession.TryGetValue(sessionId, count)
            count += request.Commands.Count

            'Reject the request in case it would exceed 100 AddNode commands per session.
            If count > 100 Then
                e.Cancel = True
            Else
                nodesPerSession(sessionId) = count
            End If
        End SyncLock
    End If
End Sub

```

Das gezeigte Beispiel beschränkt die Anzahl der Aufrufe von `AddNode` je Sitzung auf 100. Eine Anfrage, die die Beschränkung überschreiten würde, verweigert der Eventhandler. Dazu setzt er die Eigenschaft `Cancel` der Ereignisargumente auf `true`. Damit setzt das SDK den Code der Eigenschaft `Result` der Ereignisargumente automatisch auf `BadNotSupported`. Sie können die Anfrage auch abbrechen, indem Sie (zusätzlich) einen „Bad“-Code setzen. Ein Eventhandler des Ereignisses `RequestValidating` darf eine Exception auslösen.

Wenn der Eventhandler eine Exception auslöst, `Cancel` auf `true` setzt oder `Result` auf einen „Bad“-Code setzt, führt der Server die Eventhandler des Ereignisses `RequestValidated` nicht aus. Das entspricht dem Validating-Validated-Muster aus dem .NET Framework. Wenn kein Eventhandler die Anfrage abbricht, führt der Server die Eventhandler des Ereignisses `RequestValidated` aus:

```csharp
void HandleRequestValidated(object sender, OpcRequestValidatedEventArgs e)
{
    Console.Write(" -> Validated");
}

// ...

server.RequestValidated += HandleRequestValidated;

```

```vbnet
Sub HandleRequestValidated(sender As Object, e As OpcRequestValidatedEventArgs)
    Console.Write(" -> Validated")
End Sub

' ...

AddHandler server.RequestValidated, AddressOf HandleRequestValidated

```

Wie beim Ereignis `RequestProcessing` dienen die Informationen vor allem der Diagnose des Nachrichtenverkehrs zwischen Client und Server. Das Ereignis ist nützlich, weil der Server erst nach seinem Aufruf versucht, die Anfrage zu bearbeiten und zu beantworten. Ein Eventhandler dieses Ereignisses sollte keine Exception auslösen. Nachdem der Server die Anfrage verarbeitet hat, beantwortet er sie mit den resultierenden Ergebnissen.

## Anfragen nachverarbeiten

Die dabei erzeugte Antwort werten Sie zusammen mit der Anfrage im Ereignis `RequestProcessed` aus:

```csharp
private static void HandleRequestProcessed(object sender, OpcRequestProcessedEventArgs e)
{
    if (e.Response.Success)
        Console.WriteLine(" -> Processed!");
    else
        Console.WriteLine(" -> FAILED: {0}!", e.Exception?.Message ?? e.Response.ToString());
}

// ...

server.RequestProcessed += HandleRequestProcessed;

```

```vbnet
Private Shared Sub HandleRequestProcessed(sender As Object, e As OpcRequestProcessedEventArgs)
    If e.Response.Success Then
        Console.WriteLine(" -> Processed!")
    Else
        Console.WriteLine(" -> FAILED: {0}!", If(e.Exception?.Message, e.Response.ToString()))
    End If
End Sub

' ...

AddHandler server.RequestProcessed, AddressOf HandleRequestProcessed

```

Wie das Beispiel zeigt, liefern die Argumente des Ereignisses zusätzlich Informationen über eine gegebenenfalls aufgetretene Ausnahme (engl. Exception). Auch dieser Eventhandler sollte keine Exception auslösen. Welchen Service die Anfrage aufgerufen hat, nennt die Eigenschaft `RequestType`, auch bei Services, die der Server nicht unterstützt. Solche Anfragen erkennen Sie an der Eigenschaft `IsUnsupportedService`.

## Veraltete und auffällige Anfragen behandeln

Beteiligte Typen: `OpcServer`, `OpcRequestJudgementEventArgs`, `OpcRequestImpeachment`

Bevor der Server eine Anfrage verarbeitet, prüft er sie auf Auffälligkeiten (`OpcRequestImpeachment`), zum Beispiel ob sie veraltet ist, weil ihr Zeitstempel zu weit in der Vergangenheit liegt. Standardmäßig weist der Server eine solche Anfrage zurück. Über das Ereignis `RequestJudgement` können Sie diese Beurteilung übersteuern: Wenn Sie die Eigenschaft `Dismiss` der Argumente auf `true` setzen, verarbeitet der Server die Anfrage trotzdem. Das ist zum Beispiel hilfreich, wenn die Uhren von Client und Server voneinander abweichen:

```csharp
server.RequestJudgement += HandleRequestJudgement;
...
private static void HandleRequestJudgement(
        object sender,
        OpcRequestJudgementEventArgs e)
{
    // Accept outdated requests, e.g. in case the clocks of client and server differ.
    if (e.Impeachment == OpcRequestImpeachment.Outdated)
        e.Dismiss = true;
}
```

```vbnet
AddHandler server.RequestJudgement, AddressOf HandleRequestJudgement
...
Private Shared Sub HandleRequestJudgement( _
        ByVal sender As Object, _
        ByVal e As OpcRequestJudgementEventArgs)
    'Accept outdated requests, e.g. in case the clocks of client and server differ.
    If e.Impeachment = OpcRequestImpeachment.Outdated Then
        e.Dismiss = True
    End If
End Sub
```

## Sitzungen überwachen

Beteiligte Typen: `OpcServer`, `OpcSession`, `OpcSessionEventArgs`, `OpcSessionActivationFailedEventArgs`, `OpcSubscriptionEventArgs`

Neben den Anfragen lassen sich auch die Sitzungen und Subscriptions der Clients überwachen. Dazu stellt der Server unter anderem die Ereignisse `SessionCreated`, `SessionActivated`, `SessionActivationFailed`, `SessionClosing`, `SubscriptionCreated` und `SubscriptionDeleted` bereit. `SessionActivationFailed` tritt bei jeder abgelehnten Anmeldung ein (siehe [Audit-Ereignisse](https://www.traeger.de/docs/development/opcua/security/server#audit-ereignisse)). Die aktuell bestehenden Sitzungen liefert zudem die Methode `GetSessions` des Servers:

```csharp
server.SessionCreated += (sender, e) => Console.WriteLine("Session created: {0}", e.Session.Name);
server.SessionClosing += (sender, e) => Console.WriteLine("Session closing: {0}", e.Session.Name);
 
server.SubscriptionCreated += (sender, e) => Console.WriteLine(
        "Subscription {0} created in session {1}",
        e.Subscription.Id,
        e.Subscription.Session.Name);
```

```vbnet
AddHandler server.SessionCreated, Sub(sender, e) Console.WriteLine("Session created: {0}", e.Session.Name)
AddHandler server.SessionClosing, Sub(sender, e) Console.WriteLine("Session closing: {0}", e.Session.Name)

AddHandler server.SubscriptionCreated, Sub(sender, e) Console.WriteLine( _
        "Subscription {0} created in session {1}", _
        e.Subscription.Id, _
        e.Subscription.Session.Name)
```

## Gleichzeitige Anfragen behandeln

Beteiligte Typen: `OpcServer`, `OpcNodeManager`, `OpcReadVariableValueCallback`, `OpcWriteVariableValueCallback`, `IOpcNodeHistoryProvider`, `OpcNodeHistorian`

Der Server verarbeitet die Anfragen seiner Clients parallel, jede Anfrage in einem eigenen Task. Die Ereignisse `RequestProcessing`, `RequestValidating`, `RequestValidated` und `RequestProcessed` treten im Thread der jeweiligen Anfrage auf. Ihre Handler laufen deshalb für mehrere Anfragen gleichzeitig. Schützen Sie Daten, die die Handler gemeinsam nutzen, vor gleichzeitigen Änderungen.

Für die Callbacks Ihrer Anwendung gilt:

- **Lesen und Schreiben:** Die Callbacks `ReadVariableValueCallback` und `WriteVariableValueCallback` laufen ebenfalls parallel. Der Server sperrt dabei jeweils den einzelnen Node: Zugriffe auf denselben Node laufen nacheinander, Zugriffe auf verschiedene Nodes gleichzeitig. Eine Anfrage über mehrere Nodes ist deshalb nicht atomar. Wenn Werte mehrerer Nodes nur zusammen gültig sind, synchronisieren Sie den Zugriff auf die Datenquelle selbst.
- **Methoden:** Für die Callbacks von Methoden-Nodes setzt das SDK keine Sperre. Aufrufe aus verschiedenen Anfragen laufen deshalb gleichzeitig, auch für dieselbe Methode. Nur innerhalb einer Anfrage führt der Server synchrone Methoden nacheinander aus. Mit `UseAsyncMethodCalls` führt er auch dort jeden Aufruf in einem eigenen Task aus (siehe [Asynchrone Methoden](https://www.traeger.de/docs/development/opcua/server/node-types#asynchrone-methoden)). Schreiben Sie die Callbacks threadsicher und schützen Sie Daten, die eine Methode mit anderen Callbacks teilt, selbst.
- **Historische Daten:** Einen History Provider ruft der Server ohne Sperre des Node-Managers auf. `OpcNodeHistorian` und `OpcNodeHistory<T>` sperren sich selbst. Ein eigener `IOpcNodeHistoryProvider` muss gleichzeitige Aufrufe selbst absichern (siehe [Leseanfragen beantworten](https://www.traeger.de/docs/development/opcua/server/historical-data#leseanfragen-beantworten)).

Die Eigenschaft `SyncRoot` des Node-Managers verwendet der Server beim Lesen und Schreiben nicht. Eine eigene Sperre auf `SyncRoot` schützt Callbacks deshalb nicht vor gleichzeitigen Anfragen.

## Diagnose

Beteiligte Typen: `OpcServer`, `OpcServerDiagnostics`, `OpcSession`, `OpcSessionDiagnostics`, `OpcServiceCounter`, `OpcSubscription`, `OpcSubscriptionDiagnostics`

Für Überwachung, Health Checks oder ein Betriebsprotokoll stellt der Server Kennzahlen bereit, die er im Betrieb ohnehin erfasst. Vor dem Start des Servers sind alle Werte 0.

### Kennzahlen des Servers

Die Eigenschaft `Diagnostics` des Servers liefert die Zahl der aktuellen und der seit dem Start insgesamt erstellten Sitzungen und Subscriptions. Außerdem liefert sie die Zahl abgelehnter Sitzungen und Anfragen sowie die Zahl abgebrochener und abgelaufener Sitzungen:

```csharp
OpcServerDiagnostics diagnostics = server.Diagnostics;
 
Console.WriteLine(
        "Sessions: {0} (total {1})",
        diagnostics.CurrentSessionCount,
        diagnostics.CumulatedSessionCount);
 
Console.WriteLine(
        "Subscriptions: {0}",
        diagnostics.CurrentSubscriptionCount);
 
Console.WriteLine(
        "Rejected sessions: {0}, by security: {1}",
        diagnostics.RejectedSessionCount,
        diagnostics.SecurityRejectedSessionCount);
 
Console.WriteLine(
        "Rejected requests: {0}, by security: {1}",
        diagnostics.RejectedRequestsCount,
        diagnostics.SecurityRejectedRequestsCount);
 
Console.WriteLine(
        "Session timeouts: {0}, aborts: {1}",
        diagnostics.SessionTimeoutCount,
        diagnostics.SessionAbortCount);
```

```vbnet
Dim diagnostics As OpcServerDiagnostics = server.Diagnostics

Console.WriteLine( _
        "Sessions: {0} (total {1})", _
        diagnostics.CurrentSessionCount, _
        diagnostics.CumulatedSessionCount)

Console.WriteLine( _
        "Subscriptions: {0}", _
        diagnostics.CurrentSubscriptionCount)

Console.WriteLine( _
        "Rejected sessions: {0}, by security: {1}", _
        diagnostics.RejectedSessionCount, _
        diagnostics.SecurityRejectedSessionCount)

Console.WriteLine( _
        "Rejected requests: {0}, by security: {1}", _
        diagnostics.RejectedRequestsCount, _
        diagnostics.SecurityRejectedRequestsCount)

Console.WriteLine( _
        "Session timeouts: {0}, aborts: {1}", _
        diagnostics.SessionTimeoutCount, _
        diagnostics.SessionAbortCount)
```

Wenn die Werte `SecurityRejectedSessionCount` und `SecurityRejectedRequestsCount` steigen, lehnt der Server Sitzungen oder Anfragen aus Sicherheitsgründen ab. Ursachen sind zum Beispiel ein nicht vertrauenswürdiges Zertifikat oder eine fehlgeschlagene Anmeldung. Das kann auf eine fehlerhafte Konfiguration eines Clients oder auf einen Angriff hinweisen. Die Schutzmechanismen des Servers beschreibt die Seite [Server absichern](https://www.traeger.de/docs/development/opcua/security/server).

### Sitzungen und Subscriptions untersuchen

Die Methode `GetSessions` liefert die bestehenden Sitzungen. Jede Sitzung nennt unter anderem ihren Namen, den verwendeten Endpunkt (`EndpointUrl`), die angemeldete Identität (`UsedIdentity`) und den Zeitpunkt des Verbindungsaufbaus. Ihre Eigenschaft `Diagnostics` zählt je Dienst die Aufrufe und die fehlgeschlagenen Aufrufe (zum Beispiel `ReadCounter`, `WriteCounter`, `CallCounter` und `TotalRequestCounter`). Außerdem zählt sie unberechtigte Anfragen (`UnauthorizedRequestsCount`). Die Eigenschaft `Diagnostics` einer Subscription zählt unter anderem gesendete Benachrichtigungen und Überläufe ihrer Warteschlangen. Beide Eigenschaften können `null` sein:

```csharp
foreach (OpcSession session in server.GetSessions()) {
    Console.WriteLine(
            "{0} {1} {2} since {3:u}",
            session.Name,
            session.EndpointUrl,
            session.UsedIdentity,
            session.ConnectTime);
 
    OpcSessionDiagnostics sessionDiagnostics = session.Diagnostics;
 
    if (sessionDiagnostics != null) {
        Console.WriteLine(
                "  requests: {0}, failed: {1}",
                sessionDiagnostics.TotalRequestCounter.TotalCalls,
                sessionDiagnostics.TotalRequestCounter.FailedCalls);
 
        Console.WriteLine(
                "  unauthorized: {0}, writes: {1}",
                sessionDiagnostics.UnauthorizedRequestsCount,
                sessionDiagnostics.WriteCounter.TotalCalls);
    }
 
    foreach (OpcSubscription subscription in session.GetSubscriptions()) {
        OpcSubscriptionDiagnostics subscriptionDiagnostics = subscription.Diagnostics;
 
        if (subscriptionDiagnostics != null) {
            Console.WriteLine(
                    "  subscription {0}: queue overflows {1}",
                    subscription.Id,
                    subscriptionDiagnostics.MonitoringQueueOverflowCount);
        }
    }
}
```

```vbnet
For Each session As OpcSession In server.GetSessions()
    Console.WriteLine( _
            "{0} {1} {2} since {3:u}", _
            session.Name, _
            session.EndpointUrl, _
            session.UsedIdentity, _
            session.ConnectTime)

    Dim sessionDiagnostics As OpcSessionDiagnostics = session.Diagnostics

    If sessionDiagnostics IsNot Nothing Then
        Console.WriteLine( _
                "  requests: {0}, failed: {1}", _
                sessionDiagnostics.TotalRequestCounter.TotalCalls, _
                sessionDiagnostics.TotalRequestCounter.FailedCalls)

        Console.WriteLine( _
                "  unauthorized: {0}, writes: {1}", _
                sessionDiagnostics.UnauthorizedRequestsCount, _
                sessionDiagnostics.WriteCounter.TotalCalls)
    End If

    For Each subscription As OpcSubscription In session.GetSubscriptions()
        Dim subscriptionDiagnostics As OpcSubscriptionDiagnostics = subscription.Diagnostics

        If subscriptionDiagnostics IsNot Nothing Then
            Console.WriteLine( _
                    "  subscription {0}: queue overflows {1}", _
                    subscription.Id, _
                    subscriptionDiagnostics.MonitoringQueueOverflowCount)
        End If
    Next
Next
```

### Diagnose-Nodes im Adressraum

Clients lesen die Diagnose eines Servers über die Diagnose-Nodes in seinem Adressraum, zum Beispiel ServerDiagnosticsSummary und die Diagnose-Objekte je Sitzung und je Subscription. Diese Nodes pflegt der Server nur, wenn die Eigenschaft `Diagnostics.IsEnabled` den Wert `true` hat (standardmäßig `false`). Auf die Kennzahlen und die Eigenschaften `Diagnostics` der Sitzungen und Subscriptions in den Abschnitten oben wirkt sich die Einstellung nicht aus. Setzen Sie die Eigenschaft vor dem Start des Servers:

```csharp
server.Diagnostics.IsEnabled = true;
server.Start();
```

```vbnet
server.Diagnostics.IsEnabled = True
server.Start()
```
