VNC Server Protokoll verstehen

Aus Hackerspace Ffm
Version vom 20. November 2023, 02:27 Uhr von Tut (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Hintergrund

Das VNC Protokoll eignet sich gut, um es auch bei Microcontrollern mit Netzwerkzugang wie z.B. den ESP32 / ESP8266 zu nutzen. Damit lassen sich Bildschirminhalte von Displays in Echtzeit übertragen, was bei der Entwicklung von Nutzen sein kann. Sogar "Headless" Anwendungen sind denkbar, also grafische Anwendungen auf dem ESP laufen lassen, der selbst kein Display hat, wo die Steuerung dann ausschließlich über VNC erfolgt.

Arduino/PlattformIO Beispiel auf Github

Protokoll

Das Protokoll ist in der RFC6143 beschrieben. Die Komplexität würde ich als mittelschwer beschreiben: Also nicht ganz so einfach wie ein HTTP-Request, aber auch super komplex. Ich habe erfolgreich einen VNC Server auf TCP-Basis programmiert, mit dem der Inhalt eines kleinen Displays übertragen und manipulitert werden kann.

Während in der RFC die Details des Protokolls gut beschrieben sind, gibt es einige Dinge, die noch etwas anders beschrieben werden können.

Zwei Haupzustände der VNC Verbindung

Eine VNC Verbindung kann im wesentlichen in zwei Hauptzustände unterteilt werden:

  1. Verbindungsaufbau
  2. Fernsteuerung

Verbindungsaufbau

Während des Verbindungsaufbaus funktioniert das Prokoll im strengen Halbduplex (Handshake): Es folgt einer strikten Struktur und es gibt immer genau eine Anfrage und genau eine Antwort darauf. Die Nachrichten sind hier genau vorgegeben und haben daher typischerweise kein separates Header-Byte, was die Art der Nachricht vorgibt.

Für eine Verbindung ohne Passwort-Authentifikation sieht das wie folgt aus und kann recht simpel "hart" kodiert werden:

  1. Client baut TCP Verbindung zum Server aus, Server nimmt diese an.
  2. Server sendet "RFB 003.008\n" - das ist die derzeit typische Protokollversion.
  3. Client antwortet ebenfalls mit "RFB 003.008\n" - und bestätigt damit, das er diese Protkollversion akzeptiert.
  4. Server sendet 0x01 0x01 - Er sagt damit, dass er nur den Security Handshake "None" unterstützt, also gar keine Authentifikation. Natürlich könnte man hier auch komplexere Handshakes erlauben, der Einfachheit halber soll das aber hier genügen.
  5. Client antwortet mit 0x01 - Er sagt damit, dass er den Security Handshake "None" akzeptiert.
  6. Server sendet 0x00 0x00 0x00 0x00 - Er sagt damit, dass der Security Handshake erfolgreich war und er die Verbindung akzeptiert.
  7. Client sendet nun genau ein Byte, entweder 0x00 oder 0x01. Bei einer 0x00 wünscht er eine exlusive Verbindung zum Server - der Server sollte also bestehende Verbindungen trennen. Bei einer 0x01 braucht es nicht exlusiv zu sein. Ob 0x00 oder 0x01 ist für einfache Anwendungen unwichtig, ggf lässt der Simple-Server ja eh nur eine Verbindung zu.
  8. Server sendet nun die sog. ServerInit Message: Diese enthält die Auflösung und das Farbformat des Framebuffers sowie den Namen der Verbindung. Dieses PIXEL_FORMAT ist allerdings nur eine Art maximale Empfehlung für den Client, denn dieser kann das PIXEL_FORMAT jederzeit ändern, was zumindest bei RealVNC auch häufig passiert. Statt das z.B. die Farbpixel mit 24Bit übertragen werden (typischerweise dann als 32 Bit Werte), kann der Client z.B. ein 8 Bit Farbformat anfragen, um schneller ein erstes Bild zu erhalten. Wichtig ist hier daher, dass auch bei einfachsten Implementierungen des Servers trotzdem verschiedene PIXEL_FORMAT Nachrichten richtig ausgewertet werden müssen.
  9. Übergang in den Fernsteuerungsmodus: siehe nächstes Kapitel. Typischerweise kommen hier gleich mehrere Pakete hintereinander vom Client, ohne dass der Server irgendwas sendet. Bei RealVNC wurden die folgenden 3 Pakete beobachtet:
    1. SetEncodings
    2. SetPixelFormat
    3. FramebufferUpdateRequest

Fernsteuerung

Am Ende davon geht das Protokoll in den Fernsteuerungsmodus über - hier ist die Situation anders: Es gibt nun kein festes Handshake mehr, stattdessen werden verschiedene Nachrichtentypen unabhängig voneinander übertragen. So kann z.B. der Client in freier Folge Nachrichten senden, wenn er Bildschirmupdates haben möchte oder wenn er Tastatur- oder Mausereignisse hat. Der Server sendet im wesentlichen Nachrichten vom Typ FramebufferUpdate und updatet damit die Bildschirmdarstellung auf dem Client.

Im Fernsteuerungsmodus sind die Nachrichten über das erste Byte, das Type-Byte kodiert. Es gibt allerdings keine direkte Info, wie lang ein Paket ist. Damit der Server die Nachrichten auseinander nehmen kann, muss er auch für die einfachste Implementierung zumindest die folgenden Nachrichtentypen soweit parsen können, dass er die Längen richtig einliest:

Client-Server Nachrichten

Nummer Name Länge
0 SetPixelFormat 19 Byte
2 SetEncodings variable Länge
3 FramebufferUpdateRequest 10 Byte
4 KeyEvent 8 Byte
5 PointerEvent 6 Byte
6 ClientCutEvent variable Länge

Server-Client Nachrichten

Für den Server ist die Sache einfacher, es gibt zwar 4 verschiedene Nachrichten (FramebufferUpdate, SetColorMapEntries (nur bei Paletten Farbformat), Bell und ServerCutText), aber eigentlich macht FramebufferUpdate die ganze Arbeit.

Die FramebufferUpdate-Nachricht kann verschiedene rechteckige Bereiche mit neuen Bilddaten auffrischen und dazu verschiene Codierungen nutzen. Die Kodierungen werden über SetEncodings abgeglichen, aber für die Einfachstimplementierung geht immer die RAW-Kodierung, die einfach die Rohdaten ohne Kompression allerdings im jeweils aktuellen PIXEL_FORMAT überträgt.

PIXEL_FORMAT

Wie schon oben erwähnt kann der Client jederzeit über eine SetPixelFormat-Nachricht das aktuelle PIXEL_FORMAT verändern - und zumindest RealVNC macht davon auch Gebrauch. Daher muss auch eine Einfachimplementation hier adequat reagieren.

Bits-per-Pixel

Die Zahl in diesem Byte bestimmt, wie viele Bytes pro Pixel übertragen werden. Lauf RFC sind hier genau die folgenden drei Werte erlaubt:

  1. 8 (true-color-flag beachten)
  2. 16 (big-endian-flag beachten)
  3. 32 (big-endian-flag beachten)

Obwohl eine Farbtiefe von 24 Bit nicht unüblich ist, werden hierzu immer 32 Bit (= 4 Byte) übertragen. Eine 3 Byte Übertragung ist nicht vorgesehen.

Depth

In diesem Byte wird die echte Farbtiefe übertragen. Allerdings scheint das den Client nicht soviel zu interessieren...

Big-Endian-Flag

Ist Bits-per-Pixel größer als 8 bestimmt das Flag die Reihenfolge mit der die Bytes übertragen werden. Da der Client hier freie Wahl hat, muss auch der Einfach-Server hier beide Varianten unterstützen.

True-Color-Flag

Wenn dieses Flag gesetzt ist, wird nicht mit Farbpaletten gearbeitet sondern mit MAX und SHIFT-Werten für die Farben. Ich habe zumindest nicht beobachet, dass der Client dieses Bit anders setzt als bei der initialen Aushandlung vom Server vorgegeben. Für die einfache Implementation setze ich daher dieses Bit in der Server-Nachricht und benötige dann keine Farbpaletten-Nachrichten mehr.

Max und Shift Werte

Im True-Color-Modus werden diese Werte vom Client für alle drei Farbkomponenten übergeben und bestimmen, wie die Farbe übertragen wird. Für 24 Bit Farbtiefe werden je 8 Bits pro Farbe genommen. In der Einstellung "LOW" bei RealVNC wurde die Farbtiefe auf 2 Bit pro Farbe vom Client reduziert. Die PIXEL_FORMAT Nachricht wurde in diesen Fällen wie folgt bestückt:

Type Description Je 8 Bit pro Farbe Je 2 Bit pro Farbe
U8 bits-per-pixel 32 8
U8 depth 24 6
U8 big-endian-flag 0 0
U8 true-color-flag 1 1
U16 red-max 255 3
U16 green-max 255 3
U16 blue-max 255 3
U8 red-shift 16 4
U8 green-shift 8 2
U8 blue-shift 0 0