Julia. Serviços Web


Continuamos a considerar as tecnologias Julia. E hoje falaremos sobre pacotes projetados para criar serviços da web. Não é segredo que o principal nicho da linguagem Julia é a computação de alto desempenho. Portanto, uma etapa bastante lógica é criar diretamente serviços da Web capazes de executar esses cálculos sob demanda. Obviamente, os serviços da Web não são a única maneira de se comunicar em um ambiente de rede. Porém, como agora eles são os mais amplamente utilizados em sistemas distribuídos, consideraremos a criação de serviços que atendem a solicitações HTTP.


Observe que, devido à juventude de Julia, há um conjunto de pacotes concorrentes. Portanto, tentaremos descobrir como e por que usá-los. Ao longo do caminho, comparamos a implementação do mesmo serviço da web JSON com a ajuda deles.


A infraestrutura de Julia vem se desenvolvendo ativamente nos últimos dois anos. E, neste caso, essa não é apenas uma frase on-line inscrita para um belo começo do texto, mas uma ênfase no fato de que tudo está mudando intensamente, e o que era relevante há alguns anos agora está desatualizado. No entanto, tentaremos destacar pacotes estáveis ​​e fornecer recomendações sobre como implementar serviços da Web com sua ajuda. Por definição, criaremos um serviço da web que aceita uma solicitação POST com dados JSON no seguinte formato:


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

Assumimos que o serviço que criamos não é RESTful. Nossa principal tarefa é considerar com precisão os métodos para descrever rotas e manipular solicitações.


Pacote HTTP.jl


Este pacote é a principal implementação do protocolo HTTP na Julia e é gradualmente coberto de novos recursos. Além de implementar estruturas e funções típicas para executar solicitações de clientes HTTP, este pacote também implementa funções para criar servidores HTTP. Ao mesmo tempo, à medida que se desenvolve, o pacote recebeu funções que tornam bastante conveniente para o programador registrar manipuladores e, assim, criar serviços típicos. Além disso, nas versões mais recentes, há suporte interno para o protocolo WebSocket, cuja implementação foi feita anteriormente como parte de um pacote separado WebSocket.jl. Ou seja, o HTTP.jl, atualmente, pode satisfazer a maioria das necessidades de um programador. Vejamos alguns exemplos com mais detalhes.


Cliente HTTP


Iniciamos a implementação com o código do cliente, que usaremos para verificar a operacionalidade.


 #!/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) 

O pacote HTTP fornece métodos que correspondem aos nomes dos comandos do protocolo HTTP. Nesse caso, usamos get e post . O argumento nomeado opcional verbose permite definir a quantidade de informações de depuração a serem produzidas. Assim, por exemplo, verbose=1 produzirá:


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

E no caso de verbose=3 já temos um conjunto completo de dados transmitidos e recebidos:


 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) 

No futuro, usaremos apenas verbose=1 para ver apenas informações mínimas sobre o que está acontecendo.


Alguns comentários sobre o código.


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

Como declaramos anteriormente a estrutura do documento (além disso, imutável), um construtor está disponível para ela por padrão, cujos argumentos correspondem aos campos declarados da estrutura. Para convertê-lo em JSON, usamos o pacote JSON.jl e seu método json(doc) .
Preste atenção ao fragmento:


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

Como estamos transmitindo JSON, você deve especificar explicitamente o tipo application/json no cabeçalho Content-Type . Os cabeçalhos são passados ​​para o método HTTP.post (no entanto, como todos os outros) usando uma matriz (do tipo Vector, mas não Dict) contendo os pares nome-valor do cabeçalho.


Para um teste de saúde, realizaremos três consultas:


  • Solicitação GET para a rota raiz;
  • Solicitação GET no formato / usuário / nome, em que nome é o nome transmitido;
  • Pedido / recurso / processo POST com um objeto JSON passado. Esperamos receber o mesmo documento, mas com o campo server_mark adicionado.

Usaremos esse código do cliente para testar todas as opções de implementação do servidor.


Servidor HTTP


Depois de descobrir o cliente, é hora de começar a implementar o servidor. Para começar, faremos o serviço apenas com a ajuda do HTTP.jl para mantê-lo como uma opção básica, que não requer a instalação de outros pacotes. Lembramos que todos os outros pacotes usam 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) 

No exemplo, você deve prestar atenção ao seguinte código:


 dump(req) 

imprime no console tudo o que é conhecido pelo objeto. Incluindo tipos de dados, valores, bem como todos os campos aninhados e seus valores. Este método é útil para pesquisa e depuração de bibliotecas.


String


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

é uma expressão regular que analisa a rota na qual o manipulador está registrado. O pacote HTTP.jl não fornece maneiras automáticas de identificar um padrão em uma rota.


Dentro do manipulador process_resource , analisamos o JSON aceito pelo serviço.


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

