
Wir betrachten weiterhin Technologien Julia. Und heute werden wir über Pakete sprechen, die zum Erstellen von Webdiensten entwickelt wurden. Es ist kein Geheimnis, dass die Hauptnische der Julia-Sprache das Hochleistungsrechnen ist. Daher ist es ein ziemlich logischer Schritt, direkt Webdienste zu erstellen, die diese Berechnungen bei Bedarf durchführen können. Natürlich sind Webdienste nicht die einzige Möglichkeit, in einer Netzwerkumgebung zu kommunizieren. Da sie heute in verteilten Systemen am häufigsten verwendet werden, werden wir die Erstellung von Diensten in Betracht ziehen, die HTTP-Anforderungen bedienen.
Beachten Sie, dass es aufgrund der Jugend von Julia eine Reihe konkurrierender Pakete gibt. Daher werden wir versuchen herauszufinden, wie und warum sie verwendet werden. Vergleichen Sie dabei die Implementierung desselben JSON-Webdienstes mit ihrer Hilfe.
Die Infrastruktur von Julia hat sich in den letzten ein oder zwei Jahren aktiv entwickelt. Und in diesem Fall ist dies nicht nur eine Online-Phrase, die für einen schönen Anfang des Textes eingeschrieben ist, sondern eine Betonung der Tatsache, dass sich alles intensiv ändert und das, was vor ein paar Jahren relevant war, jetzt veraltet ist. Wir werden jedoch versuchen, stabile Pakete hervorzuheben und Empfehlungen zur Implementierung von Webdiensten mit deren Hilfe zu geben. Aus Gründen der Bestimmtheit erstellen wir einen Webdienst, der eine POST-Anforderung mit JSON-Daten im folgenden Format akzeptiert:
{ "title": "something", "body": "something" }
Wir gehen davon aus, dass der von uns erstellte Service nicht RESTful ist. Unsere Hauptaufgabe besteht darin, die Methoden zur Beschreibung von Routen und Anforderungshandlern genau zu berücksichtigen.
HTTP.jl-Paket
Dieses Paket ist die Hauptimplementierung des HTTP-Protokolls in Julia und wird schrittweise mit neuen Funktionen erweitert. Dieses Paket implementiert nicht nur typische Strukturen und Funktionen zum Ausführen von HTTP-Client-Anforderungen, sondern implementiert auch Funktionen zum Erstellen von HTTP-Servern. Gleichzeitig hat das Paket während seiner Entwicklung Funktionen erhalten, die es dem Programmierer recht bequem machen, Handler zu registrieren und so typische Dienste zu erstellen. In den neuesten Versionen ist außerdem das WebSocket-Protokoll integriert, dessen Implementierung zuvor als Teil eines separaten Pakets WebSocket.jl vorgenommen wurde. Das heißt, HTTP.jl kann derzeit die meisten Anforderungen eines Programmierers erfüllen. Schauen wir uns einige Beispiele genauer an.
HTTP-Client
Wir beginnen die Implementierung mit dem Client-Code, mit dem wir die Funktionsfähigkeit überprüfen.
Das HTTP-Paket enthält Methoden, die mit den Namen der HTTP-Protokollbefehle übereinstimmen. In diesem Fall verwenden wir get
und post
. Mit dem optionalen verbose
benannten Argument können Sie die Menge der auszugebenden Debugging-Informationen festlegen. So verbose=1
beispielsweise verbose=1
:
GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1)
Und im Fall von verbose=3
wir bereits einen vollständigen Satz gesendeter und empfangener Daten:
DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "POST /resource/process HTTP/1.1\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Content-Type: application/json\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Host: 127.0.0.1\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "Content-Length: 67\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 eb4f ️-> "\r\n" (write) DEBUG: 2019-04-21T22:40:40.961 e1c6 ️-> "{\"title\":\"Some document\",\"body\":\"Test document with some content.\"}" (unsafe_write) DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "HTTP/1.1 200 OK\r\n" (readuntil) DEBUG: "Content-Type: application/json\r\n" DEBUG: "Transfer-Encoding: chunked\r\n" DEBUG: "\r\n" DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "5d\r\n" (readuntil) DEBUG: 2019-04-21T22:40:40.963 eb4f ️<- "{\"body\":\"Test document with some content.\",\"server_mark\":\"confirmed\",\"title\":\"Some document\"}" (unsafe_read) DEBUG: 2019-04-21T22:40:40.968 eb4f ️<- "\r\n" (readuntil) DEBUG: "0\r\n" DEBUG: 2019-04-21T22:40:40.968 eb4f ️<- "\r\n" (readuntil)
In Zukunft werden wir nur verbose=1
verwenden, um nur minimale Informationen darüber zu sehen, was passiert.
Ein paar Kommentare zum Code.
doc = Document("Some document", "Test document with some content.")
Da wir zuvor die Dokumentstruktur deklariert haben (außerdem unveränderlich), steht standardmäßig ein Konstruktor dafür zur Verfügung, dessen Argumente den deklarierten Feldern der Struktur entsprechen. Um es in JSON zu konvertieren, verwenden wir das Paket JSON.jl
und seine Methode json(doc)
.
Achten Sie auf das Fragment:
r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3)
Da wir JSON übergeben, müssen Sie den Typ application/json
im Content-Type
Header explizit angeben. Die Header werden (wie alle anderen auch) an die HTTP.post
Methode übergeben, wobei ein Array (Vektortyp, aber kein Dict-Typ) verwendet wird, das die Header-Name-Wert-Paare enthält.
Für einen Gesundheitstest führen wir drei Abfragen durch:
- GET-Anforderung an die Root-Route;
- GET-Anfrage im Format / Benutzer / Name, wobei Name der übertragene Name ist;
- POST-Anforderung / Ressource / Prozess mit übergebenem JSON-Objekt. Wir erwarten dasselbe Dokument, jedoch mit dem
server_mark
Feld server_mark
.
Wir werden diesen Client-Code verwenden, um alle Server-Implementierungsoptionen zu testen.
HTTP-Server
Nachdem Sie den Client herausgefunden haben, ist es Zeit, mit der Implementierung des Servers zu beginnen. Zunächst werden wir den Dienst nur mit Hilfe von HTTP.jl
, um ihn als HTTP.jl
, für die keine Installation anderer Pakete erforderlich ist. Wir erinnern Sie daran, dass alle anderen Pakete HTTP.jl
Im Beispiel sollten Sie den folgenden Code beachten:
dump(req)
druckt alles, was dem Objekt bekannt ist, auf die Konsole. Einschließlich Datentypen, Werte sowie aller verschachtelten Felder und ihrer Werte. Diese Methode ist sowohl für die Bibliotheksrecherche als auch für das Debuggen nützlich.
String
(m = match( r".*/user/([[:alpha:]]+)", req.target))
ist ein regulärer Ausdruck, der die Route analysiert, auf der der Handler registriert ist. Das Paket HTTP.jl
keine automatischen Möglichkeiten zum Identifizieren eines Musters in einer Route.
Innerhalb des process_resource
analysieren wir den JSON, der vom Service akzeptiert wird.
message = JSON.parse(String(req.body))
Der Zugriff auf Daten erfolgt über das Feld req.body
. Beachten Sie, dass die Daten in einem Byte-Array-Format vorliegen. Um mit ihnen als Zeichenfolge zu arbeiten, wird daher eine explizite Konvertierung in eine Zeichenfolge durchgeführt. Die JSON.parse
Methode ist eine JSON.jl
, die Daten deserialisiert und ein Objekt erstellt. Da das Objekt in diesem Fall ein assoziatives Array (Dict) ist, können wir ihm leicht einen neuen Schlüssel hinzufügen. String
message["server_mark"] = "confirmed"
server_mark
Schlüssel server_mark
mit dem confirmed
Wert hinzu.
Der Dienst wird HTTP.serve(ROUTER, Sockets.localhost, 8080)
wenn die Zeile HTTP.serve(ROUTER, Sockets.localhost, 8080)
.
Die Steuerantwort für den Dienst basierend auf HTTP.jl (erhalten, wenn der Clientcode mit verbose=1
):
GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) Hello World GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) Hello Jemand POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
Vor dem Hintergrund des Debuggens von Informationen mit verbose=1
wir deutlich die Zeilen: Hello World
, Hello Jemand
, "server_mark":"confirmed"
.
Nach dem Anzeigen des Service-Codes stellt sich natürlich die Frage, warum wir alle anderen Pakete benötigen, wenn in HTTP alles so einfach ist. Darauf gibt es eine sehr einfache Antwort. HTTP - ermöglicht die Registrierung dynamischer Handler, aber selbst eine elementare Implementierung des Lesens einer statischen Bilddatei aus einem Verzeichnis erfordert eine separate Implementierung. Daher betrachten wir auch Pakete, die sich auf die Erstellung von Webanwendungen konzentrieren.
Mux.jl-Paket
Dieses Paket ist als Zwischenschicht für auf Julia implementierte Webanwendungen positioniert. Die Implementierung ist sehr leicht. Der Hauptzweck besteht darin, eine einfache Möglichkeit zur Beschreibung von Handlern bereitzustellen. Dies bedeutet nicht, dass sich das Projekt nicht entwickelt, sondern nur langsam. Sehen Sie sich jedoch den Code für unseren Service an, der dieselben Routen bedient.
Hier werden die Routen mit der page
. Die Webanwendung wird mit dem Makro @app
deklariert. Die Argumente für die page
sind die Route und der Handler. Ein Handler kann als eine Funktion angegeben werden, die eine Anforderung als Eingabe akzeptiert, oder er kann als vorhandene Lambda-Funktion angegeben werden. Von den zusätzlichen nützlichen Funktionen ist Mux.notfound()
vorhanden, um die angegebene Antwort " Not found
zu senden. Und das Ergebnis, das an den Client gesendet werden soll, muss nicht wie im vorherigen Beispiel in HTTP.Response
, da Mux dies selbst tun wird. Sie müssen das JSON-Parsing jedoch weiterhin selbst durchführen, ebenso wie das Serialisieren des Objekts für die Antwort - JSON.json(message)
.
message = JSON.parse(String(req[:data])) message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] )
Die Antwort wird als assoziatives Array mit den Feldern :body
:headers
gesendet.
Das Starten des Servers mit der serve(test, 8080)
Methode serve(test, 8080)
ist asynchron. Eine der Optionen in Julia zum Organisieren des Wartens auf den Abschluss besteht darin, den folgenden Code aufzurufen:
Base.JLOptions().isinteractive == 0 && wait()
Ansonsten macht der Dienst dasselbe wie die vorherige Version auf HTTP.jl
Kontrollantwort für den Dienst:
GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) <h1>Hello World!</h1> GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) <h1>Hello, Jemand!</h1> POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
Paket Bukdu.jl
Das Paket wurde unter dem Einfluss des Phoenix-Frameworks entwickelt, das wiederum auf Elixir implementiert ist und die Umsetzung von Webbuilding-Ideen der Ruby-Community in Projektion auf Elixir darstellt. Das Projekt entwickelt sich sehr aktiv und ist als Werkzeug zum Erstellen einer RESTful-API und leichter Webanwendungen positioniert. Es gibt Funktionen zur Vereinfachung der JSON-Serialisierung und -Deserialisierung. Dies fehlt in HTTP.jl
und Mux.jl
Schauen wir uns die Implementierung unseres Webdienstes an.
Das erste, worauf Sie achten sollten, ist die Deklaration der Struktur zum Speichern des Controller-Status.
struct WelcomeController <: ApplicationController conn::Conn end
In diesem Fall handelt es sich um einen konkreten Typ, der als Nachkomme des abstrakten Typs ApplicationController
.
Methoden für die Steuerung werden in ähnlicher Weise in Bezug auf frühere Implementierungen deklariert. Es gibt einen kleinen Unterschied im Handler unseres JSON-Objekts.
function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end
Wie Sie sehen können, wird die Deserialisierung auch unabhängig mit der JSON.parse
Methode durchgeführt, aber die JSON.parse
Methode render(JSON, message)
wird zum Serialisieren der Antwort verwendet.
Die Deklaration der Routen erfolgt im traditionellen Stil für Rubisten, einschließlich der Verwendung des do...end
-Endblocks.
routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end
Außerdem wird auf herkömmliche Weise für Rubisten ein Segment in der Routenzeile /user/:user
deklariert. Mit anderen Worten, der variable Teil des Ausdrucks, auf den über den in der Vorlage angegebenen Namen zugegriffen werden kann. Es wird syntaktisch als Vertreter des Typs Symbol
. Übrigens bedeutet der Typ Symbol
für Julia im Wesentlichen dasselbe wie für Ruby - dies ist eine unveränderliche Zeichenfolge, die im Speicher durch eine einzelne Instanz dargestellt wird.
Dementsprechend können wir, nachdem wir eine Route mit einem variablen Teil deklariert und auch den Typ dieses variablen Teils angegeben haben, auf die Daten verweisen, die bereits durch den zugewiesenen Namen analysiert wurden. Bei der Methode, die die Anforderung verarbeitet, greifen wir einfach über einen Punkt in der Form c.params.user
auf das Feld zu.
welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user)
Kontrollantwort für den Dienst:
GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) "Hello World" GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) "Hello Jemand" POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
Abschluss des Service für die Konsole:
>./bukdu_json.jl INFO: Bukdu Listening on 127.0.0.1:8080 INFO: GET WelcomeController index 200 / INFO: GET WelcomeController welcome_user 200 /user/Jemand INFO: Dict{String,Any}("body"=>"Test document with some content.","title"=>"Some document") INFO: POST WelcomeController process_resource200 /resource/process
Paket Genie.jl
Ein ehrgeiziges Projekt, das als MVC-Webframework positioniert ist. In seinem Ansatz sind die „Rails“ auf Julia ziemlich deutlich sichtbar, einschließlich der vom Generator erstellten Verzeichnisstruktur. Das Projekt wird jedoch aus unbekannten Gründen entwickelt. Dieses Paket ist nicht im Julia-Paket-Repository enthalten. Das heißt, die Installation ist nur über das Git-Repository mit dem folgenden Befehl möglich:
julia>]
Der Code unseres Dienstes in Genie lautet wie folgt (wir verwenden keine Generatoren):
Hier sollten Sie auf das Format der Erklärung achten.
route("/") do "Hello World!" end
Dieser Code ist Ruby-Programmierern sehr vertraut. Der do...end
Block als Handler und die Route als Argument für die Methode. Beachten Sie, dass dieser Code für Julia in der folgenden Form umgeschrieben werden kann:
route(req -> "Hello World!", "/")
Das heißt, die Handlerfunktion steht an erster Stelle, die Route an zweiter Stelle. Aber für unseren Fall lassen wir den Rubinstil.
Genie packt das Ausführungsergebnis automatisch in eine HTTP-Antwort. Im Mindestfall müssen wir nur das Ergebnis des richtigen Typs zurückgeben, z. B. String. Von den zusätzlichen Annehmlichkeiten wird eine automatische Überprüfung des Eingabeformats und seiner Analyse implementiert. Für JSON müssen Sie beispielsweise nur die Methode jsonpayload()
.
route("/resource/process", method = POST) do message = jsonpayload()
Achten Sie auf das hier auskommentierte Codefragment. Die Methode jsonpayload()
gibt nothing
wenn das Eingabeformat aus irgendeinem Grund nicht als JSON erkannt wird. Beachten Sie, dass nur aus diesem Grund der Header [("Content-Type" => "application/json")]
zu unserem HTTP-Client hinzugefügt wird, da Genie sonst nicht einmal anfängt, die Daten als JSON zu analysieren. Falls etwas Unverständliches rawpayload()
ist, ist es nützlich, rawpayload()
auf das zu rawpayload()
was es ist. Da dies jedoch nur eine Debugging-Phase ist, sollten Sie sie nicht im Code belassen.
Sie sollten auch darauf achten, das Ergebnis in der Formatnachricht message |> json!
. Die json!(str)
-Methode wird hier als letzte in die Pipeline aufgenommen. Es bietet eine Serialisierung von Daten im JSON-Format und stellt außerdem sicher, dass Genie den richtigen Content-Type
hinzufügt. Beachten Sie auch die Tatsache, dass das Wort return
in den meisten Fällen in den obigen Beispielen redundant ist. Julia gibt wie Ruby beispielsweise immer das Ergebnis der letzten Operation oder den Wert des zuletzt angegebenen Ausdrucks zurück. Das heißt, das Wort return
ist optional.
Die Funktionen von Genie enden hier nicht, aber wir benötigen sie nicht, um einen Webdienst zu implementieren.
Kontrollantwort für den Dienst:
GET / HTTP/1.1 HTTP/1.1 200 OK <= (GET / HTTP/1.1) Hello World! GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) Hello Jemand POST /resource/process HTTP/1.1 HTTP/1.1 200 OK <= (POST /resource/process HTTP/1.1) {"body":"Test document with some content.","server_mark":"confirmed","title":"Some document"}
Abschluss des Service für die Konsole:
>./genie_json.jl [ Info: Ready! 2019-04-24 17:18:51:DEBUG:Main: Web Server starting at http://127.0.0.1:8080 2019-04-24 17:18:51:DEBUG:Main: Web Server running at http://127.0.0.1:8080 2019-04-24 17:19:21:INFO:Main: / 200 2019-04-24 17:19:21:INFO:Main: /user/Jemand 200 2019-04-24 17:19:22:INFO:Main: /resource/process 200
Paket JuliaWebAPI.jl
Dieses Paket wurde in jenen Tagen als Zwischenschicht für die Erstellung von Webanwendungen positioniert, als HTTP.jl nur eine Bibliothek war, die das Protokoll implementiert. Der Autor dieses Pakets implementierte auch einen Servercode-Generator basierend auf der Swagger-Spezifikation (OpenAPI und http://editor.swagger.io/ ) - siehe das Projekt https://github.com/JuliaComputing/Swagger.jl , und dieser Generator verwendete JuliaWebAPI .jl. Das Problem mit JuliaWebAPI.jl besteht jedoch darin, dass die Möglichkeit, den Hauptteil der Anforderung (z. B. JSON), die über eine POST-Anforderung an den Server gesendet wurde, zu verarbeiten, nicht implementiert wird. Der Autor glaubte, dass die Übergabe von Parametern in einem Schlüsselwertformat für alle Gelegenheiten geeignet ist ... Die Zukunft dieses Pakets ist nicht klar. Alle seine Funktionen sind bereits in vielen anderen Paketen implementiert, einschließlich HTTP.jl. Das Swagger.jl-Paket verwendet es auch nicht mehr.
WebSockets.jl
Eine frühe Implementierung des WebSocket-Protokolls. Das Paket wurde lange Zeit als Hauptimplementierung dieses Protokolls verwendet. Derzeit ist seine Implementierung jedoch im Paket HTTP.jl enthalten. Das WebSockets.jl-Paket selbst verwendet HTTP.jl, um eine Verbindung herzustellen. Jetzt sollten Sie es jedoch nicht in neuen Entwicklungen verwenden. Es sollte aus Kompatibilitätsgründen als Paket betrachtet werden.
Fazit
Diese Überprüfung zeigt verschiedene Möglichkeiten zum Implementieren eines Webdienstes für Julia. Der einfachste und universellste Weg ist die direkte Verwendung des Pakets HTTP.jl. Auch die Pakete Bukdu.jl und Genie.jl sind sehr nützlich. Zumindest sollte ihre Entwicklung überwacht werden. In Bezug auf das Mux.jl-Paket werden seine Vorteile jetzt vor dem Hintergrund von HTTP.jl aufgelöst. Daher ist persönliche Meinung nicht zu verwenden. Genie.jl ist ein sehr vielversprechendes Framework. Bevor Sie es jedoch verwenden, müssen Sie zumindest verstehen, warum der Autor es nicht als offizielles Paket registriert.
Beachten Sie, dass der JSON-Deserialisierungscode in den Beispielen ohne Fehlerbehandlung verwendet wurde. In allen Fällen außer Genie ist es erforderlich, Analysefehler zu behandeln und den Benutzer darüber zu informieren. Ein Beispiel für einen solchen Code für HTTP.jl:
local message = nothing local body = IOBuffer(HTTP.payload(req)) try message = JSON.parse(body) catch err @error err.msg return HTTP.Response(400, string(err.msg)) end
Generell können wir sagen, dass es in Julia bereits genügend Mittel gibt, um Webdienste zu erstellen. Das heißt, es besteht keine Notwendigkeit, das Rad neu zu erfinden, um sie zu schreiben. Der nächste Schritt besteht darin, zu bewerten, wie Julia der Belastung in den vorhandenen Benchmarks standhalten kann, wenn jemand bereit ist, sie zu übernehmen. Lassen Sie uns jedoch zunächst auf diese Überprüfung eingehen.
Referenzen