Julia Servicios web


Seguimos considerando las tecnologías de Julia. Y hoy hablaremos de paquetes diseñados para crear servicios web. No es ningún secreto que el nicho principal del lenguaje Julia es la informática de alto rendimiento. Por lo tanto, un paso bastante lógico es crear directamente servicios web capaces de realizar estos cálculos a pedido. Por supuesto, los servicios web no son la única forma de comunicarse en un entorno en red. Pero, dado que ahora son los más utilizados en sistemas distribuidos, consideraremos la creación de servicios que atiendan solicitudes HTTP.


Tenga en cuenta que debido a la juventud de Julia, hay un conjunto de paquetes competitivos. Por lo tanto, trataremos de descubrir cómo y por qué usarlos. En el camino, comparamos la implementación del mismo servicio web JSON con su ayuda.


La infraestructura de Julia se ha desarrollado activamente en el último año o dos. Y, en este caso, esta no es solo una frase en línea inscrita para un hermoso comienzo del texto, sino un énfasis en el hecho de que todo está cambiando intensamente y que lo que era relevante hace un par de años ahora está desactualizado. Sin embargo, intentaremos resaltar paquetes estables y dar recomendaciones sobre cómo implementar servicios web con su ayuda. Para mayor claridad, crearemos un servicio web que acepte una solicitud POST con datos JSON en el siguiente formato:


{ "title": "something", "body": "something" } 

Asumimos que el servicio que creamos no es RESTful. Nuestra tarea principal es considerar con precisión los métodos para describir rutas y manejadores de solicitudes.


Paquete HTTP.jl


Este paquete es la implementación principal del protocolo HTTP en Julia y está gradualmente cubierto de nuevas características. Además de implementar estructuras y funciones típicas para ejecutar solicitudes de clientes HTTP, este paquete también implementa funciones para crear servidores HTTP. Al mismo tiempo, a medida que se desarrolla, el paquete ha recibido funciones que hacen que sea muy conveniente para el programador registrar controladores y, por lo tanto, crear servicios típicos. Además, en las últimas versiones, hay soporte incorporado para el protocolo WebSocket, cuya implementación se realizó previamente como parte de un paquete separado WebSocket.jl. Es decir, HTTP.jl, en la actualidad, puede satisfacer la mayoría de las necesidades de un programador. Veamos un par de ejemplos con más detalle.


Cliente HTTP


Comenzamos la implementación con el código del cliente, que usaremos para verificar la operatividad.


 #!/usr/bin/env julia --project=@. import HTTP import JSON.json const PORT = "8080" const HOST = "127.0.0.1" const NAME = "Jemand" #    struct Document title::String body::String end #         Base.show(r::HTTP.Messages.Response) = println(r.status == 200 ? String(r.body) : "Error: " * r.status) #    r = HTTP.get("http://$(HOST):$(PORT)") show(r) #   /user/:name r = HTTP.get("http://$(HOST):$(PORT)/user/$(NAME)"; verbose=1) show(r) #  JSON- POST- doc = Document("Some document", "Test document with some content.") r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3) show(r) 

El paquete HTTP proporciona métodos que coinciden con los nombres de los comandos del protocolo HTTP. En este caso, usamos get y post . El argumento opcional con nombre verbose permite establecer la cantidad de información de depuración que se generará. Entonces, por ejemplo, verbose=1 producirá:


 GET /user/Jemand HTTP/1.1 HTTP/1.1 200 OK <= (GET /user/Jemand HTTP/1.1) 

Y en el caso de verbose=3 ya obtenemos un conjunto completo de datos transmitidos y recibidos:


 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) 

En el futuro, usaremos solo verbose=1 para ver información mínima sobre lo que está sucediendo.


Algunos comentarios sobre el código.


 doc = Document("Some document", "Test document with some content.") 

Como declaramos previamente la estructura del documento (además, inmutable), un constructor está disponible de forma predeterminada, cuyos argumentos corresponden a los campos declarados de la estructura. Para convertirlo a JSON, utilizamos el paquete JSON.jl y su método json(doc) .
Presta atención al fragmento:


 r = HTTP.post( "http://$(HOST):$(PORT)/resource/process", [("Content-Type" => "application/json")], json(doc); verbose=3) 

Como estamos pasando JSON, debe especificar explícitamente el tipo application/json en el encabezado Content-Type . Los encabezados se pasan al método HTTP.post (sin embargo, como todos los demás) utilizando una matriz (de tipo Vector, pero no Dict) que contiene el nombre del encabezado - pares de valores.


Para una prueba de salud, realizaremos tres consultas:


  • OBTENER solicitud a la ruta raíz;
  • Solicitud GET en el formato / usuario / nombre, donde nombre es el nombre transmitido;
  • Solicitud POST / recurso / proceso con un objeto JSON pasado. Esperamos recibir el mismo documento, pero con el campo server_mark agregado.

