JSON-RPC mit C# am Beispiel des Kodi MediaPlayers

JSON-RPC ist ein statusloses, leichtgewichtiges Protokoll zum Aufruf entfernter Methoden bzw. Funktionen in verteilten Systemen. JSON steht dabei für JavaScript Object Notation und RPC für Remote Procedure Call. Bei der Spezifikation wurde darauf geachtet, dass JSON-RPC möglichst keine unnötige Komplexität enthält und über verschiedene Kommunikationsprotokolle genutzt werden kann (es ist z.B. möglich HTTP oder Websockets zu verwenden). Da alle Anfragen und Antworten eine ID enthalten ist auch eine asynchrone Verarbeitung möglich (über die ID kann einer Anfrage dann leicht die Antwort zugeordnet werden). Dadurch ist das Protokoll flexibel einsetzbar und es kommt sowohl in Enterprise-Applikationen als auch in sonstigen Anwendungen (wie dem Kodi MediaPlayer) zum Einsatz. In diesem Beitrag wird nun beschrieben wie man einen JSON-RPC Request an den Kodi MediaPlayer sendet um z.B. Filminformationen abzurufen.

Bevor es an die eigentliche Implementierung geht zunächst einmal noch ein paar Grundlagen zum Protokoll und dann die Implementierung eines JSON-Requests in Verbindung mit Kodi. Zunächst einmal ein paar Details zu JSON.

JSON

Die JavaScript Object Notation, kurz JSON, ist ein kompaktes Datenformat in einer einfach lesbaren Textform zum Zweck des Datenaustauschs zwischen Anwendungen. JSON ist aber unabhängig von der Programmiersprache JAVA und Parser existieren in praktisch allen verbreiteten Sprachen. JSON wird zur Übertragung und zum Speichern von strukturierten Daten eingesetzt; es dient als Datenformat bei der Serialisierung. Insbesondere bei Webanwendungen und mobilen Apps wird es in Verbindung mit JavaScript, Ajax oder WebSockets zum Transfer von Daten zwischen dem Client und dem Server häufig genutzt. Im Gegensatz zu XML ist die Syntax von JSON einfacher gestaltet und erscheint daher oft lesbarer und insbesondere leichter schreibbar. In der Regel reduziert JSON auch den Overhead im Vergleich zu XML. Hier mal ein Beispiel:

Und hier mal das Pedant in XML:

Hier wird deutlich, dass JSON im Gegensatz zu XML weniger Overhead mit sich bringt. Weitere Informationen gibt es hier: JSON

Anfrage (Requests)

Ein JSON-RPC-Request besteht aus einem JSON-Objekt, das vom Client an einen Server geschickt wird. Dabei besitzt dieser Request den folgenden Aufbau:

MemberBeschreibungHinweis
jsonrpcEin String, welcher die JSON-RPC Version spezifiziert. Für Version 2.0 muss hier als Wert "2.0" stehen, wohingegen bei Version 1.0 keine Angabe gemacht werden muss.Unterschiede zwischen 1.0 und 2.0 beachten
methodEin String mit dem Namen der Funktion, die aufgerufen werden soll.
paramsEin Array oder Objekt mit den Parametern, die der Funktion übergeben werden sollen. Objekt ist erst ab Version 2.0 verfügbarUnterschiede zwischen 1.0 und 2.0 beachten.
idEin eindeutiger Identifikator für die Nachricht. Kann jeden beliebigen Datentyp haben (i. d. R. Integer). Sollte auf jeden Fall gesetzt werden, da der Server sonst davon ausgehen könnte, dass es sich um eine Benachrichtigung (Notification) handelt und somit evtl. keine Antwort gesendet wird.

Hinweis zu Notifications (Benachrichtigungen): Eine Notification zeichnet sich dadurch aus, dass das id-Feld nicht gesetzt bzw. NULL ist. Hier gibt es ebenfalls Unterschiede zwischen Version 1.0 und 2.0.

  • JSON-RPC Version 2.0: id fehlt
  • JSON-RPC Version 1.0: id NULL

Beispiele für eine gültige Anfrage:

{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}

oder

{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}

Antwort (Response)

Hat man einen Request an den Server gesendet so schickt dieser nach der Ausführung der angeforderten Methode eine Antwort als JSON-Objekt zurück an den Client. Die Bestandteile eines Response-Objektes sind in der folgenden Tabelle aufgelistet:

MemberBeschreibungHinweis
jsonrpcEin String, welcher die JSON-RPC Version spezifiziert. Für Version 2.0 muss hier als Wert "2.0" stehen, wohingegen bei Version 1.0 keine Angabe gemacht werden muss.Unterschiede zwischen 1.0 und 2.0 beachten
resultDas Rückgabeobjekt der Funktion, falls kein RPC-Fehler auftrat.

