Echtzeit-Kommunikation mit GraphQL I/O
Seite 5: GraphQL I/O, der zweite Lösungsschritt
Während GraphQL die formale Sprachspezifikation darstellt, ist GraphQL I/O ein vollständiges, homogen implementiertes Client- und Server-Framework zur Netzwerkkommunikation per GraphQL.
Dazu gleicht GraphQL I/O zuallererst den architektonischen Makel der getrennten Transportwelten aus, indem es sämtliche Kommunikation zwischen Client und Server über Websocket führt. Der ständige Verbindungsauf- und -abbau zum Server entfällt. Die durchaus mehrere Hundert Byte großen HTTP-Header entfallen ebenfalls, weil sie bei der Websocket-Kommunikation irrelevant sind. Der Austausch kleinerer Informationspakete ist damit deutlich effizienter, weil der Protokoll-Overhead im Vergleich zur Nutzlast deutlich sinkt.
Direkter Vergleich: RPC, SOAP, REST, GraphQL und GraphQL I/O
SOAP und REST lassen sich als auf Webtechnologien basierende Varianten von RPC verstehen. GraphQL und GraphQL I/O sind hingegen Abfragesprachen. Der Client legt die gewünschte Datenstruktur, Umfang und Detailgrad fest, der Server löst sie auf.
Außerdem besteht mit Websockets grundsätzlich eine bidirektionale, asynchrone Verbindung zwischen Client und Server. Der Server kann demnach aktiv und selbstständig Daten zum Client übertragen, ohne auf eine vorherige Anfrage des Clients warten zu müssen. Die Verzögerungen zwischen dem Eintritt eines Ereignisses auf Serverseite und der Reaktion auf Clientseite bleibt minimal. Damit lässt sich sowohl die Echtzeitkommunikation realisieren, als auch von Pull zu Push wechseln.
Damit Entwickler die Echtzeitaktualisierung schnell, eingängig und einfach implementieren können, nimmt GraphQL I/O zwei entscheidende Änderungen beim Umgang mit Subscriptions vor. Die erste Änderung: Subscriptions sind nicht von den Queries separiert. Stattdessen lässt sich jeder Query, den ein Client an den Server absetzt, in eine Subscription ändern. Dazu muss lediglich die Callback-Funktion zur Abarbeitung der vom Server erhaltenen Daten an .subscribe() und nicht wie bisher an .then() übergeben werden (siehe Listing). Jedes Mal, wenn der Server künftig Änderungen an den abgerufenen Daten erkennt, informiert er den Client. Der Client führt die Callback-Funktion daraufhin automatisch aus, setzt somit den Query an den Server abermals ab und erhält die aktuellen Daten.
(async () => {
const { Server } = require("graphql-io-server")
const { Client } = require("graphql-io-client")
/*
** ==== SERVER ====
*/
const server = new Server({ url: "http://127.0.0.1:12345" })
server.at("graphql-schema", () => `
type Root {
counter: Counter
}
type Counter {
value: Int
increase: Counter
}
`)
let counter = {
value: 0
}
server.at("graphql-resolver", () => ({
Root: {
counter: (obj, args, ctx, info) => {
ctx.scope.record("Counter", 0, "read", "direct", "one")
return counter
}
},
Counter: {
increase: (obj, args, ctx, info) => {
counter.value++
ctx.scope.record("Counter", 0, "update", "direct", "one")
return counter
}
}
}))
await server.start()
/*
** ==== CLIENT #1 ====
*/
const client1 = new Client({ url: "http://127.0.0.1:12345", debug: 9 })
client1.at("debug", (ev) => console.log(`client1 [${ev.level}] ${ev.msg}`))
await client1.connect()
let subscription = client1.query(`{
counter {
value
}
}`).subscribe((result) => {
console.log("Client #1: Result:", result.data)
})
/*
** ==== CLIENT #2 ====
*/
const client2 = new Client({ url: "http://127.0.0.1:12345", debug: 9 })
client2.at("debug", (ev) => console.log(`client2 [${ev.level}] ${ev.msg}`))
await client2.connect()
setInterval(async () => {
let result = await client2.mutation(`{
counter {
increase { value }
}
}`)
console.log("Client #2: Result:", result.data)
}, 1 * 1000)
})().catch((err) => {
console.log("ERROR", err)
})
(Subscription-Beispiel: Dieses einfache Beispiel startet einen Server sowie zwei Clients, die auf den Server zugreifen. Client 2 ändert die Daten, Client 1 abonniert einen Query und wird stets über die Änderungen informiert.)
Auch auf Serverseite müssen die Entwickler keine Berge versetzen, damit dieses Subscription-System funktioniert. Stattdessen kommt beim Datenzugriff ein Scope Journal zum Einsatz. Dank des Scope Journals kann der Server Änderungen an den Daten erkennen und dann all die Clients benachrichtigen, die eine Subscription auf einen Query registriert haben, der von den geänderten Daten betroffen ist.
Die Aufgabe der Entwickler besteht nur noch darin, dieses Scope Journal bei lesendem und schreibendem Zugriff zu füllen. Dazu fügen sie mit jedem Query und jeder Mutation dem Scope Journal neue Einträge hinzu und vermerken, welche Objekte und Felder betroffen sind. Für normale CRUD-Operationen ist das in den meisten Fällen sogar bereits durch Frameworks weggekapselt. Der Server gleicht nur diese Journale schlussendlich im Hintergrund dann automatisch ab.
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmmung wird hier ein externes Video (Kaltura Inc.) geladen.
Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Kaltura Inc.) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmmung wird hier ein externes Video (Kaltura Inc.) geladen.
Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Kaltura Inc.) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.