
Nous continuons à considérer les technologies Julia. Et aujourd'hui, nous parlerons de packages conçus pour la création de services Web. Ce n'est un secret pour personne que le créneau principal du langage Julia est l'informatique haute performance. Par conséquent, une étape assez logique consiste à créer directement des services Web capables d'effectuer ces calculs à la demande. Bien sûr, les services Web ne sont pas le seul moyen de communiquer dans un environnement en réseau. Mais, étant donné qu'ils sont désormais les plus utilisés dans les systèmes distribués, nous envisagerons la création de services qui servent les requêtes HTTP.
Notez qu'en raison de la jeunesse de Julia, il existe un ensemble de packages concurrents. Par conséquent, nous essaierons de comprendre comment et pourquoi les utiliser. En cours de route, nous comparons l'implémentation du même service Web JSON avec leur aide.
L'infrastructure de Julia s'est développée activement au cours des deux dernières années. Et, dans ce cas, il ne s'agit pas seulement d'une phrase en ligne inscrite pour un beau début de texte, mais d'une insistance sur le fait que tout change de manière intensive, et ce qui était pertinent il y a quelques années est désormais dépassé. Cependant, nous essaierons de mettre en évidence des packages stables et de donner des recommandations sur la façon de mettre en œuvre des services Web avec leur aide. Par souci de précision, nous allons créer un service Web qui accepte une demande POST avec des données JSON au format suivant:
{ "title": "something", "body": "something" }
Nous supposons que le service que nous créons n'est pas RESTful. Notre tâche principale est d'examiner précisément les méthodes de description des itinéraires et des gestionnaires de requêtes.
Package HTTP.jl
Ce package est la principale implémentation du protocole HTTP dans Julia et gagne progressivement de nouvelles fonctionnalités. En plus d'implémenter des structures et des fonctions typiques pour exécuter des requêtes client HTTP, ce package implémente également des fonctions pour créer des serveurs HTTP. En même temps, au fur et à mesure de son développement, le package a reçu des fonctions qui permettent au programmeur d'enregistrer les gestionnaires et donc de créer des services typiques. De plus, dans les dernières versions, il existe une prise en charge intégrée du protocole WebSocket, dont l'implémentation a été précédemment effectuée dans le cadre d'un package WebSocket.jl distinct. Autrement dit, HTTP.jl, à l'heure actuelle, peut satisfaire la plupart des besoins des programmeurs. Considérez quelques exemples plus en détail.
Client HTTP
Nous commençons l'implémentation avec le code client, que nous utiliserons pour vérifier l'opérabilité.
Le package HTTP fournit des méthodes qui correspondent aux noms des commandes de protocole HTTP. Dans ce cas, nous utilisons get
et post
. L'argument nommé facultatif verbose
vous permet de définir la quantité d'informations de débogage à afficher. Ainsi, par exemple, verbose=1
produira:
GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1)
Et dans le cas de verbose=3
nous obtenons déjà un ensemble complet de données transmises et reçues:
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)
À l'avenir, nous n'utiliserons que verbose=1
afin de ne voir que des informations minimales sur ce qui se passe.
Quelques commentaires concernant le code.
doc = Document("Some document", "Test document with some content.")
Puisque nous avons précédemment déclaré la structure Document (de plus, immuable), un constructeur est disponible pour elle par défaut, dont les arguments correspondent aux champs déclarés de la structure. Afin de le convertir en JSON, nous utilisons le package JSON.jl
et sa méthode json(doc)
.
Faites attention au fragment:
r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3)
Puisque nous transmettons JSON, vous devez spécifier explicitement le type application/json
dans l'en Content-Type
tête Content-Type
. Les en-têtes sont passés à la méthode HTTP.post
(cependant, comme toutes les autres) en utilisant un tableau (de type Vector, mais pas Dict) contenant les paires nom-valeur d'en-tête.
Pour un test d'intégrité, nous effectuerons trois requêtes:
- Demande GET à la route racine;
- Demande GET dans le format / utilisateur / nom, où nom est le nom transmis;
- Demande / ressource / processus POST avec un objet JSON transmis. Nous nous attendons à recevoir le même document, mais avec le champ
server_mark
ajouté.
Nous utiliserons ce code client pour tester toutes les options d'implémentation du serveur.
Serveur HTTP
Après avoir compris le client, il est temps de commencer à implémenter le serveur. Pour commencer, nous ne rendrons le service qu'avec l'aide de HTTP.jl
afin de le garder comme option de base, ce qui ne nécessite pas l'installation d'autres packages. Nous vous rappelons que tous les autres packages utilisent HTTP.jl
Dans l'exemple, vous devez faire attention au code suivant:
dump(req)
imprime sur la console tout ce qui est connu de l'objet. Y compris les types de données, les valeurs, ainsi que tous les champs imbriqués et leurs valeurs. Cette méthode est utile à la fois pour la recherche en bibliothèque et le débogage.
String
(m = match( r".*/user/([[:alpha:]]+)", req.target))
est une expression régulière qui analyse l'itinéraire sur lequel le gestionnaire est enregistré. Le package HTTP.jl
fournit pas de moyens automatiques pour identifier un modèle dans un itinéraire.
Dans le gestionnaire process_resource
, nous analysons le JSON qui est accepté par le service.
message = JSON.parse(String(req.body))
Les données sont accessibles via le champ req.body
. Notez que les données sont présentées dans un format de tableau d'octets. Par conséquent, pour les utiliser en tant que chaîne, une conversion explicite en chaîne est effectuée. La méthode JSON.parse
est une méthode de package JSON.jl
qui désérialise les données et crée un objet. Puisque l'objet dans ce cas est un tableau associatif (Dict), nous pouvons facilement y ajouter une nouvelle clé. String
message["server_mark"] = "confirmed"
ajoute la clé server_mark
avec la valeur confirmed
.
Le service HTTP.serve(ROUTER, Sockets.localhost, 8080)
lorsque la ligne HTTP.serve(ROUTER, Sockets.localhost, 8080)
est HTTP.serve(ROUTER, Sockets.localhost, 8080)
.
La réponse de contrôle pour le service basé sur HTTP.jl (obtenue lors de l'exécution du code client avec 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"}
Dans le contexte du débogage des informations avec verbose=1
, nous pouvons clairement voir les lignes: Hello World
, Hello Jemand
, "server_mark":"confirmed"
.
Après avoir vu le code du service, une question naturelle se pose - pourquoi avons-nous besoin de tous les autres packages, si tout est si simple en HTTP. Il y a une réponse très simple à cela. HTTP - permet d'enregistrer des gestionnaires dynamiques, mais même une implémentation élémentaire de lecture d'un fichier image statique à partir d'un répertoire nécessite une implémentation distincte. Par conséquent, nous considérons également des packages axés sur la création d'applications Web.
Package Mux.jl
Ce package est positionné comme une couche intermédiaire pour les applications Web implémentées sur Julia. Sa mise en œuvre est très légère. L'objectif principal est de fournir un moyen simple de décrire les gestionnaires. On ne peut pas dire que le projet ne se développe pas, mais il se développe lentement. Cependant, regardez le code de notre service desservant les mêmes routes.
Ici, les itinéraires sont décrits en utilisant la méthode de la page
. L'application Web est déclarée à l'aide de la macro @app
. Les arguments de la méthode de page
sont l'itinéraire et le gestionnaire. Un gestionnaire peut être spécifié comme une fonction qui accepte une demande comme entrée, ou il peut être spécifié comme une fonction lambda en place. Parmi les fonctions utiles supplémentaires, Mux.notfound()
présent pour envoyer la réponse Not found
spécifiée. Et le résultat qui doit être envoyé au client n'a pas besoin d'être empaqueté dans HTTP.Response
, comme nous l'avons fait dans l'exemple précédent, puisque Mux le fera lui-même. Cependant, vous devez toujours effectuer l'analyse JSON vous-même, tout comme la sérialisation de l'objet pour la réponse - JSON.json(message)
.
message = JSON.parse(String(req[:data])) message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] )
La réponse est envoyée sous forme de tableau associatif avec les champs :body
:headers
.
Le démarrage du serveur avec la méthode serve(test, 8080)
est asynchrone, donc l'une des options de Julia pour organiser l'attente de la fin est d'appeler le code:
Base.JLOptions().isinteractive == 0 && wait()
Sinon, le service fait la même chose que la version précédente sur HTTP.jl
Réponse de contrôle pour le service:
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"}
Paquet Bukdu.jl
Le package a été développé sous l'influence du framework Phoenix, qui, à son tour, est implémenté sur Elixir et est la mise en œuvre d'idées de construction Web de la communauté Ruby en projection sur Elixir. Le projet se développe assez activement et se positionne comme un outil de création d'une API RESTful et d'applications web légères. Il existe des fonctions pour simplifier la sérialisation et la désérialisation JSON. Cela manque dans HTTP.jl
et Mux.jl
Examinons la mise en œuvre de notre service Web.
La première chose à laquelle vous devez faire attention est la déclaration de la structure de stockage de l'état du contrôleur.
struct WelcomeController <: ApplicationController conn::Conn end
Dans ce cas, il s'agit d'un type concret créé en tant que descendant du type abstrait ApplicationController
.
Les méthodes du contrôleur sont déclarées de manière similaire par rapport aux implémentations précédentes. Il y a une légère différence dans le gestionnaire de notre objet JSON.
function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end
Comme vous pouvez le voir, la désérialisation est également effectuée indépendamment à l'aide de la méthode JSON.parse
, mais la méthode de render(JSON, message)
JSON.parse
est utilisée pour sérialiser la réponse.
La déclaration des itinéraires est effectuée dans le style traditionnel pour les rubistes, y compris l'utilisation du bloc d' do...end
routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end
De même, de manière traditionnelle pour les rubistes, un segment est déclaré dans la ligne de route /user/:user
. En d'autres termes, la partie variable de l'expression, dont l'accès peut être effectué par le nom spécifié dans le modèle. Il est syntaxiquement désigné comme représentant du type Symbol
. Soit dit en passant, pour Julia, le type Symbol
signifie, essentiellement, la même chose que pour Ruby - il s'agit d'une chaîne immuable, représentée en mémoire par une seule instance.
En conséquence, après avoir déclaré un itinéraire avec une partie variable, et également indiqué le type de cette partie variable, nous pouvons nous référer aux données déjà analysées par le nom attribué. Dans la méthode qui traite la demande, nous c.params.user
simplement au champ via un point au c.params.user
.
welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user)
Réponse de contrôle pour le service:
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"}
Conclusion du service à la console:
>./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
Paquet Genie.jl
Un projet ambitieux positionné comme un framework web MVC. Dans son approche, les «Rails» sur Julia sont assez clairement visibles, y compris la structure de répertoires créée par le générateur. Le projet se développe, cependant, pour des raisons inconnues, ce package n'est pas inclus dans le référentiel de packages Julia. Autrement dit, son installation n'est possible qu'à partir du référentiel git avec la commande:
julia>]
Le code de notre service à Genie est le suivant (nous n'utilisons pas de générateurs):
Ici, vous devez faire attention au format de la déclaration.
route("/") do "Hello World!" end
Ce code est très familier aux programmeurs Ruby. Le bloc do...end
comme gestionnaire et la route comme argument de la méthode. Notez que pour Julia ce code peut être réécrit sous la forme:
route(req -> "Hello World!", "/")
Autrement dit, la fonction de gestionnaire est en premier lieu, la route est en deuxième. Mais pour notre cas, laissons le style rubis.
Genie intègre automatiquement le résultat de l'exécution dans une réponse HTTP. Dans le cas minimum, il suffit de renvoyer le résultat du type correct, par exemple String. Parmi les commodités supplémentaires, une vérification automatique du format d'entrée et de son analyse est mise en œuvre. Par exemple, pour JSON, il vous suffit d'appeler la méthode jsonpayload()
.
route("/resource/process", method = POST) do message = jsonpayload()
Faites attention au fragment de code commenté ici. La méthode jsonpayload()
nothing
renvoie nothing
si, pour une raison quelconque, le format d'entrée n'est pas reconnu comme JSON. Notez que juste pour cela, l'en-tête [("Content-Type" => "application/json")]
ajouté à notre client HTTP, car sinon Genie ne commencera même pas à analyser les données en JSON. Dans le cas où quelque chose d'incompréhensible est venu, il est utile de regarder rawpayload()
pour ce qu'il est. Cependant, comme il ne s'agit que d'une phase de débogage, vous ne devez pas le laisser dans le code.
En outre, vous devez faire attention à renvoyer le résultat dans le message |> json!
format message |> json!
. La méthode json!(str)
est placée en dernier dans le pipeline ici. Il fournit la sérialisation des données au format JSON et garantit également que Genie ajoute le bon type de Content-Type
. Faites également attention au fait que le mot return
dans la plupart des cas dans les exemples ci-dessus est redondant. Julia, comme Ruby, par exemple, renvoie toujours le résultat de la dernière opération ou la valeur de la dernière expression spécifiée. Autrement dit, le mot return
est facultatif.
Les capacités de Genie ne s'arrêtent pas là, mais nous n'en avons pas besoin pour implémenter un service Web.
Réponse de contrôle pour le service:
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"}
Conclusion du service à la console:
>./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
Package JuliaWebAPI.jl
Ce package a été positionné comme une couche intermédiaire pour la création d'applications Web à l'époque où HTTP.jl n'était qu'une bibliothèque qui implémente le protocole. L'auteur de ce package a également implémenté un générateur de code serveur basé sur la spécification Swagger (OpenAPI et http://editor.swagger.io/ ) - voir le projet https://github.com/JuliaComputing/Swagger.jl , et ce générateur a utilisé JuliaWebAPI .jl. Cependant, le problème avec JuliaWebAPI.jl est qu'il n'implémente pas la possibilité de traiter le corps de la demande (par exemple, JSON) envoyée au serveur via une demande POST. L'auteur pensait que le passage de paramètres dans un format de valeur-clé convient à toutes les occasions ... L'avenir de ce paquet n'est pas clair. Toutes ses fonctions sont déjà implémentées dans de nombreux autres packages, y compris HTTP.jl. Le package Swagger.jl ne l'utilise plus non plus.
WebSockets.jl
Une implémentation précoce du protocole WebSocket. Le package est utilisé depuis longtemps comme implémentation principale de ce protocole, cependant, à l'heure actuelle, son implémentation est incluse dans le package HTTP.jl. Le package WebSockets.jl lui-même utilise HTTP.jl pour établir une connexion, mais maintenant, cela ne vaut pas la peine de l'utiliser dans de nouveaux développements. Il doit être considéré comme un package de compatibilité.
Conclusion
Cette revue montre différentes façons d'implémenter un service Web sur Julia. Le moyen le plus simple et le plus universel consiste à utiliser directement le package HTTP.jl. De plus, les packages Bukdu.jl et Genie.jl sont très utiles. Au minimum, leur développement doit être surveillé. En ce qui concerne le package Mux.jl, ses avantages sont maintenant dissous dans le contexte de HTTP.jl. Par conséquent, l'opinion personnelle n'est pas de l'utiliser. Genie.jl est un framework très prometteur. Cependant, avant de commencer à l'utiliser, vous devez au moins comprendre pourquoi l'auteur ne l'enregistre pas en tant que package officiel.
Notez que le code de désérialisation JSON dans les exemples a été utilisé sans gestion d'erreur. Dans tous les cas, sauf Genie, il est nécessaire de gérer les erreurs d'analyse et d'en informer l'utilisateur. Un exemple d'un tel code pour 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
En général, nous pouvons dire qu'il y a déjà suffisamment de fonds pour créer des services Web dans Julia. Autrement dit, il n'est pas nécessaire de «réinventer la roue» pour les écrire. La prochaine étape consiste à évaluer comment Julia peut supporter la charge dans les repères existants, si quelqu'un est prêt à le supporter. Cependant, pour l'instant, arrêtons-nous sur cette revue.
Les références