Andernfalls:

  • Version 2.0: wird dieses Feld weggelassen
  • Version 1.0: hat dieses Feld den Wert NULL
Unterschiede zwischen 1.0 und 2.0 beachten.
errorDas Fehlerobjekt, falls ein RPC-Fehler auftrat.

Andernfalls:

  • Version 2.0: wird dieses Feld weggelassen
  • Version 1.0: hat dieses Feld den Wert NULL
Unterschiede zwischen 1.0 und 2.0 beachten.
idEnthält den gleichen Wert wie id des Requests, der diese Response verursacht hat.

Beispiele für einen gültige Antwort (entsprechende Requests weiter oben -> Zuordnung auf Basis der ID):

{"jsonrpc": "2.0", "result": 19, "id": 1}

oder

{"jsonrpc": "2.0", "result": 19, "id": 3}

Tritt während der Verarbeitung ein Fehler auf liefert der Server ein entsprechendes Error-Objekt zurück. In der nachfolgenden Tabelle der Aufbau dieses Objekts:

Fehler (Error)

MemberBeschreibung
codeEin numerischer Wert (Integer), der den Fehler genauer spezifiziert.
messageEine kurze Beschreibung des Fehlers.
dataGenauere Fehlerbeschreibung in Form von einfachem Text oder eines "komplexeren" Objekts. Wird vom Server festgelegt.

Dabei gibt es vordefinierte Error-Codes. Hier eine Übersicht:

CodeNachrichtBedeutung
-32700Parsing-FehlerServer hat einen ungültiger JSON-Aufbau empfangen (Fehler beim Parsen des empfangenen JSON-Textes)
-32600Ungültiger RequestDie gesendete JSON ist kein gültiges Request-Objekt
-32601Methode nicht gefundenDie aufzurufende Methode wurde nicht gefunden bzw. existiert nicht.
-32602Ungültige ParameterUngültige Parameter für die aufzurufende Methode
-32603Interner FehlerInterner JSON-Fehler
-32000 bis
-32099
Sever-FehlerPlatzhalter für implementierunsabhängige Server-Fehler

Beispiele:

RPC-Call einer nicht existierenden Methode:

{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}

RPC-Call mit einer ungültigen JSON