Usaremos este código de cliente para probar todas las opciones de implementación del servidor.


Servidor HTTP


Después de descubrir el cliente, es hora de comenzar a implementar el servidor. Para comenzar, haremos el servicio solo con la ayuda de HTTP.jl para mantenerlo como una opción básica, que no requiere la instalación de otros paquetes. Le recordamos que todos los demás paquetes usan HTTP.jl


 #!/usr/bin/env julia --project=@. import Sockets import HTTP import JSON #    #    index(req::HTTP.Request) = HTTP.Response(200, "Hello World") #     function welcome_user(req::HTTP.Request) # dump(req) user = "" if (m = match( r".*/user/([[:alpha:]]+)", req.target)) != nothing user = m[1] end return HTTP.Response(200, "Hello " * user) end #  JSON function process_resource(req::HTTP.Request) # dump(req) message = JSON.parse(String(req.body)) @info message message["server_mark"] = "confirmed" return HTTP.Response(200, JSON.json(message)) end #      const ROUTER = HTTP.Router() HTTP.@register(ROUTER, "GET", "/", index) HTTP.@register(ROUTER, "GET", "/user/*", welcome_user) HTTP.@register(ROUTER, "POST", "/resource/process", process_resource) HTTP.serve(ROUTER, Sockets.localhost, 8080) 

En el ejemplo, debe prestar atención al siguiente código:


 dump(req) 

imprime en la consola todo lo que el objeto conoce. Incluyendo tipos de datos, valores, así como todos los campos anidados y sus valores. Este método es útil tanto para la investigación de bibliotecas como para la depuración.


Cadena


 (m = match( r".*/user/([[:alpha:]]+)", req.target)) 

es una expresión regular que analiza la ruta en la que está registrado el controlador. El paquete HTTP.jl no proporciona formas automáticas de identificar un patrón en una ruta.


Dentro del controlador process_resource , analizamos el JSON que acepta el servicio.


 message = JSON.parse(String(req.body)) 

Se accede a los datos a través del campo req.body . Tenga en cuenta que los datos vienen en un formato de matriz de bytes. Por lo tanto, para trabajar con ellos como una cadena, se realiza una conversión explícita a una cadena. El método JSON.parse es un método de paquete JSON.jl que deserializa los datos y crea un objeto. Dado que el objeto en este caso es una matriz asociativa (Dict), podemos agregarle fácilmente una nueva clave. Cadena


 message["server_mark"] = "confirmed" 

agrega la clave server_mark con el valor confirmed .


El servicio se HTTP.serve(ROUTER, Sockets.localhost, 8080) cuando se HTTP.serve(ROUTER, Sockets.localhost, 8080) línea HTTP.serve(ROUTER, Sockets.localhost, 8080) .