Os dados são acessados ​​através do campo req.body . Observe que os dados são fornecidos em um formato de matriz de bytes. Portanto, para trabalhar com eles como uma sequência, é executada uma conversão explícita em uma sequência. O método JSON.parse é um método de pacote JSON.jl que desserializa dados e cria um objeto. Como o objeto nesse caso é uma matriz associativa (Dict), podemos facilmente adicionar uma nova chave a ele. String


 message["server_mark"] = "confirmed" 

adiciona a chave server_mark com o valor confirmed .


O serviço HTTP.serve(ROUTER, Sockets.localhost, 8080) quando a linha HTTP.serve(ROUTER, Sockets.localhost, 8080) é HTTP.serve(ROUTER, Sockets.localhost, 8080) .


A resposta de controle para o serviço baseado em HTTP.jl (obtida ao executar o código do cliente com 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"} 

No contexto de informações de depuração com verbose=1 , podemos ver claramente as linhas: Hello World , Hello Jemand , "server_mark":"confirmed" .


Depois de visualizar o código de serviço, surge uma pergunta natural - por que precisamos de todos os outros pacotes, se tudo é tão simples em HTTP. Há uma resposta muito simples para isso. HTTP - permite registrar manipuladores dinâmicos, mas mesmo uma implementação elementar da leitura de um arquivo de imagem estática de um diretório requer uma implementação separada. Portanto, também consideramos pacotes focados na criação de aplicativos da web.


Pacote Mux.jl


Este pacote está posicionado como uma camada intermediária para aplicativos da web implementados na Julia. Sua implementação é muito leve. O principal objetivo é fornecer uma maneira fácil de descrever manipuladores. Isso não quer dizer que o projeto não esteja em desenvolvimento, mas está se desenvolvendo lentamente. No entanto, observe o código do nosso serviço que atende as mesmas rotas.


 #!/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() 

Aqui as rotas são descritas usando o método de page . O aplicativo da web é declarado usando a macro @app . Os argumentos para o método de page são a rota e o manipulador. Um manipulador pode ser especificado como uma função que aceita uma solicitação como entrada ou pode ser especificado como uma função lambda no local. Das funções úteis adicionais, Mux.notfound() presente para enviar a resposta Not found especificada. E o resultado que deve ser enviado ao cliente não precisa ser empacotado no HTTP.Response , como fizemos no exemplo anterior, pois o Mux fará isso sozinho. No entanto, você ainda precisa analisar o JSON, assim como serializar o objeto para a resposta - JSON.json(message) .


  message = JSON.parse(String(req[:data])) message["server_mark"] = "confirmed" return Dict( :body => JSON.json(message), :headers => [("Content-Type" => "application/json")] ) 

A resposta é enviada como uma matriz associativa com os campos :body :headers .


O início do servidor com o método serve(test, 8080) é assíncrono; portanto, uma das opções em Julia para organizar a espera pela conclusão é chamar o código:


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

Caso contrário, o serviço fará o mesmo que a versão anterior no HTTP.jl


Resposta de controle para o serviço:


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

Pacote Bukdu.jl


O pacote foi desenvolvido sob a influência da estrutura Phoenix, que, por sua vez, é implementada no Elixir e é a implementação de idéias de criação de web da comunidade Ruby em projeção no Elixir. O projeto está se desenvolvendo de maneira bastante ativa e está posicionado como uma ferramenta para criar uma API RESTful e aplicativos da Web leves. Existem funções para simplificar a serialização e desserialização JSON. Isso está ausente no HTTP.jl e no Mux.jl Vejamos a implementação do nosso serviço da 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() 

A primeira coisa que você deve prestar atenção é a declaração da estrutura para armazenar o estado do controlador.


 struct WelcomeController <: ApplicationController conn::Conn end 

Nesse caso, é um tipo concreto criado como descendente do tipo abstrato ApplicationController .


Os métodos para o controlador são declarados de maneira semelhante com relação às implementações anteriores. Há uma pequena diferença no manipulador do nosso 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 você pode ver, a desserialização também é executada independentemente usando o método JSON.parse , mas o método de render(JSON, message) JSON.parse é usado para serializar a resposta.


A declaração de rotas é realizada no estilo tradicional para rubists, incluindo o uso de bloco do...end .


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

Além disso, da maneira tradicional para rubists, um segmento é declarado na linha de rota /user/:user . Em outras palavras, a parte variável da expressão, cujo acesso pode ser executado pelo nome especificado no modelo. É sintaticamente designado como um representante do tipo Symbol . A propósito, para Julia, o tipo Symbol significa, em essência, o mesmo que para Ruby - essa é uma string imutável, representada na memória por uma única instância.


Assim, depois que declaramos uma rota com uma parte variável e também indicamos o tipo dessa parte variável, podemos nos referir aos dados já analisados ​​pelo nome atribuído. No método que processa a solicitação, simplesmente c.params.user o campo através de um ponto no formato c.params.user .


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

Resposta de controle para o serviço:


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

Conclusão do serviço para o 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 

Pacote Genie.jl


Um projeto ambicioso posicionado como uma estrutura da Web MVC. Em sua abordagem, os “Rails” em Julia são bem visíveis, incluindo a estrutura de diretórios criada pelo gerador. O projeto está sendo desenvolvido, no entanto, por razões desconhecidas, este pacote não está incluído no repositório de pacotes Julia. Ou seja, sua instalação é possível apenas no repositório git com o comando:


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

O código do nosso serviço no Genie é o seguinte (não usamos geradores):


 #!/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() 

Aqui você deve prestar atenção ao formato da declaração.


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

Este código é muito familiar para programadores Ruby. O bloco do...end como manipulador e a rota como argumento para o método. Observe que, para Julia, esse código pode ser reescrito no formato:


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

Ou seja, a função de manipulador está em primeiro lugar, a rota está em segundo. Mas, no nosso caso, vamos deixar o estilo ruby.


O Genie empacota automaticamente o resultado da execução em uma resposta HTTP. No caso mínimo, precisamos retornar apenas o resultado do tipo correto, por exemplo, String. Das comodidades adicionais, uma verificação automática do formato de entrada e sua análise é implementada. Por exemplo, para JSON, você só precisa chamar o 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 

Preste atenção ao fragmento de código comentado aqui. O método jsonpayload() nothing retorna nothing se, por algum motivo, o formato de entrada não for reconhecido como JSON. Observe que, para isso, o cabeçalho [("Content-Type" => "application/json")] adicionado ao nosso cliente HTTP, porque, caso contrário, o Genie nem começará a analisar os dados como JSON. Caso algo incompreensível chegue, é útil olhar para rawpayload() como ele é. No entanto, como essa é apenas uma fase de depuração, você não deve deixá-la no código.


Além disso, você deve prestar atenção ao retornar o resultado na message |> json! formato message |> json! . O método json!(str) é colocado por último no pipeline aqui. Ele fornece serialização de dados no formato JSON e também garante que o Genie adicione o Content-Type correto. Além disso, preste atenção ao fato de que a palavra return na maioria dos casos nos exemplos acima é redundante. Julia, como Ruby, por exemplo, sempre retorna o resultado da última operação ou o valor da última expressão especificada. Ou seja, a palavra return é opcional.


Os recursos da Genie não param por aí, mas não precisamos deles para implementar um serviço da web.


Resposta de controle para o serviço:


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

Conclusão do serviço para o 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 

Pacote JuliaWebAPI.jl


Este pacote foi posicionado como uma camada intermediária para criar aplicativos da Web naqueles dias em que o HTTP.jl era apenas uma biblioteca que implementa o protocolo. O autor deste pacote também implementou um gerador de código de servidor baseado na especificação Swagger (OpenAPI e http://editor.swagger.io/ ) - veja o projeto https://github.com/JuliaComputing/Swagger.jl , e este gerador usou JuliaWebAPI .jl. No entanto, o problema com o JuliaWebAPI.jl é que ele não implementa a capacidade de processar o corpo de uma solicitação (por exemplo, JSON), transmitida ao servidor por meio de uma solicitação POST. O autor acreditava que a passagem de parâmetros em um formato de valor-chave é adequada para todas as ocasiões ... O futuro deste pacote não está claro. Todas as suas funções já estão implementadas em muitos outros pacotes, incluindo HTTP.jl. O pacote Swagger.jl também não o usa mais.


WebSockets.jl


Uma implementação antecipada do protocolo WebSocket. O pacote é usado há muito tempo como a principal implementação deste protocolo, no entanto, atualmente, sua implementação está incluída no pacote HTTP.jl. O próprio pacote WebSockets.jl usa o HTTP.jl para estabelecer uma conexão, no entanto, agora, não vale a pena usá-lo em novos desenvolvimentos. Deve ser considerado como um pacote para compatibilidade.


Conclusão


Esta revisão demonstra várias maneiras de implementar um serviço da Web em Julia. A maneira mais fácil e universal é usar diretamente o pacote HTTP.jl. Além disso, os pacotes Bukdu.jl e Genie.jl são muito úteis. No mínimo, seu desenvolvimento deve ser monitorado. Com relação ao pacote Mux.jl, suas vantagens estão sendo dissolvidas no contexto do HTTP.jl. Portanto, a opinião pessoal não deve ser usada. Genie.jl é uma estrutura muito promissora. No entanto, antes de começar a usá-lo, você deve pelo menos entender por que o autor não o registra como um pacote oficial.


Observe que o código de desserialização JSON nos exemplos foi usado sem tratamento de erros. Em todos os casos, exceto no Genie, é necessário lidar com erros de análise e informar o usuário sobre isso. Um exemplo desse 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 

Em geral, podemos dizer que já existem fundos suficientes para criar serviços da web em Julia. Ou seja, não há necessidade de "reinventar a roda" para escrevê-los. O próximo passo é avaliar como Julia pode suportar a carga nos benchmarks existentes, se alguém estiver pronto para aceitá-la. No entanto, por enquanto, vamos nos debruçar sobre essa revisão.


Referências


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


All Articles