{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

Weitere Beispiele können der JSON-RPC 2.0 Spezifikation entnommen werden.

Mit diesen Grundlagen können wir uns dann an die eigentliche Implementierung machen. Wie weiter oben schon geschrieben ist das JSON-Format unabhängig von der Programmiersprache und hier gibt es auch für .NET entsprechende Implementierungen.

Demo-Applikation

In der der Demo soll ein JSON-Request an einen Kodi MediaPlayer gesendet werden um eine Liste mit Filmen abzurufen. Kodi bietet die Möglichkeit, beim Benutzer vorliegende Inhalte in einer Bibliothek zu organisieren. Durch das Abrufen zusätzlicher Metadaten aus dem Internet ermöglicht Kodi die Kategorisierung von Inhalten, beispielsweise von Musik nach Genre, Filmen nach Schauspielern oder die Zuordnung einzelner Episoden einer Fernseh-Serie zu einer bestimmten Staffel. Dabei können nicht nur Mediendaten abgerufen werden es ist darüber hinaus möglich den Player via JSON-RPC zu steuern (z.B. Abspielen von Inhalten, Scannen nach Medien, usw.). In dieser einfachen Demo-Applikation soll lediglich eine Methode aufgerufen werden, die dann eine Liste mit vorhandenen Filmen zurückliefert. Die JSON-RPC Implementierung des Kodi MediaPlayers basiert auf der JSON-RPC 2.0 Spezifikiation. Dabei kann die Schnittstelle entweder via HTTP oder über eine TCP-Socket-Verbindung angesprochen werden (im Beispiel wird HTTP verwendet). Ebenso ist eine Websocket-Verbindung möglich.

Die Demo-Applikation wird eine einfache Konsolenanwendung, die einen fest vorgegebenen Request an den Server sendet und das Ergebnis ausgibt. Durch die weite Verbreitung von JSON gibt es eigentlich in allen möglichen Programmiersprachen fertige Frameworks um JSON direkt nutzen zu können. So auch für .NET das Framework Json.NET. Dieses Framework ist im .NET Umfeld sehr populär und bietet ziemliche viele Funktionen (zu viele um diese hier alle zu beschreiben -> daher werde ich demnächst mal einen eigenen Artikel zu diesem Framework schreiben). Was kann man jetzt mit diesem Framework machen: Man kann beliebige .NET-Objekte im JSON-Format serialsieren bzw. deserialsieren. Also fangen wir mal an und installieren Json.NET über den NuGet Paketmanager:

Newtonsoft_Json

Damit wäre die Solution vorbereitet und es kann mit der Implementierung losgehen. Weiter oben wurde ja schon beschrieben wie ein JSON-RPC Request für die Version 2.0 aufgebaut sein muss. Dafür erstellen wir jetzt eine einfache Klasse und nutzen das Json.NET-Framework um diese Klasse in das JSON-Format serialisieren zu können. Diese Klasse sieht wie folgt aus:

Wie zu sehen ist dies eine ganz einfache aufgebaute Klasse. Besonderheit hier ist, dass das Attribut JsonProperty verwendet wird. Damit wird das Json.NET-Framework angewiesen die jeweilige Eigenschaft mit den angegeben Namen zu serialisieren (neben dem Namen gibt es noch weitere Eigenschaften, die gesetzt werden können). Mehr muss man nicht tun! Auf den Request folgt dann die Response und dafür erstellen wir jetzt ebenfalls eine Klasse:

Damit wäre die Response auch fertig und der Vollständigkeit halber noch die Klasse für einen JSON-RPC Exception:

Jetzt haben wir auch schon alle benötigten Klassen um einen JSON-RPC Request zum Server zu senden. Doch wie sieht nun ein gültiger Request für den Kodi MediaPlayer aus? Dazu werfen wir mal einen Blick die API-Dokumentation: JSON-RPC API und JSON-RPC API/v6. Dort ist die GetMovies-Methode beschrieben und diese ist wie folgt definiert:

Kodi_JsonRpc_GetMovies

Wie man sehen kann erwartet diese Methode 4-Parameter:

  • properties – eine Art Feldliste ähnlich einem SELECT
  • limits – hierüber kann man die Ergebnismenge eischränken (z.B. die ersten 100 Einträge)
  • sort – Sortierreihenfolge der Ergebnismenge
  • filter – hab ich noch nicht rausgefunden

Als Rückgabe bekommen wir folgendes:

  • limits – Gesamtanzahl
  • movies – die gefundenen Filme

Wie die einzelnen Parameter zu belegen sind am besten in der JSON-RPC API/v6 nachlesen (das würde hier jetzt den Umfang des Artikels überschreiten). Doch wie erstellt man jetzt einen solchen Request? Dafür wird einfach die Klasse JsonRpcRequest genutzt:

Hier wird einfach eine neue Instanz erstellt und die einzelnen Eigenschaften dieser Instanz entsprechend belegt. Ruft man jetzt die überschriebene ToString-Methode auf erhält man den folgenden Output:

Kodi_JsonRpc_GetMovies_Request

Dieser Output wird vom Json.NET-Framework erzeugt, da wir ja die einzelnen Eigenschaften der Klasse JsonRpcRequest mit dem Attribut JsonProperty ausgestattet und die ToString-Methode dieser Klasse überschrieben haben. Mit Hilfe des Json.NET-Frameworks ist es also ganz einfach ein beliebiges Objekt in das JSON-Format zu serialisieren. Diesen Request können wir jetzt via HTTP an den Server senden. Und das geht so:

Zunächst wird ein entsprechender HttpWebRequest erstellt (Zeile 3-7). In Zeile 17 kommt dann der weiter oben erstellte Request zum Einsatz. Hier wird die ToString-Methode verwendet, um das entsprechende JSON-Fragment zu erzeugen. Dieses Fragment wird dann als Anfrage an den Server gesendet. In Zeile 28 wird das Ergebnis des Requests ausgewertet und mit Hilfe der Parse-Methode in ein Objekt vom Typ JObject umgewandelt (alles Klassen aus dem Json.NET-Framework). Dieses Objekt wird dann in Zeile 30 auf der Konsole ausgegeben. Als Ergebnis erhalten wir in etwa den folgenden Output:

Kodi_JsonRpc_GetMovies_Response

Und damit wäre das Programm auch schon fertig 😉 Es gibt natürlich auch ein paar ein Verbesserungen, die man noch einbauen könnte:

  • Asynchrone Kommunikation
  • Generische Json-Response, die ein typisiertes Objekt zurückliefert
  • Einsatz eines ExpandoObjekts für die JSON-Antwort

GitHub-Repository

Der Quellcodes des Beispielprogramms ist in folgendem GitHub-Repository abgelegt: https://github.com/steve600/KodiJsonRpcSample

Literaturverzeichnis und Weblinks

Abk.Quelle
[1]JSON-RPC 2.0 Specification
[2]Newtonsoft Json.NET
[3]Kodi MediaPlayer
[4]Kodi JSON-RPC API
[5]Kodi JSON-RPC API/v6
Fork me on GitHub