La respuesta de control para el servicio basada en HTTP.jl (obtenida al ejecutar el código del cliente con 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"} 

En el contexto de la información de depuración con verbose=1 , podemos ver claramente las líneas: Hello World , Hello Jemand , "server_mark":"confirmed" .


Después de ver el código de servicio, surge una pregunta natural: ¿por qué necesitamos todos los demás paquetes, si todo es tan simple en HTTP? Hay una respuesta muy simple a esto. HTTP: permite registrar controladores dinámicos, pero incluso una implementación elemental de leer un archivo de imagen estático desde un directorio requiere una implementación separada. Por lo tanto, también consideramos paquetes que se centran en la creación de aplicaciones web.


Paquete Mux.jl


Este paquete se posiciona como una capa intermedia para aplicaciones web implementadas en Julia. Su implementación es muy ligera. El objetivo principal es proporcionar una manera fácil de describir los controladores. Esto no quiere decir que el proyecto no se esté desarrollando, sino que se está desarrollando lentamente. Sin embargo, mire el código de nuestro servicio que sirve las mismas rutas.


 #!/usr/bin/env julia --project=@. using Mux using JSON @app test = ( Mux.defaults, page(respond("<h1>Hello World!</h1>")), page("/user/:user", req -> "<h1>Hello, $(req[:params][:user])!</h1>"), route("/resource/process", req -> begin message = JSON.parse(String(req[:data])) @info message message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] ) end), Mux.notfound() ) serve(test, 8080) Base.JLOptions().isinteractive == 0 && wait() 

Aquí las rutas se describen utilizando el método de page . La aplicación web se declara utilizando la macro @app . Los argumentos del método de page son la ruta y el controlador. Un controlador puede especificarse como una función que acepta una solicitud como entrada, o puede especificarse como una función lambda en su lugar. De las funciones útiles adicionales, Mux.notfound() presente para enviar la respuesta Not found especificada. Y el resultado que debe enviarse al cliente no necesita empaquetarse en HTTP.Response . HTTP.Response , como hicimos en el ejemplo anterior, ya que Mux lo hará él mismo. Sin embargo, aún tiene que hacer el análisis JSON usted mismo, al igual que serializar el objeto para la respuesta: 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 respuesta se envía como una matriz asociativa con los campos :body :headers .


Iniciar el servidor con el método de serve(test, 8080) es asíncrono, por lo que una de las opciones en Julia para organizar la espera de finalización es llamar al código:


 Base.JLOptions().isinteractive == 0 && wait() 

De lo contrario, el servicio hace lo mismo que la versión anterior en HTTP.jl


Respuesta de control para el servicio:


 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"} 

Paquete Bukdu.jl


El paquete se desarrolló bajo la influencia del marco Phoenix, que, a su vez, se implementa en Elixir y es la implementación de ideas de construcción web de la comunidad Ruby en proyección sobre Elixir. El proyecto se está desarrollando de manera bastante activa y se posiciona como una herramienta para crear una API RESTful y aplicaciones web livianas. Hay funciones para simplificar la serialización y deserialización de JSON. Esto falta en HTTP.jl y Mux.jl Veamos la implementación de nuestro servicio web.


 #!/usr/bin/env julia --project=@. using Bukdu using JSON #   struct WelcomeController <: ApplicationController conn::Conn end #   index(c::WelcomeController) = render(JSON, "Hello World") welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user) function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end #   routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end #   Bukdu.start(8080) Base.JLOptions().isinteractive == 0 && wait() 

Lo primero a lo que debe prestar atención es a la declaración de la estructura para almacenar el estado del controlador.


 struct WelcomeController <: ApplicationController conn::Conn end 

En este caso, es un tipo concreto creado como descendiente del tipo abstracto ApplicationController .


Los métodos para el controlador se declaran de manera similar con respecto a implementaciones anteriores. Hay una ligera diferencia en el controlador de nuestro objeto JSON.


 function process_resource(c::WelcomeController) message = JSON.parse(String(c.conn.request.body)) @info message message["server_mark"] = "confirmed" render(JSON, message) end 

Como puede ver, la deserialización también se realiza de forma independiente utilizando el método JSON.parse , pero el método de render(JSON, message) JSON.parse se utiliza para serializar la respuesta.


La declaración de rutas se lleva a cabo en el estilo tradicional para los rubistas, incluido el uso del bloque do...end .


 routes() do get("/", WelcomeController, index) get("/user/:user", WelcomeController, welcome_user, :user => String) post("/resource/process", WelcomeController, process_resource) end 

Además, de la manera tradicional para los rubistas, se declara un segmento en la línea de ruta /user/:user . En otras palabras, la parte variable de la expresión, cuyo acceso puede realizarse mediante el nombre especificado en la plantilla. Se designa sintácticamente como un representante de tipo Symbol . Por cierto, para Julia, el tipo Symbol significa, en esencia, lo mismo que para Ruby: esta es una cadena inmutable, representada en la memoria por una sola instancia.


En consecuencia, después de declarar una ruta con una parte variable, y también indicar el tipo de esta parte variable, podemos referirnos a los datos ya analizados por el nombre asignado. En el método que procesa la solicitud, simplemente accedemos al campo a través de un punto en el formulario c.params.user .


 welcome_user(c::WelcomeController) = render(JSON, "Hello " * c.params.user) 

Respuesta de control para el servicio:


 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"} 

Conclusión del servicio a la consola:


 >./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 

Paquete Genie.jl


Un ambicioso proyecto posicionado como un marco web MVC. En su enfoque, los "carriles" en Julia son claramente visibles, incluida la estructura de directorios creada por el generador. El proyecto se está desarrollando, sin embargo, por razones desconocidas, este paquete no está incluido en el repositorio de paquetes de Julia. Es decir, su instalación solo es posible desde el repositorio git con el comando:


 julia>] # switch to pkg> mode pkg> add https://github.com/essenciary/Genie.jl 

El código de nuestro servicio en Genie es el siguiente (no utilizamos generadores):


 #!/usr/bin/env julia --project=@. #     import Genie import Genie.Router: route, @params, POST import Genie.Requests: jsonpayload, rawpayload import Genie.Renderer: json! #      route("/") do "Hello World!" end route("/user/:user") do "Hello " * @params(:user) end route("/resource/process", method = POST) do message = jsonpayload() # if message == nothing # dump(Genie.Requests.rawpayload()) # end message["server_mark"] = "confirmed" return message |> json! end #   Genie.AppServer.startup(8080) Base.JLOptions().isinteractive == 0 && wait() 

Aquí debe prestar atención al formato de la declaración.


 route("/") do "Hello World!" end 

Este código es muy familiar para los programadores de Ruby. El bloque do...end como manejador y la ruta como argumento del método. Tenga en cuenta que para Julia este código puede reescribirse en la forma:


 route(req -> "Hello World!", "/") 

Es decir, la función del controlador está en primer lugar, la ruta está en el segundo. Pero para nuestro caso, dejemos el estilo rubí.


Genie empaqueta automáticamente el resultado de la ejecución en una respuesta HTTP. En el caso mínimo, solo necesitamos devolver el resultado del tipo correcto, por ejemplo String. De los servicios adicionales, se implementa una verificación automática del formato de entrada y su análisis. Por ejemplo, para JSON, solo necesita llamar al método jsonpayload() .


 route("/resource/process", method = POST) do message = jsonpayload() # if message == nothing # dump(Genie.Requests.rawpayload()) # end message["server_mark"] = "confirmed" return message |> json! end 

Presta atención al fragmento de código comentado aquí. El método jsonpayload() nothing devuelve nothing si por alguna razón el formato de entrada no se reconoce como JSON. Tenga en cuenta que solo por esto, el encabezado [("Content-Type" => "application/json")] agrega a nuestro cliente HTTP, porque de lo contrario Genie ni siquiera comenzará a analizar los datos como JSON. En caso de que algo incomprensible haya llegado, es útil mirar rawpayload() para saber qué es. Sin embargo, dado que esta es solo una fase de depuración, no debe dejarla en el código.


Además, debe prestar atención a devolver el resultado en el message |> json! formato message |> json! . El método json!(str) se coloca aquí en último lugar. Proporciona la serialización de datos en formato JSON y también garantiza que Genie agregue el tipo de Content-Type correcto. Además, preste atención al hecho de que la palabra return en la mayoría de los casos en los ejemplos anteriores es redundante. Julia, como Ruby, por ejemplo, siempre devuelve el resultado de la última operación o el valor de la última expresión especificada. Es decir, la palabra return es opcional.


Las capacidades de Genie no terminan ahí, pero no las necesitamos para implementar un servicio web.


Respuesta de control para el servicio:


 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"} 

Conclusión del servicio a la consola:


 >./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 

Paquete JuliaWebAPI.jl


Este paquete se posicionó como una capa intermedia para crear aplicaciones web en aquellos días en que HTTP.jl era solo una biblioteca que implementa el protocolo. El autor de este paquete también implementó un generador de código de servidor basado en la especificación Swagger (OpenAPI y http://editor.swagger.io/ ) - vea el proyecto https://github.com/JuliaComputing/Swagger.jl , y este generador usó JuliaWebAPI .jl. Sin embargo, el problema con JuliaWebAPI.jl es que no implementa la capacidad de procesar el cuerpo de la solicitud (por ejemplo, JSON) enviada al servidor a través de una solicitud POST. El autor cree que pasar parámetros en un formato clave-valor es adecuado para todas las ocasiones ... El futuro de este paquete no está claro. Todas sus funciones ya están implementadas en muchos otros paquetes, incluido HTTP.jl. El paquete Swagger.jl tampoco lo usa más.


WebSockets.jl


Una implementación temprana del protocolo WebSocket. El paquete se ha utilizado durante mucho tiempo como la implementación principal de este protocolo, sin embargo, en la actualidad, su implementación está incluida en el paquete HTTP.jl. El paquete WebSockets.jl usa HTTP.jl para establecer una conexión, sin embargo, ahora no vale la pena usarlo en nuevos desarrollos. Debe considerarse como un paquete de compatibilidad.


Conclusión


Esta revisión muestra varias formas de implementar un servicio web en Julia. La forma más fácil y universal es usar directamente el paquete HTTP.jl. Además, los paquetes Bukdu.jl y Genie.jl son muy útiles. Como mínimo, su desarrollo debe ser monitoreado. Con respecto al paquete Mux.jl, sus ventajas ahora se están disolviendo en el contexto de HTTP.jl. Por lo tanto, la opinión personal es no usarlo. Genie.jl es un marco muy prometedor. Sin embargo, antes de comenzar a usarlo, al menos debe comprender por qué el autor no lo registra como un paquete oficial.


Tenga en cuenta que el código de deserialización JSON en los ejemplos se utilizó sin manejo de errores. En todos los casos, excepto Genie, es necesario manejar los errores de análisis e informar al usuario sobre esto. Un ejemplo de dicho código para 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 general, podemos decir que ya hay suficientes fondos para crear servicios web en Julia. Es decir, no hay necesidad de "reinventar la rueda" para escribirlos. El siguiente paso es evaluar cómo Julia puede soportar la carga en los puntos de referencia existentes, si alguien está listo para tomarla. Sin embargo, por ahora detengámonos en esta revisión.


Referencias


Source: https://habr.com/ru/post/449338/


All Articles